https://github.com/furkansarikaya/fs.entityframework.library
A comprehensive, production-ready Entity Framework Core library for .NET 9+ providing Repository pattern, Unit of Work, Specification pattern, Domain Events, Fluent Configuration API, automatic audit tracking, soft delete & restore, dynamic filtering, pagination, and modular ID generation (GUID V7, ULID) with zero-configuration setup.
https://github.com/furkansarikaya/fs.entityframework.library
audit-tracking clean-architecture csharp domain-events dotnet dynamic-filtering entity-framework-core extensible fluent-api guid-v7 id-generation nuget-package pagination production-ready repository-pattern soft-delete specification-pattern ulid unit-of-work zero-configuration
Last synced: about 2 months ago
JSON representation
A comprehensive, production-ready Entity Framework Core library for .NET 9+ providing Repository pattern, Unit of Work, Specification pattern, Domain Events, Fluent Configuration API, automatic audit tracking, soft delete & restore, dynamic filtering, pagination, and modular ID generation (GUID V7, ULID) with zero-configuration setup.
- Host: GitHub
- URL: https://github.com/furkansarikaya/fs.entityframework.library
- Owner: furkansarikaya
- License: mit
- Created: 2025-06-24T21:16:50.000Z (11 months ago)
- Default Branch: main
- Last Pushed: 2025-08-03T21:54:00.000Z (10 months ago)
- Last Synced: 2025-08-27T05:31:36.202Z (9 months ago)
- Topics: audit-tracking, clean-architecture, csharp, domain-events, dotnet, dynamic-filtering, entity-framework-core, extensible, fluent-api, guid-v7, id-generation, nuget-package, pagination, production-ready, repository-pattern, soft-delete, specification-pattern, ulid, unit-of-work, zero-configuration
- Language: C#
- Homepage: https://www.nuget.org/packages/FS.EntityFramework.Library
- Size: 252 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# FS.EntityFramework.Library
[](https://www.nuget.org/packages/FS.EntityFramework.Library/)
[](https://www.nuget.org/packages/FS.EntityFramework.Library/)
[](https://github.com/furkansarikaya/FS.EntityFramework.Library/blob/main/LICENSE)
[](https://github.com/furkansarikaya/FS.EntityFramework.Library/stargazers)
A comprehensive, production-ready Entity Framework Core library providing **Repository pattern**, **Unit of Work**, **Specification pattern**, **dynamic filtering**, **pagination support**, **Domain Events**, **Domain-Driven Design (DDD)**, **Fluent Configuration API**, and **modular ID generation** strategies for .NET applications.
## π Why Choose FS.EntityFramework.Library?
This library transforms Entity Framework Core into a powerful, enterprise-ready data access layer that follows best practices and design patterns. Whether you're building a simple application or a complex domain-rich system, this library provides the tools you need to create maintainable, testable, and scalable data access code.
## π Table of Contents
- [π Quick Start](#-quick-start)
- [πΎ Installation](#-installation)
- [ποΈ Step-by-Step Implementation Guide](#οΈ-step-by-step-implementation-guide)
- [ποΈ Domain-Driven Design Features](#οΈ-domain-driven-design-features)
- [π Advanced Features](#-advanced-features)
- [π― Best Practices](#-best-practices)
- [π§ Troubleshooting](#-troubleshooting)
- [π€ Contributing](#-contributing)
## π Quick Start
Get started with FS.EntityFramework.Library in just 5 steps:
### Step 1: Install the Package
```bash
dotnet add package FS.EntityFramework.Library
```
### Step 2: Configure Your DbContext
```csharp
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions options) : base(options) { }
public DbSet Products { get; set; }
public DbSet Categories { get; set; }
}
```
### Step 3: Configure Services
```csharp
// In Program.cs or Startup.cs
services.AddDbContext(options =>
options.UseSqlServer(connectionString));
// Add FS.EntityFramework services
services.AddFSEntityFramework()
.Build();
```
### Step 4: Create Your First Entity
```csharp
public class Product : BaseAuditableEntity
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Description { get; set; } = string.Empty;
}
```
### Step 5: Use in Your Services
```csharp
public class ProductService
{
private readonly IUnitOfWork _unitOfWork;
public ProductService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task CreateProductAsync(string name, decimal price)
{
var repository = _unitOfWork.GetRepository();
var product = new Product { Name = name, Price = price };
await repository.AddAsync(product);
await _unitOfWork.SaveChangesAsync();
return product;
}
}
```
## πΎ Installation
### Core Package
```bash
# Core library with all essential features including DDD
dotnet add package FS.EntityFramework.Library
```
### Extension Packages (Optional)
```bash
# GUID Version 7 ID generation (.NET 10+)
dotnet add package FS.EntityFramework.Library.GuidV7
# ULID ID generation
dotnet add package FS.EntityFramework.Library.UlidGenerator
```
### Requirements
- **.NET 10.0** or later
- **Entity Framework Core 10.0.3** or later
- **Microsoft.AspNetCore.Http.Abstractions 2.3.0** or later (for HttpContext support)
## ποΈ Step-by-Step Implementation Guide
Let's build a complete example from scratch, implementing all the major features of the library.
### Step 1: Set Up Your Project Structure
First, create a new project and organize it following clean architecture principles:
```
YourProject/
βββ Models/ # Entity models
βββ Services/ # Business logic
βββ Repositories/ # Custom repositories (if needed)
βββ Configuration/ # Database configuration
```
### Step 2: Install Required Packages
```bash
dotnet new webapi -n YourProject
cd YourProject
dotnet add package FS.EntityFramework.Library
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
```
### Step 3: Create Base Entities
Understanding the entity hierarchy is crucial. The library provides several base entity classes:
```csharp
// Models/Category.cs
using FS.EntityFramework.Library.Common;
///
/// Simple entity with just ID and domain events support
///
public class Category : BaseEntity
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
// Navigation property
public virtual ICollection Products { get; set; } = new List();
}
// Models/Product.cs
using FS.EntityFramework.Library.Common;
///
/// Auditable entity with creation and modification tracking
///
public class Product : BaseAuditableEntity, ISoftDelete
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Description { get; set; } = string.Empty;
public int CategoryId { get; set; }
// Navigation property
public virtual Category Category { get; set; } = null!;
// ISoftDelete properties (automatically implemented)
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public string? DeletedBy { get; set; }
// Business method with domain events
public void UpdatePrice(decimal newPrice)
{
if (newPrice <= 0)
throw new ArgumentException("Price must be positive", nameof(newPrice));
var oldPrice = Price;
Price = newPrice;
// Raise domain event
AddDomainEvent(new ProductPriceChangedEvent(Id, oldPrice, newPrice));
}
}
```
### Step 4: Create Domain Events
Domain events enable loose coupling between different parts of your application:
```csharp
// Models/Events/ProductPriceChangedEvent.cs
using FS.EntityFramework.Library.Common;
public class ProductPriceChangedEvent : DomainEvent
{
public ProductPriceChangedEvent(int productId, decimal oldPrice, decimal newPrice)
{
ProductId = productId;
OldPrice = oldPrice;
NewPrice = newPrice;
}
public int ProductId { get; }
public decimal OldPrice { get; }
public decimal NewPrice { get; }
}
// Services/EventHandlers/ProductPriceChangedEventHandler.cs
using FS.EntityFramework.Library.Events;
public class ProductPriceChangedEventHandler : IDomainEventHandler
{
private readonly ILogger _logger;
public ProductPriceChangedEventHandler(ILogger logger)
{
_logger = logger;
}
public async Task Handle(ProductPriceChangedEvent domainEvent, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Product {ProductId} price changed from {OldPrice} to {NewPrice}",
domainEvent.ProductId, domainEvent.OldPrice, domainEvent.NewPrice);
// Add your business logic here:
// - Send price change notification emails
// - Update related data
// - Trigger other business processes
await Task.CompletedTask;
}
}
```
### Step 5: Configure Your DbContext
You have two options for DbContext configuration:
#### Option A: Use FSDbContext (Recommended)
```csharp
// Data/ApplicationDbContext.cs
using FS.EntityFramework.Library.Common;
public class ApplicationDbContext : FSDbContext
{
public ApplicationDbContext(DbContextOptions options, IServiceProvider serviceProvider)
: base(options, serviceProvider)
{
// FSDbContext automatically applies all FS.EntityFramework configurations
}
public DbSet Products { get; set; } = null!;
public DbSet Categories { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder); // This applies FS configurations
// Add your custom configurations here
modelBuilder.Entity(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).HasMaxLength(200).IsRequired();
entity.Property(e => e.Price).HasPrecision(18, 2);
entity.HasOne(e => e.Category)
.WithMany(c => c.Products)
.HasForeignKey(e => e.CategoryId);
});
modelBuilder.Entity(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).HasMaxLength(100).IsRequired();
});
}
}
```
#### Option B: Use Regular DbContext with Manual Configuration
```csharp
// Data/ApplicationDbContext.cs
public class ApplicationDbContext : DbContext
{
private readonly IServiceProvider? _serviceProvider;
public ApplicationDbContext(DbContextOptions options, IServiceProvider serviceProvider)
: base(options)
{
_serviceProvider = serviceProvider;
}
public DbSet Products { get; set; } = null!;
public DbSet Categories { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Apply FS.EntityFramework configurations manually
if (_serviceProvider != null)
{
modelBuilder.ApplyFSEntityFrameworkConfigurations(_serviceProvider);
}
// Your entity configurations...
}
}
```
### Step 6: Configure Services with Fluent API
The Fluent Configuration API provides a clean way to configure all features:
```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add DbContext
builder.Services.AddDbContext(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Configure FS.EntityFramework with all features
builder.Services.AddFSEntityFramework()
// Enable audit tracking
.WithAudit()
.UsingHttpContext() // For web applications
// Enable domain events
.WithDomainEvents()
.UsingDefaultDispatcher()
.WithAutoHandlerDiscovery() // Automatically find event handlers
.Complete()
// Enable soft delete
.WithSoftDelete()
// Build the configuration
.Build();
var app = builder.Build();
```
### Step 7: Create Business Services
Now create services that use the repository pattern:
```csharp
// Services/ProductService.cs
public class ProductService
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger _logger;
public ProductService(IUnitOfWork unitOfWork, ILogger logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
}
public async Task CreateProductAsync(CreateProductRequest request)
{
var repository = _unitOfWork.GetRepository();
var product = new Product
{
Name = request.Name,
Price = request.Price,
Description = request.Description,
CategoryId = request.CategoryId
};
await repository.AddAsync(product);
await _unitOfWork.SaveChangesAsync();
_logger.LogInformation("Created product: {ProductName}", product.Name);
return product;
}
public async Task GetProductByIdAsync(int id)
{
var repository = _unitOfWork.GetRepository();
return await repository.GetByIdAsync(id);
}
public async Task> GetProductsPagedAsync(int page, int size)
{
var repository = _unitOfWork.GetRepository();
return await repository.GetPagedAsync(
pageIndex: page,
pageSize: size,
includes: new List>> { p => p.Category },
orderBy: query => query.OrderBy(p => p.Name)
);
}
public async Task UpdateProductPriceAsync(int id, decimal newPrice)
{
var repository = _unitOfWork.GetRepository();
var product = await repository.GetByIdAsync(id);
if (product == null)
throw new InvalidOperationException($"Product with ID {id} not found");
product.UpdatePrice(newPrice); // This will raise a domain event
await repository.UpdateAsync(product);
await _unitOfWork.SaveChangesAsync(); // Domain events will be dispatched here
}
public async Task SoftDeleteProductAsync(int id)
{
var repository = _unitOfWork.GetRepository();
var product = await repository.GetByIdAsync(id);
if (product != null)
{
await repository.DeleteAsync(product); // Soft delete
await _unitOfWork.SaveChangesAsync();
}
}
public async Task RestoreProductAsync(int id)
{
var repository = _unitOfWork.GetRepository();
await repository.RestoreAsync(id); // Restore soft deleted product
await _unitOfWork.SaveChangesAsync();
}
}
// DTOs for service methods
public record CreateProductRequest(string Name, decimal Price, string Description, int CategoryId);
```
### Step 8: Implement Dynamic Filtering
The library provides a comprehensive, type-safe dynamic filtering system with fluent API, OR/AND groups, sorting, convenience methods, and reusable scopes.
#### Strongly-Typed FilterBuilder\ (Recommended)
The generic `FilterBuilder` provides compile-time validated field names, IntelliSense, and automatic value conversion:
```csharp
using FS.EntityFramework.Library.Models;
public class ProductSearchService
{
private readonly IUnitOfWork _unitOfWork;
public ProductSearchService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task> SearchProductsAsync(ProductFilterRequest request)
{
var repository = _unitOfWork.GetRepository();
// Fully type-safe: field names, operators, and values validated at compile-time
var filter = FilterBuilder.Create()
.Search(request.SearchTerm)
.WhereIf(request.MinPrice.HasValue, p => p.Price,
FilterOperator.GreaterThanOrEqual, request.MinPrice)
.WhereIf(request.MaxPrice.HasValue, p => p.Price,
FilterOperator.LessThanOrEqual, request.MaxPrice)
.WhereIf(request.CategoryId.HasValue, p => p.CategoryId,
FilterOperator.Equals, request.CategoryId)
.WhereIsNull(p => p.DeletedAt)
.OrderByDescending(p => p.CreatedAt)
.OrderBy(p => p.Name)
.Build();
// Sorting is built into the filter model β no separate orderBy needed
return await repository.GetPagedWithFilterAsync(
filter,
request.Page,
request.PageSize,
includes: new List>> { p => p.Category }
);
}
}
public record ProductFilterRequest(
string? SearchTerm = null,
decimal? MinPrice = null,
decimal? MaxPrice = null,
int? CategoryId = null,
int Page = 1,
int PageSize = 10);
```
#### Convenience Methods: WhereBetween & WhereDateRange
```csharp
// Price range in a single call (adds >= and <= filters)
var filter = FilterBuilder.Create()
.WhereBetween(p => p.Price, 100m, 999m)
.WhereDateRange(p => p.CreatedAt, DateTime.Today.AddDays(-30), DateTime.Today)
.Build();
```
#### OR / AND Groups
Build complex logical expressions with grouped filters:
```csharp
var filter = FilterBuilder.Create()
.WhereEquals(p => p.IsActive, true)
.OrGroup(g => g // AND (
.WhereGreaterThan(p => p.Price, 1000m) // Price > 1000
.WhereEquals(p => p.IsFeatured, true)) // OR IsFeatured = true )
.Build();
// SQL: WHERE IsActive = 1 AND (Price > 1000 OR IsFeatured = 1)
```
#### Set Operators: WhereIn & WhereNotIn
```csharp
var filter = FilterBuilder.Create()
.WhereIn(p => p.Status, 1, 2, 3) // WHERE Status IN (1, 2, 3)
.WhereNotIn(p => p.CategoryId, 5, 10) // AND CategoryId NOT IN (5, 10)
.Build();
```
#### Reusable Filter Scopes
Extract common filter combinations into reusable scopes:
```csharp
// Define a reusable scope
public class ActiveProductScope : IFilterScope
{
public void Apply(FilterBuilder builder)
{
builder
.WhereEquals(p => p.IsActive, true)
.WhereIsNull(p => p.DeletedAt);
}
}
// Apply across different queries
var filter = FilterBuilder.Create()
.ApplyScope(new ActiveProductScope())
.WhereBetween(p => p.Price, 50m, 500m)
.OrderBy(p => p.Name)
.Build();
```
#### Dynamic Sorting via FilterModel
Sorting can be defined in the filter model and is automatically applied by the repository:
```csharp
var filter = FilterBuilder.Create()
.WhereGreaterThan(p => p.Price, 100m)
.OrderByDescending(p => p.CreatedAt) // Primary sort
.OrderBy(p => p.Name) // ThenBy
.Build();
// No orderBy parameter needed β sorts are applied from FilterModel.Sorts
var result = await repository.GetPagedWithFilterAsync(filter, pageIndex: 0, pageSize: 20);
// Explicit orderBy parameter still takes precedence when provided
```
#### String-Based FilterBuilder (API/JSON Scenarios)
The non-generic `FilterBuilder` is available for scenarios where the entity type isn't known at compile time (e.g., API controllers receiving filter JSON):
```csharp
var filter = FilterBuilder.Create()
.Search("laptop")
.WhereGreaterThanOrEqual(nameof(Product.Price), "500")
.WhereBetween(nameof(Product.Price), "100", "999")
.OrGroup(g => g
.WhereEquals("CategoryId", "1")
.WhereEquals("CategoryId", "2"))
.OrderByDescending(nameof(Product.CreatedAt))
.Build();
```
#### Direct FilterItem Constructor
```csharp
var filter = new FilterModel
{
SearchTerm = "laptop",
Filters = new List
{
new(nameof(Product.Price), FilterOperator.GreaterThanOrEqual, "500"),
new(nameof(Product.CategoryId), FilterOperator.Equals, "1"),
new(nameof(Product.Status), FilterOperator.In, "1,2,3"),
new(nameof(Product.DeletedAt), FilterOperator.IsNull)
}
};
```
#### Backward-Compatible String Syntax
The original string-based API still works and now supports short aliases:
```csharp
// All of these are equivalent:
new FilterItem { Field = "Price", Operator = "greaterthanorequal", Value = "500" }
new FilterItem { Field = "Price", Operator = "gte", Value = "500" }
new FilterItem("Price", FilterOperator.GreaterThanOrEqual, "500")
```
### Step 9: Create API Controllers
Finally, create controllers that expose your services:
```csharp
// Controllers/ProductsController.cs
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly ProductService _productService;
private readonly ProductSearchService _searchService;
public ProductsController(ProductService productService, ProductSearchService searchService)
{
_productService = productService;
_searchService = searchService;
}
[HttpPost]
public async Task> CreateProduct(CreateProductRequest request)
{
var product = await _productService.CreateProductAsync(request);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
[HttpGet("{id}")]
public async Task> GetProduct(int id)
{
var product = await _productService.GetProductByIdAsync(id);
return product == null ? NotFound() : Ok(product);
}
[HttpGet]
public async Task>> GetProducts(int page = 1, int size = 10)
{
var products = await _productService.GetProductsPagedAsync(page, size);
return Ok(products);
}
[HttpGet("search")]
public async Task>> SearchProducts([FromQuery] ProductFilterRequest request)
{
var products = await _searchService.SearchProductsAsync(request);
return Ok(products);
}
[HttpPut("{id}/price")]
public async Task UpdateProductPrice(int id, [FromBody] decimal newPrice)
{
await _productService.UpdateProductPriceAsync(id, newPrice);
return NoContent();
}
[HttpDelete("{id}")]
public async Task DeleteProduct(int id)
{
await _productService.SoftDeleteProductAsync(id);
return NoContent();
}
[HttpPost("{id}/restore")]
public async Task RestoreProduct(int id)
{
await _productService.RestoreProductAsync(id);
return NoContent();
}
}
```
### Step 10: Register Services
Don't forget to register your custom services:
```csharp
// Program.cs (continued)
builder.Services.AddScoped();
builder.Services.AddScoped();
```
## ποΈ Domain-Driven Design Features
The library provides comprehensive support for Domain-Driven Design patterns.
### Aggregate Roots
Aggregate Roots are the entry points to your aggregates and ensure consistency boundaries:
```csharp
using FS.EntityFramework.Library.Common;
using FS.EntityFramework.Library.Domain;
public class OrderAggregate : AggregateRoot
{
private readonly List _items = new();
public string OrderNumber { get; private set; } = string.Empty;
public decimal TotalAmount { get; private set; }
public DateTime OrderDate { get; private set; }
// Read-only access to items
public IReadOnlyCollection Items => _items.AsReadOnly();
// Factory method enforcing business rules
public static OrderAggregate Create(string orderNumber)
{
DomainGuard.AgainstNullOrWhiteSpace(orderNumber, nameof(orderNumber));
// AggregateRoot base class automatically generates Guid.CreateVersion7() in default constructor
var order = new OrderAggregate
{
OrderNumber = orderNumber,
OrderDate = DateTime.UtcNow,
TotalAmount = 0
};
// Raise domain event
order.RaiseDomainEvent(new OrderCreatedEvent(order.Id, orderNumber));
return order;
}
// Business method with domain logic
public void AddItem(string productName, decimal unitPrice, int quantity)
{
DomainGuard.AgainstNullOrWhiteSpace(productName, nameof(productName));
DomainGuard.AgainstNegativeOrZero(unitPrice, nameof(unitPrice));
DomainGuard.AgainstNegativeOrZero(quantity, nameof(quantity));
var item = new OrderItem(productName, unitPrice, quantity);
_items.Add(item);
RecalculateTotal();
RaiseDomainEvent(new OrderItemAddedEvent(Id, productName, quantity));
}
private void RecalculateTotal()
{
TotalAmount = _items.Sum(i => i.TotalPrice);
}
}
public class OrderItem
{
public string ProductName { get; }
public decimal UnitPrice { get; }
public int Quantity { get; }
public decimal TotalPrice => UnitPrice * Quantity;
public OrderItem(string productName, decimal unitPrice, int quantity)
{
ProductName = productName;
UnitPrice = unitPrice;
Quantity = quantity;
}
}
```
### Value Objects
Value Objects encapsulate business concepts and ensure type safety:
```csharp
using FS.EntityFramework.Library.Common;
using FS.EntityFramework.Library.Domain;
public class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency = "USD")
{
DomainGuard.AgainstNegative(amount, nameof(amount));
DomainGuard.AgainstNullOrWhiteSpace(currency, nameof(currency));
Amount = amount;
Currency = currency;
}
public static Money Zero => new(0);
public static Money FromDecimal(decimal amount) => new(amount);
// Value object operations
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Cannot add money with different currencies");
return new Money(Amount + other.Amount, Currency);
}
protected override IEnumerable GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
// Operators
public static Money operator +(Money left, Money right) => left.Add(right);
}
```
### Business Rules
Implement business rules for comprehensive domain validation:
```csharp
using FS.EntityFramework.Library.Domain;
// Simple business rule implementation
public class OrderMustHaveItemsRule : BusinessRule
{
private readonly IReadOnlyCollection _items;
public OrderMustHaveItemsRule(IReadOnlyCollection items)
{
_items = items;
}
public override bool IsBroken() => _items.Count == 0;
public override string Message => "Order must have at least one item";
public override string ErrorCode => "ORDER_NO_ITEMS";
}
// Complex business rule with dependencies
public class CustomerCreditLimitRule : BusinessRule
{
private readonly decimal _orderAmount;
private readonly decimal _currentCredit;
private readonly decimal _creditLimit;
public CustomerCreditLimitRule(decimal orderAmount, decimal currentCredit, decimal creditLimit)
{
_orderAmount = orderAmount;
_currentCredit = currentCredit;
_creditLimit = creditLimit;
}
public override bool IsBroken() => (_currentCredit + _orderAmount) > _creditLimit;
public override string Message =>
$"Order amount {_orderAmount:C} would exceed credit limit. Available credit: {(_creditLimit - _currentCredit):C}";
public override string ErrorCode => "CREDIT_LIMIT_EXCEEDED";
}
// Usage in aggregate with DomainGuard
public void ProcessOrder()
{
// Check multiple business rules
DomainGuard.Against(
new OrderMustHaveItemsRule(_items),
new CustomerCreditLimitRule(TotalAmount, _customer.CurrentCredit, _customer.CreditLimit)
);
// Alternative: Check individual rules
CheckRule(new OrderMustHaveItemsRule(_items));
// Process the order...
}
```
### Enhanced Domain Guard Usage
DomainGuard provides comprehensive validation utilities:
```csharp
using FS.EntityFramework.Library.Domain;
public class OrderAggregate : AggregateRoot
{
public void AddItem(string productName, decimal unitPrice, int quantity)
{
// Guard against null/empty values
DomainGuard.AgainstNullOrEmpty(productName, nameof(productName));
// Guard against invalid values
DomainGuard.Against(unitPrice <= 0, "Unit price must be positive", "INVALID_UNIT_PRICE");
DomainGuard.Against(quantity <= 0, "Quantity must be positive", "INVALID_QUANTITY");
// Guard against business rule violations
DomainGuard.Against(new MaxItemsPerOrderRule(_items.Count));
// Guard against null objects
var product = _productService.GetProduct(productName);
DomainGuard.AgainstNull(product, nameof(product));
// Business logic continues...
var item = new OrderItem(productName, unitPrice, quantity);
_items.Add(item);
RaiseDomainEvent(new OrderItemAddedEvent(Id, productName, quantity));
}
// Guard utilities for common scenarios
public void SetCustomerInfo(string customerId, string customerName)
{
DomainGuard.AgainstNullOrWhiteSpace(customerId, nameof(customerId));
DomainGuard.AgainstNullOrWhiteSpace(customerName, nameof(customerName));
DomainGuard.Against(customerId.Length > 50, "Customer ID too long", "CUSTOMER_ID_TOO_LONG");
_customerId = customerId;
_customerName = customerName;
}
}
```
### Domain Specifications
Build reusable domain logic with specifications and combine them for complex queries:
```csharp
using FS.EntityFramework.Library.Domain;
// Basic specification
public class ExpensiveProductsSpecification : DomainSpecification
{
private readonly decimal _minimumPrice;
public ExpensiveProductsSpecification(decimal minimumPrice)
{
_minimumPrice = minimumPrice;
}
public override bool IsSatisfiedBy(Product candidate)
{
return candidate.Price >= _minimumPrice;
}
public override Expression> ToExpression()
{
return product => product.Price >= _minimumPrice;
}
}
// Category-based specification
public class ProductsInCategorySpecification : DomainSpecification
{
private readonly int _categoryId;
public ProductsInCategorySpecification(int categoryId)
{
_categoryId = categoryId;
}
public override bool IsSatisfiedBy(Product candidate)
{
return candidate.CategoryId == _categoryId;
}
public override Expression> ToExpression()
{
return product => product.CategoryId == _categoryId;
}
}
// Available products specification
public class AvailableProductsSpecification : DomainSpecification
{
public override bool IsSatisfiedBy(Product candidate)
{
return !candidate.IsDeleted && candidate.Stock > 0;
}
public override Expression> ToExpression()
{
return product => !product.IsDeleted && product.Stock > 0;
}
}
// Specification combinations
public class ProductSearchService
{
private readonly IDomainRepository _repository;
public async Task> FindProductsAsync(ProductSearchCriteria criteria)
{
// Start with base specification
ISpecification specification = new AvailableProductsSpecification();
// Combine with price filter if specified
if (criteria.MinimumPrice.HasValue)
{
var priceSpec = new ExpensiveProductsSpecification(criteria.MinimumPrice.Value);
specification = specification.And(priceSpec);
}
// Combine with category filter if specified
if (criteria.CategoryId.HasValue)
{
var categorySpec = new ProductsInCategorySpecification(criteria.CategoryId.Value);
specification = specification.And(categorySpec);
}
// Execute combined specification
return await _repository.FindAsync(specification);
}
// Advanced specification combinations
public async Task> FindPremiumOrDiscountedProductsAsync()
{
var expensiveSpec = new ExpensiveProductsSpecification(1000);
var discountedSpec = new DiscountedProductsSpecification();
// OR combination: expensive OR discounted products
var combinedSpec = expensiveSpec.Or(discountedSpec);
return await _repository.FindAsync(combinedSpec);
}
public async Task> FindNonExpensiveProductsAsync()
{
var expensiveSpec = new ExpensiveProductsSpecification(500);
// NOT combination: products that are NOT expensive
var nonExpensiveSpec = expensiveSpec.Not();
return await _repository.FindAsync(nonExpensiveSpec);
}
}
// Complex specification with multiple conditions
public class PremiumProductsSpecification : DomainSpecification
{
public override bool IsSatisfiedBy(Product candidate)
{
return candidate.Price >= 1000 &&
candidate.Rating >= 4.5 &&
!candidate.IsDeleted;
}
public override Expression> ToExpression()
{
return product => product.Price >= 1000 &&
product.Rating >= 4.5 &&
!product.IsDeleted;
}
}
```
### Advanced Specification Features
The `DomainSpecification` class provides powerful features for building complex queries:
#### 1. Pagination Support
```csharp
public class PagedProductsSpecification : DomainSpecification
{
public PagedProductsSpecification(int pageIndex, int pageSize)
{
// 0-based pagination
ApplyPagingByIndex(pageIndex, pageSize);
AddOrderBy(p => p.Name);
}
public override bool IsSatisfiedBy(Product candidate) => true;
public override Expression> ToExpression()
{
return product => !product.IsDeleted;
}
}
// Alternative: Skip/Take based pagination
public class OffsetProductsSpecification : DomainSpecification
{
public OffsetProductsSpecification(int skip, int take)
{
ApplyPagingBySkipAndTake(skip, take);
}
public override bool IsSatisfiedBy(Product candidate) => true;
public override Expression> ToExpression() => p => true;
}
```
#### 2. Sorting and Ordering
```csharp
public class SortedProductsSpecification : DomainSpecification
{
public SortedProductsSpecification()
{
// Multiple order expressions applied in sequence
AddOrderByDescending(p => p.CreatedAt); // Primary sort
AddOrderBy(p => p.Name); // Secondary sort (ThenBy)
AddOrderBy(p => p.Price); // Tertiary sort
}
public override bool IsSatisfiedBy(Product candidate) => true;
public override Expression> ToExpression() => p => !p.IsDeleted;
}
```
#### 3. Text Search Across Properties
```csharp
public class ProductSearchSpecification : DomainSpecification
{
public ProductSearchSpecification(string searchTerm)
{
// Search across multiple properties (case-insensitive Contains)
ApplySearch(searchTerm,
p => p.Name,
p => p.Description,
p => p.Brand,
p => p.Category.Name);
AsNoTracking(); // Read-only query optimization
}
public override bool IsSatisfiedBy(Product candidate)
{
return !candidate.IsDeleted;
}
public override Expression> ToExpression()
{
return product => !product.IsDeleted;
}
}
```
#### 4. Eager Loading with Includes
```csharp
public class ProductWithRelationsSpecification : DomainSpecification
{
public ProductWithRelationsSpecification()
{
// Expression-based includes
AddInclude(p => p.Category);
AddInclude(p => p.Supplier);
// String-based includes for nested properties
AddInclude("Reviews.User");
AddInclude("OrderItems.Order");
// Multiple includes at once
AddIncludes(
p => p.Images,
p => p.Tags,
p => p.Variants
);
// Prevent Cartesian explosion with split queries
EnableSplitQuery();
}
public override bool IsSatisfiedBy(Product candidate) => true;
public override Expression> ToExpression() => p => !p.IsDeleted;
}
```
#### 5. Query Filters and Tracking Control
```csharp
public class AllProductsIncludingDeletedSpecification : DomainSpecification
{
public AllProductsIncludingDeletedSpecification()
{
// Ignore global query filters (e.g., soft delete filter)
ApplyIgnoreQueryFilters();
// Enable tracking for updates
EnableTracking();
}
public override bool IsSatisfiedBy(Product candidate) => true;
public override Expression> ToExpression() => p => true;
}
```
#### 6. Grouping for Aggregations
```csharp
public class ProductsByCategorySpecification : DomainSpecification
{
public ProductsByCategorySpecification()
{
ApplyGroupBy(p => p.CategoryId);
AddOrderBy(p => p.CategoryId);
}
public override bool IsSatisfiedBy(Product candidate) => !candidate.IsDeleted;
public override Expression> ToExpression() => p => !p.IsDeleted;
}
```
#### 7. Complex Real-World Example
```csharp
public class AdvancedProductSearchSpecification : DomainSpecification
{
public AdvancedProductSearchSpecification(
string? searchTerm = null,
decimal? minPrice = null,
decimal? maxPrice = null,
int? categoryId = null,
int pageIndex = 0,
int pageSize = 20,
bool includeDeleted = false)
{
// Dynamic criteria - only applied when parameter has value
AddCriteriaIf(minPrice.HasValue, p => p.Price >= minPrice!.Value);
AddCriteriaIf(maxPrice.HasValue, p => p.Price <= maxPrice!.Value);
AddCriteriaIf(categoryId.HasValue, p => p.CategoryId == categoryId!.Value);
// Text search if provided
if (!string.IsNullOrWhiteSpace(searchTerm))
{
ApplySearch(searchTerm, p => p.Name, p => p.Description);
}
// Eager load relations
AddIncludes(
p => p.Category,
p => p.Supplier,
p => p.Reviews
);
// Use split query for multiple collections
EnableSplitQuery();
// Sorting
AddOrderByDescending(p => p.CreatedAt);
AddOrderBy(p => p.Name);
// Pagination
ApplyPagingByIndex(pageIndex, pageSize);
// Include soft-deleted if requested
if (includeDeleted)
{
ApplyIgnoreQueryFilters();
}
// Read-only optimization
AsNoTracking();
}
public override bool IsSatisfiedBy(Product candidate)
{
return !candidate.IsDeleted;
}
public override Expression> ToExpression()
{
return product => !product.IsDeleted;
}
}
// Usage in repository
public class ProductService
{
private readonly IDomainRepository _repository;
public async Task> SearchProductsAsync(
string searchTerm,
int page,
int pageSize)
{
var specification = new AdvancedProductSearchSpecification(
searchTerm: searchTerm,
minPrice: 10,
maxPrice: 1000,
pageIndex: page,
pageSize: pageSize
);
return await _repository.FindAsync(specification);
}
}
```
#### Dynamic Criteria with AddCriteria / AddCriteriaIf (v10.0.2+)
Build dynamic queries with optional filters directly in specifications:
```csharp
public class ProductSearchSpecification : DomainSpecification
{
public ProductSearchSpecification(
string? categoryName = null,
decimal? minPrice = null,
decimal? maxPrice = null,
bool? isActive = null,
string? searchTerm = null)
{
// Conditional criteria - only applied when parameter has value
AddCriteriaIf(!string.IsNullOrEmpty(categoryName),
p => p.Category.Name == categoryName!);
AddCriteriaIf(minPrice.HasValue,
p => p.Price >= minPrice!.Value);
AddCriteriaIf(maxPrice.HasValue,
p => p.Price <= maxPrice!.Value);
AddCriteriaIf(isActive.HasValue,
p => p.IsActive == isActive!.Value);
// Always-applied criteria
AddCriteria(p => !p.IsDeleted);
// Search
if (!string.IsNullOrEmpty(searchTerm))
ApplySearch(searchTerm, p => p.Name, p => p.Description);
AddOrderByDescending(p => p.CreatedAt);
}
public override bool IsSatisfiedBy(Product candidate) => !candidate.IsDeleted;
public override Expression> ToExpression() => p => true;
}
```
#### Filtered Include (v10.0.2+)
EF Core's filtered include is supported through `IncludeCollection`:
```csharp
public class BlogWithActivePostsSpecification : DomainSpecification
{
public BlogWithActivePostsSpecification()
{
// Filtered include - only load active posts
IncludeCollection(b => b.Posts.Where(p => p.IsPublished && !p.IsDeleted))
.ThenInclude(p => p.Author);
// Regular include
Include(b => b.Owner);
}
public override bool IsSatisfiedBy(Blog candidate) => true;
public override Expression> ToExpression() => b => true;
}
```
#### Type-Safe ThenInclude Support (v10.0.2+)
```csharp
public class OrderWithDetailsSpecification : DomainSpecification
{
public OrderWithDetailsSpecification(Guid orderId)
{
// Type-safe ThenInclude chaining
Include(order => order.Customer)
.ThenInclude(customer => customer.Address);
// Collection ThenInclude
IncludeCollection(order => order.OrderItems)
.ThenInclude(item => item.Product)
.ThenInclude(product => product.Category);
// Multiple levels deep
IncludeCollection(order => order.Payments)
.ThenInclude(payment => payment.PaymentMethod);
AsTracking(); // Enable tracking for updates
}
public override bool IsSatisfiedBy(Order candidate) => true;
public override Expression> ToExpression() => o => o.Id == orderId;
}
```
#### Specification with Pagination (FindPagedAsync) (v10.0.2+)
```csharp
var specification = new ActiveProductsSpecification(pageIndex: 0, pageSize: 20);
// Returns IPaginate with total count, pages, etc.
var pagedResult = await repository.FindPagedAsync(specification);
// With projection
var pagedDtos = await repository.FindPagedAsync(
specification,
selector: p => new ProductDto { Id = p.Id, Name = p.Name });
```
#### Single Entity from Specification (v10.0.2+)
```csharp
// FirstOrDefaultAsync with specification
var product = await repository.FirstOrDefaultAsync(specification);
// SingleOrDefaultAsync with specification (throws if multiple)
var uniqueProduct = await repository.SingleOrDefaultAsync(specification);
// FirstOrDefaultAsync with projection
var dto = await repository.FirstOrDefaultAsync(
specification,
selector: p => new ProductDto { Id = p.Id, Name = p.Name });
```
#### Specification Composition Summary
**Available Methods:**
**Includes:**
- `AddInclude(expression)` - Simple eager load navigation property
- `AddInclude(string)` - String-based include for nested properties
- `AddIncludes(expressions...)` - Multiple includes at once
- `Include(expression)` - Type-safe include with ThenInclude support
- `IncludeCollection(expression)` - Collection include with ThenInclude support (supports filtered include)
- `ClearIncludes()` - Remove all includes
**Ordering:**
- `AddOrderBy(expression)` - Ascending sort
- `AddOrderByDescending(expression)` - Descending sort
- `ClearOrdering()` - Reset all ordering
**Pagination & Limiting:**
- `ApplyPagingByIndex(pageIndex, pageSize)` - 0-based page pagination
- `ApplyPagingBySkipAndTake(skip, take)` - Offset-based pagination
- `ApplyLimit(count)` - Limit results without pagination metadata
**Filtering & Criteria:**
- `AddCriteria(predicate)` - Add an additional filter predicate
- `AddCriteriaIf(condition, predicate)` - Conditionally add a filter predicate
- `ClearCriteria()` - Remove all additional criteria
- `ApplySearch(term, properties...)` - Text search across properties
- `ApplyIgnoreQueryFilters()` - Bypass global filters
- `ApplyDistinct()` - Return only distinct results
**Grouping:**
- `ApplyGroupBy(expression)` - Group results
**Projection:**
- `ApplySelector(expression)` - Define projection in specification
**Tracking:**
- `AsNoTracking()` - Disable change tracking (default)
- `AsTracking()` / `EnableTracking()` - Enable change tracking
- `AsNoTrackingWithIdentityResolution()` - No tracking with identity resolution
**Query Optimization:**
- `EnableSplitQuery()` - Prevent Cartesian explosion
- `TagWith(tag)` - Add query tag for debugging/logging
**Composition:**
- `And(spec)`, `Or(spec)`, `Not()` - Logical combinations
## π Advanced Features
### Interceptor System
The library provides a robust interceptor system that automatically handles cross-cutting concerns:
#### Audit Interceptor
Automatically tracks entity creation and modification:
```csharp
// Automatic configuration via Fluent API
services.AddFSEntityFramework()
.WithAudit()
.UsingHttpContext() // Uses current HTTP user
.Build();
// Manual interceptor registration
services.AddScoped(provider =>
{
var userProvider = () => provider.GetService()
?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return new AuditInterceptor(userProvider);
});
```
#### Soft Delete Interceptor
Automatically handles soft delete operations:
```csharp
// Entities implementing ISoftDelete are automatically soft deleted
public class Product : BaseAuditableEntity, ISoftDelete
{
public string Name { get; set; } = string.Empty;
// ISoftDelete properties (automatically managed)
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public string? DeletedBy { get; set; }
}
// Configuration
services.AddFSEntityFramework()
.WithSoftDelete() // Enables soft delete interceptor
.Build();
// Usage - automatically becomes soft delete
var repository = _unitOfWork.GetRepository();
await repository.DeleteAsync(product); // Soft delete
await repository.RestoreAsync(productId); // Restore
```
#### ID Generation Interceptor
Automatically generates IDs for new entities:
```csharp
// Register ID generators
services.AddFSEntityFramework()
.WithIdGeneration()
.WithGenerator() // GUID V7 for Guid properties
.WithGenerator() // Custom string IDs
.Complete()
.Build();
// Custom ID generator example
public class CustomStringIdGenerator : IIdGenerator
{
public Type KeyType => typeof(string);
public string Generate()
{
return $"PROD_{DateTime.UtcNow:yyyyMMdd}_{Guid.NewGuid():N}"[..20];
}
object IIdGenerator.Generate() => Generate();
}
```
### FluentConfiguration API Reference
The Fluent Configuration API provides a clean, type-safe way to configure all library features:
#### Core Configuration Methods
```csharp
// Start configuration
services.AddFSEntityFramework()
// Audit Configuration Chain
.WithAudit()
.UsingHttpContext() // Use HTTP context for user
.UsingUserProvider(provider => "user") // Custom user provider
.UsingUserContext() // Interface-based user context
.UsingTimeProvider(provider => DateTime.UtcNow) // Custom time provider
.Complete() // End audit configuration
// Domain Events Configuration Chain
.WithDomainEvents()
.UsingDefaultDispatcher() // Use built-in dispatcher
.UsingCustomDispatcher() // Custom dispatcher
.WithAutoHandlerDiscovery() // Auto-discover handlers
.WithHandlerDiscovery(assembly) // Discover from specific assembly
.WithAttributedHandlers(assembly) // Use attributed handlers
.Complete() // End domain events configuration
// Soft Delete Configuration
.WithSoftDelete()
// ID Generation Configuration Chain
.WithIdGeneration()
.WithGenerator() // Register generator for type
.WithFactory() // Custom factory
.Complete() // End ID generation configuration
// Validation and Build
.ValidateConfiguration() // Validate all configurations
.Build(); // Build and register services
```
#### Configuration Validation
```csharp
// The fluent API includes built-in validation
services.AddFSEntityFramework()
.WithAudit()
.UsingHttpContext()
.WithDomainEvents()
.UsingDefaultDispatcher()
.WithAutoHandlerDiscovery()
.Complete()
.ValidateConfiguration() // Throws detailed exceptions for invalid configs
.Build();
```
### Infrastructure Layer Details
The library provides a complete infrastructure layer implementing DDD patterns:
#### Domain Repository Implementation
```csharp
// IDomainRepository interface for aggregate roots
public interface IDomainRepository
where TAggregate : AggregateRoot
where TKey : IEquatable
{
// Core CRUD
Task GetByIdAsync(TKey id, List>>? includes = null, bool disableTracking = false, CancellationToken cancellationToken = default);
Task GetByIdRequiredAsync(TKey id, List>>? includes = null, bool disableTracking = false, CancellationToken cancellationToken = default);
Task AddAsync(TAggregate aggregate, CancellationToken cancellationToken = default);
Task UpdateAsync(TAggregate aggregate, CancellationToken cancellationToken = default);
Task RemoveAsync(TAggregate aggregate, CancellationToken cancellationToken = default);
// Specification queries
Task> FindAsync(ISpecification specification, CancellationToken cancellationToken = default);
Task AnyAsync(ISpecification specification, CancellationToken cancellationToken = default);
Task CountAsync(ISpecification specification, CancellationToken cancellationToken = default);
// Single entity from specification
Task FirstOrDefaultAsync(ISpecification specification, CancellationToken cancellationToken = default);
Task SingleOrDefaultAsync(ISpecification specification, CancellationToken cancellationToken = default);
// Projection methods
Task GetByIdAsync(TKey id, Expression> selector, CancellationToken cancellationToken = default);
Task> FindAsync(ISpecification specification, Expression> selector, CancellationToken cancellationToken = default);
Task> FindWithSelectorAsync(ISpecification specification, CancellationToken cancellationToken = default);
Task FirstOrDefaultAsync(ISpecification specification, Expression> selector, CancellationToken cancellationToken = default);
// Paginated results
Task> FindPagedAsync(ISpecification specification, CancellationToken cancellationToken = default);
Task> FindPagedAsync(ISpecification specification, Expression> selector, CancellationToken cancellationToken = default);
}
// Usage with automatic registration
services.AddDomainServices()
.AddDomainRepository()
.AddDomainRepository();
// Custom repository implementation
public class OrderRepository : DomainRepository, IOrderRepository
{
public OrderRepository(DbContext context, IServiceProvider serviceProvider)
: base(context, serviceProvider) { }
public async Task FindByOrderNumberAsync(string orderNumber)
{
return await FirstOrDefaultAsync(new OrderByNumberSpecification(orderNumber));
}
}
```
#### Domain Unit of Work
```csharp
// IDomainUnitOfWork for aggregate-focused operations
public interface IDomainUnitOfWork : IDisposable
{
IDomainRepository GetRepository()
where TAggregate : AggregateRoot
where TKey : IEquatable;
Task SaveChangesAsync(CancellationToken cancellationToken = default);
Task BeginTransactionAsync(CancellationToken cancellationToken = default);
Task CommitTransactionAsync(CancellationToken cancellationToken = default);
Task RollbackTransactionAsync(CancellationToken cancellationToken = default);
}
// Usage in application services
public class OrderApplicationService
{
private readonly IDomainUnitOfWork _domainUnitOfWork;
public async Task ProcessOrderAsync(ProcessOrderCommand command)
{
var orderRepository = _domainUnitOfWork.GetRepository();
var order = await orderRepository.GetByIdAsync(command.OrderId);
order?.ProcessOrder();
await _domainUnitOfWork.SaveChangesAsync(); // Domain events dispatched automatically
}
}
```
### Enhanced Pagination Support
The library provides comprehensive pagination capabilities:
#### Basic Pagination
```csharp
// IPaginate interface provides rich pagination information
public interface IPaginate
{
int Index { get; } // Current page index (0-based)
int Size { get; } // Page size
int Count { get; } // Total item count
int Pages { get; } // Total page count
IList Items { get; } // Current page items
bool HasPrevious { get; } // Has previous page
bool HasNext { get; } // Has next page
}
// Repository pagination methods
var repository = _unitOfWork.GetRepository();
// Simple pagination
var pagedProducts = await repository.GetPagedAsync(
pageIndex: 0,
pageSize: 20,
orderBy: query => query.OrderBy(p => p.Name)
);
// Pagination with includes
var pagedProductsWithCategory = await repository.GetPagedAsync(
pageIndex: 0,
pageSize: 20,
includes: new List>> { p => p.Category },
orderBy: query => query.OrderBy(p => p.Name)
);
```
#### Advanced Pagination with Filtering
```csharp
// Pagination with dynamic filtering
var filter = new FilterModel
{
SearchTerm = "laptop", // Searches across all string properties
Filters = new List
{
new() { Field = "Price", Operator = "greaterthan", Value = "500" },
new() { Field = "CategoryId", Operator = "equals", Value = "1" }
}
};
var filteredPage = await repository.GetPagedWithFilterAsync(
filter,
pageIndex: 0,
pageSize: 20,
orderBy: query => query.OrderByDescending(p => p.CreatedAt),
includes: new List>> { p => p.Category }
);
// Available filter operators (full name / alias):
// "equals" (eq), "notequals" (neq), "contains", "startswith" (sw), "endswith" (ew)
// "greaterthan" (gt), "greaterthanorequal" (gte), "lessthan" (lt), "lessthanorequal" (lte)
// "isnull", "isnotnull", "isempty", "isnotempty" (no value required)
// "in", "notin" (comma-separated values, e.g. "1,2,3")
//
// Or use the type-safe FilterOperator enum / FilterBuilder for compile-time validation
```
#### Cursor-Based Pagination (v10.0.2+)
Cursor pagination is more efficient than offset pagination for large datasets:
```csharp
// Basic cursor pagination
var repository = _unitOfWork.GetRepository();
// Get first page
var firstPage = await repository.GetCursorPagedAsync(
pageSize: 20,
afterCursor: null, // null for first page
beforeCursor: null,
cursorSelector: p => p.Id, // Use ID as cursor
predicate: p => p.IsActive,
orderBy: q => q.OrderBy(p => p.Id)
);
// Get next page using LastCursor
var nextPage = await repository.GetCursorPagedAsync(
pageSize: 20,
afterCursor: firstPage.LastCursor, // Start after last item
beforeCursor: null,
cursorSelector: p => p.Id
);
// ICursorPaginate interface
// - Items: Current page items
// - Size: Requested page size
// - Count: Actual items returned
// - FirstCursor/LastCursor: Cursor values for navigation
// - HasNext/HasPrevious: Navigation availability
// Cursor pagination with projection
var projectedPage = await repository.GetCursorPagedAsync(
selector: p => new ProductDto { Id = p.Id, Name = p.Name, Price = p.Price },
pageSize: 20,
afterCursor: null,
beforeCursor: null,
cursorSelector: p => p.Id
);
```
### Projection/Select Support (v10.0.2+)
The library provides comprehensive projection methods for efficient data retrieval:
#### Repository Projection Methods
```csharp
var repository = _unitOfWork.GetRepository();
// Project single entity by ID
var productDto = await repository.GetByIdAsync(
id: 1,
selector: p => new ProductDto
{
Id = p.Id,
Name = p.Name,
CategoryName = p.Category.Name,
Price = p.Price
});
// Project all entities
var allProductDtos = await repository.GetAllAsync(
selector: p => new ProductSummaryDto
{
Id = p.Id,
Name = p.Name
});
// Project first match
var cheapestProduct = await repository.FirstOrDefaultAsync(
predicate: p => p.CategoryId == categoryId,
selector: p => new ProductDto { Id = p.Id, Name = p.Name, Price = p.Price });
// Project single match (throws if multiple)
var uniqueProduct = await repository.SingleOrDefaultAsync(
predicate: p => p.Sku == "ABC123",
selector: p => new ProductDto { Id = p.Id, Name = p.Name });
// Project with filter and ordering
var filteredProducts = await repository.FindAsync(
predicate: p => p.Price > 100,
selector: p => new ProductDto { Id = p.Id, Name = p.Name, Price = p.Price },
orderBy: q => q.OrderByDescending(p => p.Price));
// Paginated projection
var pagedProducts = await repository.GetPagedAsync(
selector: p => new ProductListDto
{
Id = p.Id,
Name = p.Name,
CategoryName = p.Category.Name
},
pageIndex: 0,
pageSize: 20,
predicate: p => p.IsActive,
orderBy: q => q.OrderBy(p => p.Name));
// Filtered pagination with projection
var filteredPagedProducts = await repository.GetPagedWithFilterAsync(
selector: p => new ProductDto { Id = p.Id, Name = p.Name },
filter: filterModel,
pageIndex: 0,
pageSize: 20);
// Specification with projection
var spec = new ActiveProductsSpecification();
var specProducts = await repository.GetAsync(spec,
selector: p => new ProductDto { Id = p.Id, Name = p.Name });
```
#### Domain Repository Projection Methods
```csharp
var domainRepository = _domainUnitOfWork.GetRepository();
// Project aggregate by ID
var productDto = await domainRepository.GetByIdAsync(
id: productId,
selector: p => new ProductDetailDto
{
Id = p.Id,
Name = p.Name,
TotalOrderCount = p.Orders.Count
});
// Project with specification
var specification = new ActiveProductsSpecification();
var products = await domainRepository.FindAsync(
specification,
selector: p => new ProductSummaryDto { Id = p.Id, Name = p.Name });
```
#### Specification-Based Projection
Define projections directly in specifications:
```csharp
public class ProductListSpecification : DomainSpecification
{
public ProductListSpecification(int categoryId, int pageIndex, int pageSize)
{
// Define the projection
ApplySelector(p => new ProductListDto
{
Id = p.Id,
Name = p.Name,
CategoryName = p.Category.Name,
Price = p.Price,
ReviewCount = p.Reviews.Count
});
AddInclude(p => p.Category);
AddOrderBy(p => p.Name);
ApplyPagingByIndex(pageIndex, pageSize);
}
public override bool IsSatisfiedBy(Product candidate) =>
candidate.CategoryId == categoryId;
public override Expression> ToExpression() =>
p => p.CategoryId == categoryId;
}
// Usage
var specification = new ProductListSpecification(categoryId: 5, pageIndex: 0, pageSize: 20);
var results = await repository.FindWithSelectorAsync(specification);
```
### Additional Query Methods (v10.0.2+)
#### SingleOrDefaultAsync
Get exactly one entity or null, throws if multiple match:
```csharp
// Entity version
var product = await repository.SingleOrDefaultAsync(
predicate: p => p.Sku == "UNIQUE-SKU",
includes: new List>> { p => p.Category },
disableTracking: true);
// Projection version
var productDto = await repository.SingleOrDefaultAsync(
predicate: p => p.Sku == "UNIQUE-SKU",
selector: p => new ProductDto { Id = p.Id, Name = p.Name });
```
#### AnyAsync with Predicate
Check existence with optional predicate:
```csharp
// Check if any products exist
var hasAnyProducts = await repository.AnyAsync();
// Check with predicate
var hasExpensiveProducts = await repository.AnyAsync(p => p.Price > 1000);
var hasActiveProducts = await repository.AnyAsync(p => p.IsActive && !p.IsDeleted);
```
### ID Generation Extensions
The library supports modular ID generation strategies:
#### GUID Version 7 (Requires extension package)
```csharp
// Install: dotnet add package FS.EntityFramework.Library.GuidV7
services.AddFSEntityFramework()
.WithGuidV7() // Automatic GUID V7 generation
.Build();
// Entity with GUID V7
public class User : BaseAuditableEntity
{
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
// ID will be automatically generated as GUID V7
}
```
#### ULID (Requires extension package)
```csharp
// Install: dotnet add package FS.EntityFramework.Library.UlidGenerator
services.AddFSEntityFramework()
.WithUlid() // Automatic ULID generation
.Build();
// Entity with ULID
public class Order : BaseAuditableEntity
{
public string OrderNumber { get; set; } = string.Empty;
// ID will be automatically generated as ULID
}
```
### Advanced Audit Configuration
Configure audit tracking with different user context providers:
```csharp
// Web applications with HttpContext
services.AddFSEntityFramework()
.WithAudit()
.UsingHttpContext() // Uses NameIdentifier claim
.Build();
// Custom user provider
services.AddFSEntityFramework()
.WithAudit()
.UsingUserProvider(provider =>
{
var userService = provider.GetService();
return userService?.GetCurrentUserId();
})
.Build();
// Interface-based user context
public class ApplicationUserContext : IUserContext
{
private readonly ICurrentUserService _userService;
public ApplicationUserContext(ICurrentUserService userService)
{
_userService = userService;
}
public string? CurrentUser => _userService.GetCurrentUserId();
}
services.AddScoped();
services.AddFSEntityFramework()
.WithAudit()
.UsingUserContext()
.Build();
```
### Comprehensive Configuration Example
Here's a full-featured configuration example:
```csharp
services.AddFSEntityFramework()
// Audit Configuration
.WithAudit()
.UsingHttpContext() // User tracking via HTTP context
// Domain Events Configuration
.WithDomainEvents()
.UsingDefaultDispatcher() // Default event dispatcher
.WithAutoHandlerDiscovery() // Auto-discover event handlers
.Complete()
// Soft Delete Configuration
.WithSoftDelete()
// ID Generation Configuration
.WithIdGeneration()
.WithGenerator()
.Complete()
// Validation & Build
.ValidateConfiguration()
.Build();
```
### Error Handling & Exception Management
The library provides comprehensive error handling patterns:
```csharp
using FS.EntityFramework.Library.Domain;
// Domain-specific exceptions
public class OrderDomainException : DomainException
{
public OrderDomainException(string message) : base(message) { }
public OrderDomainException(string message, Exception innerException) : base(message, innerException) { }
}
// Business rule validation exception handling
public class OrderApplicationService
{
private readonly IDomainUnitOfWork _unitOfWork;
private readonly ILogger _logger;
public async Task ProcessOrderAsync(ProcessOrderCommand command)
{
try
{
var repository = _unitOfWork.GetRepository();
var order = await repository.GetByIdAsync(command.OrderId);
if (order == null)
{
return OrderResult.NotFound(command.OrderId);
}
// Business logic with domain validation
order.ProcessOrder();
await _unitOfWork.SaveChangesAsync();
return OrderResult.Success(order);
}
catch (BusinessRuleValidationException ex)
{
_logger.LogWarning("Business rule violation: {Rule} - {Message}",
ex.BrokenRule.ErrorCode, ex.BrokenRule.Message);
return OrderResult.BusinessRuleViolation(ex.BrokenRule);
}
catch (DomainException ex)
{
_logger.LogError(ex, "Domain error processing order {OrderId}", command.OrderId);
return OrderResult.DomainError(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error processing order {OrderId}", command.OrderId);
return OrderResult.UnexpectedError();
}
}
}
// Result pattern for better error handling
public class OrderResult
{
public bool IsSuccess { get; private set; }
public string? ErrorMessage { get; private set; }
public string? ErrorCode { get; private set; }
public OrderAggregate? Order { get; private set; }
public static OrderResult Success(OrderAggregate order) =>
new() { IsSuccess = true, Order = order };
public static OrderResult NotFound(Guid orderId) =>
new() { IsSuccess = false, ErrorMessage = $"Order {orderId} not found", ErrorCode = "ORDER_NOT_FOUND" };
public static OrderResult BusinessRuleViolation(IBusinessRule rule) =>
new() { IsSuccess = false, ErrorMessage = rule.Message, ErrorCode = rule.ErrorCode };
public static OrderResult DomainError(string message) =>
new() { IsSuccess = false, ErrorMessage = message, ErrorCode = "DOMAIN_ERROR" };
public static OrderResult UnexpectedError() =>
new() { IsSuccess = false, ErrorMessage = "An unexpected error occurred", ErrorCode = "UNEXPECTED_ERROR" };
}
```
### Performance Considerations
Optimize your application with these performance best practices:
#### Repository Query Optimization
```csharp
// β
Good: Use built-in projection methods (v10.0.2+)
public async Task> GetProductSummariesAsync()
{
var repository = _unitOfWork.GetRepository();
// Built-in projection method - cleaner and more efficient
return await repository.GetAllAsync(p => new ProductSummaryDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
CategoryName = p.Category.Name
});
}
// β
Good: Alternative using GetQueryable for complex scenarios
public async Task> GetProductSummariesManualAsync()
{
var repository = _unitOfWork.GetRepository();
return await repository.GetQueryable(disableTracking: true)
.Select(p => new ProductSummaryDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
CategoryName = p.Category.Name
})
.ToListAsync();
}
// β
Good: Use includes strategically
public async Task GetProductWithDetailsAsync(int id)
{
var repository = _unitOfWork.GetRepository();
return await repository.GetQueryable()
.Include(p => p.Category)
.Include(p => p.Reviews.Take(5)) // Limit related data
.FirstOrDefaultAsync(p => p.Id == id);
}
// β
Good: Use compiled queries for frequently used queries
private static readonly Func> GetProductByIdCompiled =
EF.CompileAsyncQuery((ApplicationDbContext context, int id) =>
context.Products.FirstOrDefault(p => p.Id == id));
public async Task GetProductByIdOptimizedAsync(int id)
{
return await GetProductByIdCompiled(_context, id);
}
```
#### Bulk Operations
```csharp
// β
Good: Use bulk operations for large datasets
public async Task ImportProductsAsync(IEnumerable products)
{
var repository = _unitOfWork.GetRepository();
// Bulk insert for better performance
await repository.BulkInsertAsync(products, saveChanges: true);
}
// β
Good: Batch operations
public async Task UpdateMultipleProductPricesAsync(Dictionary priceUpdates)
{
var repository = _unitOfWork.GetRepository();
var productIds = priceUpdates.Keys.ToList();
var products = await repository.GetQueryable()
.Where(p => productIds.Contains(p.Id))
.ToListAsync();
foreach (var product in products)
{
if (priceUpdates.TryGetValue(product.Id, out var newPrice))
{
product.SetPrice(newPrice);
}
}
await _unitOfWork.SaveChangesAsync(); // Single save operation
}
```
#### Caching Strategies
```csharp
// β
Good: Implement caching for frequently accessed data
public class CachedProductService
{
private readonly IUnitOfWork _unitOfWork;
private readonly IMemoryCache _cache;
private readonly TimeSpan _cacheExpiry = TimeSpan.FromMinutes(15);
public async Task GetProductAsync(int id)
{
var cacheKey = $"product_{id}";
if (_cache.TryGetValue(cacheKey, out Product? cachedProduct))
{
return cachedProduct;
}
var repository = _unitOfWork.GetRepository();
var product = await repository.GetByIdAsync(id);
if (product != null)
{
_cache.Set(cacheKey, product, _cacheExpiry);
}
return product;
}
}
```
### Diagnostics & Metrics (v10.0.3+)
The library provides opt-in OpenTelemetry-compatible metrics via `System.Diagnostics.Metrics`. Metrics are **disabled by default** and can be enabled via the fluent configuration API.
#### Enable Metrics
```csharp
services.AddFSEntityFramework()
.WithMetrics() // Enable production metrics
.WithAudit()
.UsingHttpContext()
.Build();
```
#### Available Metrics
| Metric | Type | Description |
|--------|------|-------------|
| `repository.operations` | Counter | Total repository operations (tag: operation) |
| `repository.operations.errors` | Counter | Repository operation errors (tags: operation, error_type) |
| `repository.operation.duration` | Histogram | Operation duration in ms (tag: operation) |
| `unitofwork.savechanges` | Counter | SaveChanges calls (tag: status) |
| `unitofwork.transactions` | Counter | Transaction operations (tag: type) |
| `unitofwork.cache.hits` | Counter | Repository cache hits |
| `unitofwork.cache.misses` | Counter | Repository cache misses |
| `interceptor.audit.entities` | Counter | Audited entities (tag: state) |
| `interceptor.idgeneration.generated` | Counter | Generated IDs (tag: key_type) |
| `events.dispatched` | Counter | Dispatched domain events (tag: event_type) |
| `events.handler.errors` | Counter | Handler errors (tags: event_type, handler_type) |
| `events.dispatch.duration` | Histogram | Event dispatch duration in ms (tag: event_type) |
All metrics use the meter name `FS.EntityFramework.Library` and are compatible with any OpenTelemetry collector (Prometheus, Grafana, Azure Monitor, etc.).
#### Consume with OpenTelemetry
```csharp
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddMeter("FS.EntityFramework.Library");
});
```
## π― Best Practices
### Entity Design Guidelines
Follow these guidelines when designing your entities:
```csharp
// β
Good: Well-designed entity
public class Product : BaseAuditableEntity, ISoftDelete
{
// Private setters for business logic enforcement
public string Name { get; private set; } = string.Empty;
public decimal Price { get; private set; }
// Public properties for simple data
public string Description { get; set; } = string.Empty;
// Soft delete properties (automatic)
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public string? DeletedBy { get; set; }
// Factory method for creation
public static Product Create(string name, decimal price)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Name cannot be empty", nameof(name));
if (price <= 0)
throw new ArgumentException("Price must be positive", nameof(price));
var product = new Product();
product.SetName(name);
product.SetPrice(price);
// Raise domain event
product.AddDomainEvent(new ProductCreatedEvent(product.Id, name, price));
return product;
}
// Business methods with validation
public void SetName(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Name cannot be empty", nameof(name));
Name = name;
}
public void SetPrice(decimal price)
{
if (price <= 0)
throw new ArgumentException("Price must be positive", nameof(price));
var oldPrice = Price;
Price = price;
if (oldPrice != price)
{
AddDomainEvent(new ProductPriceChangedEvent(Id, oldPrice, price));
}
}
}
```
### Service Layer Patterns
Implement clean service layer patterns:
```csharp
// β
Good: Service with proper separation of concerns
public class ProductApplicationService
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger _logger;
public ProductApplicationService(
IUnitOfWork unitOfWork,
ILogger logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
}
public async Task CreateProductAsync(CreateProductCommand command)
{
// Input validation
if (string.IsNullOrWhiteSpace(command.Name))
throw new ArgumentException("Product name is required");
var repository = _unitOfWork.GetRepository();
// Business logic
var product = Product.Create(command.Name, command.Price);
// Persistence
await repository.AddAsync(product);
await _unitOfWork.SaveChangesAsync(); // Domain events dispatched here
_logger.LogInformation("Created product {ProductId}: {ProductName}",
product.Id, product.Name);
// Return DTO
return new ProductDto(product.Id, product.Name, product.Price);
}
}
```
## π§ Troubleshooting
### Common Issues and Solutions
#### Issue: Domain Events Not Being Dispatched
**Problem:** Domain events are not being handled even though handlers are registered.
**Solution:** Ensure you're using the domain unit of work or have properly configured event dispatching:
```csharp
// β Wrong: Using regular SaveChanges
await _unitOfWork.SaveChangesAsync(); // Events might not be dispatched
// β
Correct: Ensure domain events are configured
services.AddFSEntityFramework()
.WithDomainEvents()
.UsingDefaultDispatcher()
.WithAutoHandlerDiscovery()
.Complete()
.Build();
```
#### Issue: Soft Delete Not Working
**Problem:** Entities are being hard deleted instead of soft deleted.
**Solution:** Ensure entity implements `ISoftDelete` and soft delete is configured:
```csharp
// β
Entity must implement ISoftDelete
public class Product : BaseAuditableEntity, ISoftDelete
{
// ISoftDelete properties
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public string? DeletedBy { get; set; }
}
// β
Configure soft delete
services.AddFSEntityFramework()
.WithSoftDelete()
.Build();
```
#### Issue: Audit Properties Not Being Set
**Problem:** `CreatedAt`, `CreatedBy`, etc., are not being populated automatically.
**Solution:** Ensure audit configuration is properly set up:
```csharp
// β
Configure audit with user provider
services.AddFSEntityFramework()
.WithAudit()
.UsingHttpContext() // or another user provider
.Build();
```
#### Issue: Repository Not Found
**Problem:** `InvalidOperationException` when trying to get a repository.
**Solution:** Ensure your DbContext is properly registered before adding FS.EntityFramework:
```csharp
// β
Register DbContext first
services.AddDbContext(options =>
options.UseSqlServer(connectionString));
// β
Then add FS.EntityFramework
services.AddFSEntityFramework()
.Build();
```
### Performance Optimization Tips
#### Use Projections for Read-Only Data
```csharp
// β
Use built-in projection methods for better performance (v10.0.2+)
public async Task> GetProductSummariesAsync()
{
var repository = _unitOfWork.GetRepository();
// Direct projection method
return await repository.GetAllAsync(p => new ProductSummaryDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
});
}
// β
Paginated projection
public async Task> GetPagedProductSummariesAsync(int page, int size)
{
var repository = _unitOfWork.GetRepository();
return await repository.GetPagedAsync(
selector: p => new ProductSummaryDto { Id = p.Id, Name = p.Name, Price = p.Price },
pageIndex: page,
pageSize: size,
orderBy: q => q.OrderBy(p => p.Name));
}
```
#### Disable Tracking for Read-Only Operations
```csharp
// β
Disable tracking for read-only queries
var products = await repository.GetQueryable(disableTracking: true)
.Where(p => p.Price > 100)
.ToListAsync();
```
#### Use Bulk Operations for Large Data Sets
```csharp
// β
Use bulk operations for better performance
await repository.BulkInsertAsync(products, saveChanges: true);
```
## π€ Contributing
We welcome contributions! This project is open source and benefits from community involvement.
### Areas for Contribution
- ποΈ **Enhanced DDD patterns** (Saga patterns, Event Sourcing support)
- π **Additional domain event dispatchers** (Mass Transit, NServiceBus, etc.)
- β‘ **Performance optimizations** for aggregate loading and persistence
- π **Advanced specification implementations**
- π **Documentation and examples**
- π§ͺ **Test coverage improvements**
- π **New ID generation strategies**
- π― **Domain modeling tools and utilities**
### Code Style
- Use **meaningful domain language** in code
- Follow **DDD naming conventions**
- Add **XML documentation** for public APIs
- Include **unit tests** for domain logic
- Follow **SOLID principles** and **DDD patterns**
---
## π License
This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
---
## π Acknowledgments
- Thanks to all contributors who have helped make this library better
- Inspired by **Domain-Driven Design** principles by Eric Evans
- Built on top of the excellent **Entity Framework Core**
- Special thanks to the .NET community for continuous feedback and support
---
## π Support
If you encounter any issues or have questions:
1. Check the [troubleshooting section](#-troubleshooting)
2. Search existing [GitHub issues](https://github.com/furkansarikaya/FS.EntityFramework.Library/issues)
3. Create a new issue with detailed information
4. Join our community discussions
**Happy Domain Modeling! ποΈ**
---
**Made with β€οΈ by [Furkan SarΔ±kaya](https://github.com/furkansarikaya)**
[](https://github.com/furkansarikaya)
[](https://www.linkedin.com/in/furkansarikaya/)
[](https://medium.com/@furkansarikaya)