A comprehensive, production-ready Entity Framework Core library providing Repository pattern, Unit of Work, Specification pattern, Domain-Driven Design (DDD), Aggregate Roots, Value Objects, Business Rules, Domain Events, dynamic filtering, pagination support, Fluent Configuration API, robust interceptor system, and modular ID generation strategies for enterprise .NET applications.
$ dotnet add package FS.EntityFramework.LibraryA comprehensive Entity Framework Core library providing Repository pattern, Unit of Work, Specification pattern, dynamic filtering, pagination support, Domain Events, and Fluent Configuration API for .NET applications.
dotnet add package FS.EntityFramework.LibraryYou can configure FS.EntityFramework.Library using either the new Fluent Configuration API (recommended) or the classic approach.
The Fluent Configuration API provides an intuitive, chainable way to configure the library with better readability and validation.
services.AddDbContext<YourDbContext>(options =>
options.UseSqlServer(connectionString));
services.AddFSEntityFramework<YourDbContext>()
.Build();// Using HttpContext (for web applications)
services.AddFSEntityFramework<YourDbContext>()
.WithAudit()
.UsingHttpContext()
.Build();
// Using custom user provider
services.AddFSEntityFramework<YourDbContext>()
.WithAudit()
.UsingUserProvider(provider =>
{
var userService = provider.GetService<ICurrentUserService>();
return userService?.GetCurrentUserId();
})
.Build();
// Using interface-based approach
services.AddScoped<IUserContext, MyUserContext>();
services.AddFSEntityFramework<YourDbContext>()
.WithAudit()
.UsingUserContext<IUserContext>()
.Build();
// For testing with static user
services.AddFSEntityFramework<YourDbContext>()
.WithAudit()
.UsingStaticUser("test-user-123")
.Build();// Basic domain events with auto handler discovery
services.AddFSEntityFramework<YourDbContext>()
.WithDomainEvents()
.UsingDefaultDispatcher()
.WithAutoHandlerDiscovery()
.Complete()
.Build();
// With custom dispatcher (e.g., for MediatR integration)
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
services.AddFSEntityFramework<YourDbContext>()
.WithDomainEvents()
.UsingCustomDispatcher<YourCustomDomainEventDispatcher>()
.WithAutoHandlerDiscovery(typeof(ProductCreatedEvent).Assembly)
.Complete()
.Build();
// Advanced handler registration options
services.AddFSEntityFramework<YourDbContext>()
.WithDomainEvents()
.UsingDefaultDispatcher()
.WithAutoHandlerDiscoveryFromTypes(typeof(ProductCreatedEvent), typeof(OrderPlacedEvent))
.WithAttributeBasedDiscovery(Assembly.GetExecutingAssembly())
.WithCustomHandlerDiscovery(
Assembly.GetExecutingAssembly(),
type => type.Name.EndsWith("Handler") && !type.Name.Contains("Test"),
ServiceLifetime.Scoped)
.WithHandler<ProductCreatedEvent, ProductCreatedEventHandler>()
.Complete()
.Build();services.AddFSEntityFramework<YourDbContext>()
.WithAudit()
.UsingHttpContext()
.WithDomainEvents()
.UsingDefaultDispatcher()
.WithAutoHandlerDiscovery()
.WithAttributeBasedDiscovery(Assembly.GetExecutingAssembly())
.Complete()
.WithSoftDelete()
.WithCustomRepository<Product, int, ProductRepository>()
.WithRepositoriesFromAssembly(Assembly.GetExecutingAssembly())
.ValidateConfiguration()
.Build();services.AddFSEntityFramework<YourDbContext>()
.WithAudit()
.UsingHttpContext()
.When(isDevelopment, builder =>
builder.WithDomainEvents()
.UsingDefaultDispatcher()
.WithAutoHandlerDiscovery()
.Complete())
.When(!isDevelopment, builder =>
builder.WithServices(s => s.AddSingleton<ILoggingService, ProductionLoggingService>()))
.ValidateConfiguration()
.Build();The original configuration methods are still supported for backward compatibility:
services.AddDbContext<YourDbContext>(options =>
options.UseSqlServer(connectionString));
services.AddGenericUnitOfWork<YourDbContext>();Option A: Using your existing user service
services.AddScoped<ICurrentUserService, CurrentUserService>();
services.AddGenericUnitOfWorkWithAudit<YourDbContext>(
provider => provider.GetRequiredService<ICurrentUserService>().UserId);Option B: Using HttpContext directly
services.AddHttpContextAccessor();
services.AddGenericUnitOfWorkWithAudit<YourDbContext>(
provider =>
{
var httpContextAccessor = provider.GetRequiredService<IHttpContextAccessor>();
return httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
});Option C: Using IUserContext interface
public class MyUserContext : IUserContext
{
public MyUserContext(ICurrentUserService currentUserService)
{
CurrentUser = currentUserService.UserId;
}
public string? CurrentUser { get; }
}
services.AddScoped<IUserContext, MyUserContext>();
services.AddGenericUnitOfWorkWithAudit<YourDbContext, MyUserContext>();Simple Setup - Automatic Handler Registration:
// All-in-one: Domain events + automatic handler registration from calling assembly
services.AddDomainEventsWithHandlers();
// Or from specific assembly
services.AddDomainEventsWithHandlers(typeof(ProductCreatedEvent).Assembly);Advanced Setup - Manual Control:
// Basic domain events support
services.AddDomainEvents();
// Automatic handler registration from multiple assemblies
services.AddDomainEventHandlersFromAssemblies(
typeof(ProductEvents).Assembly,
typeof(OrderEvents).Assembly
);
// Or manual registration (still supported)
services.AddDomainEventHandler<ProductCreatedEvent, ProductCreatedEventHandler>();
services.AddDomainEventHandler<ProductUpdatedEvent, ProductUpdatedEventHandler>();
// Custom filter-based registration
services.AddDomainEventHandlers(
typeof(ProductEvents).Assembly,
type => type.Name.EndsWith("Handler") && !type.Name.Contains("Test"),
ServiceLifetime.Scoped
);
// Attribute-based registration
services.AddAttributedDomainEventHandlers(typeof(ProductEvents).Assembly);public class Product : BaseAuditableEntity<int>
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Description { get; set; } = string.Empty;
}
// When you save a product, these properties are automatically set:
// - CreatedAt: DateTime.UtcNow (when entity is first created)
// - CreatedBy: Current user ID (from your user context)
// - UpdatedAt: DateTime.UtcNow (when entity is modified)
// - UpdatedBy: Current user ID (when entity is modified)// Create a soft deletable entity by implementing ISoftDelete interface
public class Product : BaseAuditableEntity<int>, ISoftDelete
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Description { get; set; } = string.Empty;
// ISoftDelete properties - automatically handled by AuditInterceptor
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public string? DeletedBy { get; set; }
}
// When you delete a product:
// - IsDeleted: Set to true (logical deletion)
// - DeletedAt: DateTime.UtcNow (when entity is soft deleted)
// - DeletedBy: Current user ID (who performed the deletion)public class Product : BaseAuditableEntity<int>, ISoftDelete
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Description { get; set; } = string.Empty;
// ISoftDelete properties
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public string? DeletedBy { get; set; }
// Factory method that raises domain events
public static Product Create(string name, decimal price, string description)
{
var product = new Product
{
Name = name,
Price = price,
Description = description
};
// Raise domain event (completely optional)
product.AddDomainEvent(new ProductCreatedEvent(product.Id, product.Name, product.Price));
return product;
}
public void UpdatePrice(decimal newPrice)
{
var oldPrice = Price;
Price = newPrice;
// Raise domain event for price change
AddDomainEvent(new ProductPriceChangedEvent(Id, oldPrice, newPrice));
}
public void Delete()
{
// Add domain event before deletion
AddDomainEvent(new ProductDeletedEvent(Id, Name));
}
}public class ProductService
{
private readonly IUnitOfWork _unitOfWork;
public ProductService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task<Product> CreateProductAsync(string name, decimal price, string description)
{
// Using factory method that raises domain events
var product = Product.Create(name, price, description);
var repository = _unitOfWork.GetRepository<Product, int>();
await repository.AddAsync(product);
// Domain events are automatically dispatched during SaveChanges
await _unitOfWork.SaveChangesAsync();
return product;
}
public async Task DeleteProductAsync(int productId)
{
var repository = _unitOfWork.GetRepository<Product, int>();
var product = await repository.GetByIdAsync(productId);
if (product != null)
{
product.Delete(); // Raises domain event
// Soft delete (if entity implements ISoftDelete)
await repository.DeleteAsync(product, saveChanges: true);
}
}
public async Task RestoreProductAsync(int productId)
{
var repository = _unitOfWork.GetRepository<Product, int>();
// Restore a soft-deleted product
await repository.RestoreAsync(productId, saveChanges: true);
}
public async Task<IPaginate<Product>> GetProductsAsync(int page, int size)
{
var repository = _unitOfWork.GetRepository<Product, int>();
return await repository.GetPagedAsync(page, size);
}
}var filter = new FilterModel
{
SearchTerm = "laptop",
Filters = new List<FilterItem>
{
new() { Field = "Price", Operator = "greaterthan", Value = "100" },
new() { Field = "Name", Operator = "contains", Value = "gaming" }
}
};
var products = await repository.GetPagedWithFilterAsync(filter, 1, 10);public class ExpensiveProductsSpecification : BaseSpecification<Product>
{
public ExpensiveProductsSpecification(decimal minPrice)
{
AddCriteria(p => p.Price >= minPrice);
AddInclude(p => p.Category);
ApplyOrderByDescending(p => p.Price);
}
}
// Usage
var spec = new ExpensiveProductsSpecification(1000);
var expensiveProducts = await repository.GetAsync(spec);// Basic pagination
var pagedProducts = await repository.GetPagedAsync(pageIndex: 1, pageSize: 10);
// Pagination with filtering and ordering
var pagedProducts = await repository.GetPagedAsync(
pageIndex: 1,
pageSize: 10,
predicate: p => p.Price > 100,
orderBy: query => query.OrderBy(p => p.Name),
includes: new List<Expression<Func<Product, object>>> { p => p.Category }
);
// Access pagination metadata
Console.WriteLine($"Total items: {pagedProducts.Count}");
Console.WriteLine($"Total pages: {pagedProducts.Pages}");
Console.WriteLine($"Has next page: {pagedProducts.HasNext}");
Console.WriteLine($"Has previous page: {pagedProducts.HasPrevious}");public class OrderService
{
private readonly IUnitOfWork _unitOfWork;
public OrderService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task CreateOrderWithProductsAsync(Order order, List<Product> products)
{
// Manual transaction management
await _unitOfWork.BeginTransactionAsync();
try
{
var orderRepository = _unitOfWork.GetRepository<Order, int>();
var productRepository = _unitOfWork.GetRepository<Product, int>();
await orderRepository.AddAsync(order);
await productRepository.BulkInsertAsync(products);
await _unitOfWork.SaveChangesAsync();
await _unitOfWork.CommitTransactionAsync();
}
catch
{
await _unitOfWork.RollbackTransactionAsync();
throw;
}
}
public async Task<Order> CreateOrderWithTransactionScopeAsync(Order order)
{
// Automatic transaction management
return await _unitOfWork.ExecuteInTransactionAsync(async () =>
{
var repository = _unitOfWork.GetRepository<Order, int>();
await repository.AddAsync(order);
await _unitOfWork.SaveChangesAsync();
return order;
});
}
}// In your DbContext's OnModelCreating method:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Apply global query filters to exclude soft-deleted entities
modelBuilder.ApplySoftDeleteQueryFilters();
}
// Usage examples:
// Normal queries automatically exclude soft-deleted entities
var activeProducts = await repository.GetAllAsync(); // Only non-deleted products
// Include soft-deleted entities when needed
var allProducts = await repository.GetQueryable()
.IncludeDeleted()
.ToListAsync();
// Get only soft-deleted entities
var deletedProducts = await repository.GetQueryable()
.OnlyDeleted()
.ToListAsync();
// Soft delete (entity must implement ISoftDelete)
await repository.DeleteAsync(productId, saveChanges: true);
// Sets: IsDeleted = true, DeletedAt = DateTime.UtcNow, DeletedBy = currentUser
// Restore a soft-deleted entity
await repository.RestoreAsync(productId, saveChanges: true);
// Sets: IsDeleted = false, DeletedAt = null, DeletedBy = null
// Check if entity is soft deletable
if (typeof(ISoftDelete).IsAssignableFrom(typeof(Product)))
{
// Entity supports soft delete operations
await repository.RestoreAsync(productId);
}// Domain event for product creation
public class ProductCreatedEvent : DomainEvent
{
public ProductCreatedEvent(int productId, string productName, decimal price)
{
ProductId = productId;
ProductName = productName;
Price = price;
}
public int ProductId { get; }
public string ProductName { get; }
public decimal Price { get; }
}
// Domain event for product deletion
public class ProductDeletedEvent : DomainEvent
{
public ProductDeletedEvent(int productId, string productName)
{
ProductId = productId;
ProductName = productName;
}
public int ProductId { get; }
public string ProductName { get; }
}Basic Handler:
public class ProductCreatedEventHandler : IDomainEventHandler<ProductCreatedEvent>
{
private readonly ILogger<ProductCreatedEventHandler> _logger;
private readonly IEmailService _emailService;
public ProductCreatedEventHandler(
ILogger<ProductCreatedEventHandler> logger,
IEmailService emailService)
{
_logger = logger;
_emailService = emailService;
}
public async Task Handle(ProductCreatedEvent domainEvent, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Product created: {ProductName} with price: {Price}",
domainEvent.ProductName, domainEvent.Price);
// Send notification email
await _emailService.SendProductCreatedNotificationAsync(
domainEvent.ProductName,
domainEvent.Price,
cancellationToken);
}
}Advanced Handler with Attributes:
[DomainEventHandler(ServiceLifetime = ServiceLifetime.Scoped, Order = 1)]
public class ProductCreatedAuditHandler : IDomainEventHandler<ProductCreatedEvent>
{
private readonly IAuditService _auditService;
public ProductCreatedAuditHandler(IAuditService auditService)
{
_auditService = auditService;
}
public async Task Handle(ProductCreatedEvent domainEvent, CancellationToken cancellationToken = default)
{
await _auditService.LogEventAsync("ProductCreated", domainEvent, cancellationToken);
}
}
// Handler for multiple events
public class GeneralAuditHandler :
IDomainEventHandler<ProductCreatedEvent>,
IDomainEventHandler<ProductDeletedEvent>
{
private readonly IAuditService _auditService;
public GeneralAuditHandler(IAuditService auditService)
{
_auditService = auditService;
}
public async Task Handle(ProductCreatedEvent domainEvent, CancellationToken cancellationToken = default)
{
await _auditService.LogEventAsync("ProductCreated", domainEvent, cancellationToken);
}
public async Task Handle(ProductDeletedEvent domainEvent, CancellationToken cancellationToken = default)
{
await _auditService.LogEventAsync("ProductDeleted", domainEvent, cancellationToken);
}
}// Example: MediatR integration
public class MediatRDomainEventDispatcher : IDomainEventDispatcher
{
private readonly IMediator _mediator;
public MediatRDomainEventDispatcher(IMediator mediator)
{
_mediator = mediator;
}
public async Task DispatchAsync(IDomainEvent domainEvent, CancellationToken cancellationToken = default)
{
await _mediator.Publish(domainEvent, cancellationToken);
}
public async Task DispatchAsync(IEnumerable<IDomainEvent> domainEvents, CancellationToken cancellationToken = default)
{
foreach (var domainEvent in domainEvents)
{
await _mediator.Publish(domainEvent, cancellationToken);
}
}
}
// Register with Fluent API
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
services.AddFSEntityFramework<YourDbContext>()
.WithDomainEvents()
.UsingCustomDispatcher<MediatRDomainEventDispatcher>()
.WithAutoHandlerDiscovery(typeof(ProductCreatedEvent).Assembly)
.Complete()
.Build();Core repository interface providing:
Coordinates multiple repositories and provides:
Flexible specification implementation supporting:
Framework-agnostic domain event system providing:
[DomainEventHandler] attributeMain Builder Interface:
Configuration Builders:
Extension Methods:
Type-safe audit implementation:
BaseEntity<TKey>: Simple entity with Id property and optional domain events supportBaseAuditableEntity<TKey>: Entity with creation and modification audit properties and domain eventsValueObject: Base class for value objects with equality comparisonISoftDelete interfaceRestoreAsync methods to recover deleted entities| Feature | Fluent API | Classic API |
|---|---|---|
| Readability | ✅ Excellent | ⚠️ Good |
| Validation | ✅ Built-in | ❌ Manual |
| Chaining | ✅ Yes | ❌ No |
| Conditional Setup | ✅ Yes | ⚠️ Limited |
| Error Messages | ✅ Helpful | ⚠️ Generic |
| IDE Support | ✅ Excellent | ⚠️ Good |
// 1. Simplest - One line setup (recommended for most projects)
services.AddFSEntityFramework<MyDbContext>()
.WithDomainEvents()
.UsingDefaultDispatcher()
.WithAutoHandlerDiscovery()
.Complete()
.Build();
// 2. Specific assembly
services.AddFSEntityFramework<MyDbContext>()
.WithDomainEvents()
.WithAutoHandlerDiscovery(typeof(ProductEvents).Assembly)
.Complete()
.Build();
// 3. Multiple assemblies
services.AddFSEntityFramework<MyDbContext>()
.WithDomainEvents()
.WithAutoHandlerDiscovery(
typeof(ProductEvents).Assembly,
typeof(OrderEvents).Assembly)
.Complete()
.Build();
// 4. Custom filtering
services.AddFSEntityFramework<MyDbContext>()
.WithDomainEvents()
.WithCustomHandlerDiscovery(
assembly,
type => type.Name.EndsWith("Handler"),
ServiceLifetime.Scoped)
.Complete()
.Build();
// 5. Attribute-based (advanced control)
services.AddFSEntityFramework<MyDbContext>()
.WithDomainEvents()
.WithAttributeBasedDiscovery(assembly)
.Complete()
.Build();
// 6. Manual (fine-grained control)
services.AddFSEntityFramework<MyDbContext>()
.WithDomainEvents()
.WithHandler<ProductCreatedEvent, ProductCreatedEventHandler>()
.Complete()
.Build();// Option 1: Implement ISoftDelete on existing auditable entity
public class Product : BaseAuditableEntity<int>, ISoftDelete
{
// Entity properties
public string Name { get; set; } = string.Empty;
// ISoftDelete properties (required)
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public string? DeletedBy { get; set; }
}
// Option 2: Only basic audit (no soft delete)
public class Category : BaseAuditableEntity<int>
{
public string Name { get; set; } = string.Empty;
// No ISoftDelete - hard deletes only
}
// Option 3: Minimal entity (no audit, no soft delete)
public class Tag : BaseEntity<int>
{
public string Name { get; set; } = string.Empty;
// Only basic entity with domain events support
}// Entities that implement ISoftDelete automatically support:
await repository.DeleteAsync(entity); // Soft delete
await repository.RestoreAsync(entity); // Restore
await repository.RestoreAsync(id); // Restore by ID
// Entities that don't implement ISoftDelete:
await repository.DeleteAsync(entity); // Hard delete only
// repository.RestoreAsync() throws InvalidOperationExceptionBefore (Classic):
services.AddGenericUnitOfWorkWithAudit<MyDbContext>(
provider => provider.GetRequiredService<ICurrentUserService>().UserId);
services.AddDomainEvents();
services.AddDomainEventHandler<ProductCreatedEvent, ProductCreatedEventHandler>();After (Fluent):
services.AddFSEntityFramework<MyDbContext>()
.WithAudit()
.UsingUserProvider(provider =>
provider.GetRequiredService<ICurrentUserService>().UserId)
.WithDomainEvents()
.UsingDefaultDispatcher()
.WithHandler<ProductCreatedEvent, ProductCreatedEventHandler>()
.Complete()
.Build();Use Fluent API for new projects:
services.AddFSEntityFramework<MyDbContext>()
.WithAudit()
.UsingHttpContext()
.ValidateConfiguration()
.Build();
Validate configuration in development:
services.AddFSEntityFramework<MyDbContext>()
.WithAudit()
.UsingHttpContext()
.When(isDevelopment, builder => builder.ValidateConfiguration())
.Build();
Use conditional configuration for different environments:
services.AddFSEntityFramework<MyDbContext>()
.WithAudit()
.UsingHttpContext()
.When(isProduction, builder =>
builder.WithDomainEvents()
.UsingCustomDispatcher<ProductionEventDispatcher>()
.WithAutoHandlerDiscovery()
.Complete())
.Build();
Implement interfaces based on needs:
// For entities that need audit + soft delete
public class Product : BaseAuditableEntity<int>, ISoftDelete
// For entities that only need audit
public class Category : BaseAuditableEntity<int>
// For simple entities
public class Tag : BaseEntity<int>
Use factory methods for domain events:
public static Product Create(string name, decimal price)
{
var product = new Product { Name = name, Price = price };
product.AddDomainEvent(new ProductCreatedEvent(product.Id, name));
return product;
}
Use specifications for complex queries:
public class ActiveExpensiveProductsSpec : BaseSpecification<Product>
{
public ActiveExpensiveProductsSpec(decimal minPrice)
{
AddCriteria(p => p.Price >= minPrice && !p.IsDeleted);
AddInclude(p => p.Category);
ApplyOrderByDescending(p => p.CreatedAt);
}
}
Use Unit of Work for transactions:
public async Task<Order> ProcessOrderAsync(CreateOrderRequest request)
{
return await _unitOfWork.ExecuteInTransactionAsync(async () =>
{
// Multiple repository operations
var order = await CreateOrderAsync(request);
await UpdateInventoryAsync(request.Items);
await SendNotificationAsync(order);
return order;
});
}
disableTracking: true for read-only operationsDbContext not registered error:
// Ensure DbContext is registered before AddFSEntityFramework
services.AddDbContext<MyDbContext>(options => options.UseSqlServer(connectionString));
services.AddFSEntityFramework<MyDbContext>(); // Then register FS.EntityFramework
Handler not found errors:
// Ensure handlers are in the scanned assembly
services.AddFSEntityFramework<MyDbContext>()
.WithDomainEvents()
.WithAutoHandlerDiscovery(typeof(MyHandler).Assembly) // Specify correct assembly
.Complete()
.Build();
Audit properties not being set:
// Ensure audit is configured and user context is available
services.AddFSEntityFramework<MyDbContext>()
.WithAudit()
.UsingHttpContext() // or other user provider
.Build();
Enable validation in development:
services.AddFSEntityFramework<MyDbContext>()
.ValidateConfiguration() // Will throw helpful errors
.Build();
Check registration with validation:
services.AddFSEntityFramework<MyDbContext>()
.WithServices(s =>
{
// Add debugging services
s.AddSingleton<IValidationService, ValidationService>();
})
.ValidateConfiguration()
.Build();
We welcome contributions! This project is open source and benefits from community involvement:
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)Development Guidelines:
Areas for Contribution:
This project is licensed under the MIT License. See the LICENSE file for details.
Breaking Changes: None - Fully backward compatible with existing configurations
If you find this library useful, please consider giving it a star on GitHub! It helps others discover the project.
Made with ❤️ by Furkan Sarıkaya
If you encounter any issues or have questions:
Happy coding! 🚀