FS.EntityFramework.Library.GuidV7 9.0.6.9

There is a newer version of this package available.
See the version list below for details.
dotnet add package FS.EntityFramework.Library.GuidV7 --version 9.0.6.9
                    
NuGet\Install-Package FS.EntityFramework.Library.GuidV7 -Version 9.0.6.9
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FS.EntityFramework.Library.GuidV7" Version="9.0.6.9" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FS.EntityFramework.Library.GuidV7" Version="9.0.6.9" />
                    
Directory.Packages.props
<PackageReference Include="FS.EntityFramework.Library.GuidV7" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FS.EntityFramework.Library.GuidV7 --version 9.0.6.9
                    
#r "nuget: FS.EntityFramework.Library.GuidV7, 9.0.6.9"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FS.EntityFramework.Library.GuidV7@9.0.6.9
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FS.EntityFramework.Library.GuidV7&version=9.0.6.9
                    
Install as a Cake Addin
#tool nuget:?package=FS.EntityFramework.Library.GuidV7&version=9.0.6.9
                    
Install as a Cake Tool

FS.EntityFramework.Library

NuGet Version NuGet Downloads GitHub License GitHub Stars

A comprehensive, production-ready Entity Framework Core library providing Repository pattern, Unit of Work, Specification pattern, dynamic filtering, pagination support, Domain Events, Fluent Configuration API, and modular ID generation strategies for .NET applications.

🌟 Why Choose FS.EntityFramework.Library?

graph TB
    A[🎯 FS.EntityFramework.Library] --> B[πŸ—οΈ Repository Pattern]
    A --> C[πŸ”„ Unit of Work]
    A --> D[πŸ“‹ Specification Pattern]
    A --> E[πŸ” Dynamic Filtering]
    A --> F[πŸ“„ Pagination]
    A --> G[🎭 Domain Events]
    A --> H[βš™οΈ Fluent Configuration]
    A --> I[πŸ”‘ Modular ID Generation]
    A --> J[πŸ“Š Audit Tracking]
    A --> K[πŸ—‘οΈ Soft Delete & Restore]

πŸ“‹ Table of Contents

πŸš€ Quick Start

1️⃣ Basic Setup (30 seconds)

// 1. Install NuGet package
dotnet add package FS.EntityFramework.Library

// 2. Configure your DbContext
services.AddDbContext<YourDbContext>(options =>
    options.UseSqlServer(connectionString));

// 3. Add FS.EntityFramework (One line setup!)
services.AddFSEntityFramework<YourDbContext>()
    .Build();

// 4. Create your entities
public class Product : BaseAuditableEntity<int>
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
}

// 5. Use in your services
public class ProductService
{
    private readonly IUnitOfWork _unitOfWork;
    
    public ProductService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    public async Task<Product> CreateProductAsync(string name, decimal price)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var product = new Product { Name = name, Price = price };
        
        await repository.AddAsync(product);
        await _unitOfWork.SaveChangesAsync();
        
        return product;
    }
}
services.AddFSEntityFramework<YourDbContext>()
    .WithAudit()                          // πŸ“Š Automatic audit tracking
        .UsingHttpContext()               // πŸ‘€ User tracking via HTTP context
    .WithDomainEvents()                   // 🎭 Domain events support
        .UsingDefaultDispatcher()         // πŸ“‘ Default event dispatcher
        .WithAutoHandlerDiscovery()       // πŸ” Auto-discover event handlers
    .Complete()                           // βœ… Complete domain events setup
    .WithSoftDelete()                     // πŸ—‘οΈ Soft delete functionality
    .WithGuidV7()                         // πŸ”‘ GUID V7 ID generation (requires extension package)
    .ValidateConfiguration()              // βœ… Validate setup
    .Build();

πŸ’Ύ Installation

Core Package

# Core library with all essential features
dotnet add package FS.EntityFramework.Library

Extension Packages (Optional)

# GUID Version 7 ID generation (.NET 9+)
dotnet add package FS.EntityFramework.Library.GuidV7

# ULID ID generation
dotnet add package FS.EntityFramework.Library.Ulid

Requirements

  • .NET 9.0 or later
  • Entity Framework Core 9.0.6 or later
  • Microsoft.AspNetCore.Http.Abstractions 2.3.0 or later (for HttpContext support)

βš™οΈ Configuration

The Fluent Configuration API provides an intuitive, chainable way to configure the library with better readability and validation.

graph LR
    A[AddFSEntityFramework] --> B[WithAudit]
    A --> C[WithDomainEvents]
    A --> D[WithSoftDelete]
    A --> E[WithIdGeneration]
    B --> F[UsingHttpContext]
    B --> G[UsingUserProvider]
    B --> H[UsingUserContext]
    C --> I[UsingDefaultDispatcher]
    C --> J[UsingCustomDispatcher]
    C --> K[WithAutoHandlerDiscovery]
    I --> L[Complete]
    J --> L
    K --> L
    L --> M[Build]
    D --> M
    E --> M
    F --> M
    G --> M
    H --> M
Basic Configuration Options
// πŸ”§ Minimal setup
services.AddFSEntityFramework<YourDbContext>()
    .Build();

// πŸ“Š With audit tracking
services.AddFSEntityFramework<YourDbContext>()
    .WithAudit()
        .UsingHttpContext()
    .Build();

// 🎭 With domain events
services.AddFSEntityFramework<YourDbContext>()
    .WithDomainEvents()
        .UsingDefaultDispatcher()
        .WithAutoHandlerDiscovery()
    .Complete()
    .Build();

// πŸ—‘οΈ With soft delete
services.AddFSEntityFramework<YourDbContext>()
    .WithSoftDelete()
    .Build();
Advanced Configuration
services.AddFSEntityFramework<YourDbContext>()
    // πŸ“Š Audit Configuration
    .WithAudit()
        .UsingUserProvider(provider => 
        {
            var userService = provider.GetService<ICurrentUserService>();
            return userService?.GetCurrentUserId();
        })
    
    // 🎭 Domain Events Configuration
    .WithDomainEvents()
        .UsingDefaultDispatcher()
        .WithAutoHandlerDiscovery(typeof(ProductCreatedEvent).Assembly)
        .WithAttributeBasedDiscovery(Assembly.GetExecutingAssembly())
        .WithHandler<ProductCreatedEvent, ProductCreatedEventHandler>()
    .Complete()
    
    // πŸ—‘οΈ Soft Delete Configuration
    .WithSoftDelete()
    
    // πŸ”‘ ID Generation Configuration
    .WithIdGeneration()
        .WithGenerator<Guid, MyCustomGuidGenerator>()
    .Complete()
    
    // 🎯 Custom Repository Registration
    .WithCustomRepository<Product, int, ProductRepository>()
    .WithRepositoriesFromAssembly(Assembly.GetExecutingAssembly())
    
    // βš™οΈ Additional Services
    .WithServices(services =>
    {
        services.AddScoped<IMyCustomService, MyCustomService>();
    })
    
    // πŸ” Conditional Configuration
    .When(isDevelopment, builder =>
        builder.WithDetailedLogging(enableSensitiveDataLogging: true))
    
    // βœ… Validation & Build
    .ValidateConfiguration()
    .Build();
User Context Configuration Options
// 1️⃣ Using HttpContext (Web Applications)
services.AddFSEntityFramework<YourDbContext>()
    .WithAudit()
        .UsingHttpContext("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier")
    .Build();

// 2️⃣ Using Custom User Provider
services.AddFSEntityFramework<YourDbContext>()
    .WithAudit()
        .UsingUserProvider(provider =>
        {
            var userService = provider.GetService<ICurrentUserService>();
            return userService?.GetCurrentUserId();
        })
    .Build();

// 3️⃣ Using Interface-Based Approach
public class MyUserContext : IUserContext
{
    private readonly ICurrentUserService _userService;
    
    public MyUserContext(ICurrentUserService userService)
    {
        _userService = userService;
    }
    
    public string? CurrentUser => _userService.UserId;
}

services.AddScoped<IUserContext, MyUserContext>();
services.AddFSEntityFramework<YourDbContext>()
    .WithAudit()
        .UsingUserContext<IUserContext>()
    .Build();

// 4️⃣ Using Static User (Testing)
services.AddFSEntityFramework<YourDbContext>()
    .WithAudit()
        .UsingStaticUser("test-user-123")
    .Build();

πŸ”§ Classic Configuration

The original configuration methods are still supported for backward compatibility:

// Basic setup without audit
services.AddGenericUnitOfWork<YourDbContext>();

// With audit support using user service
services.AddGenericUnitOfWorkWithAudit<YourDbContext>(
    provider => provider.GetRequiredService<ICurrentUserService>().UserId);

// With audit support using HttpContext
services.AddHttpContextAccessor();
services.AddGenericUnitOfWorkWithAudit<YourDbContext>(
    provider =>
    {
        var httpContextAccessor = provider.GetRequiredService<IHttpContextAccessor>();
        return httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    });

// With domain events
services.AddDomainEvents();
services.AddDomainEventHandlersFromAssembly(typeof(ProductCreatedEvent).Assembly);

πŸ—οΈ Core Concepts

πŸ“¦ Base Entities

The library provides several base entity classes to build your domain models:

// 1️⃣ Basic Entity (ID + Domain Events)
public class Tag : BaseEntity<int>
{
    public string Name { get; set; } = string.Empty;
    public string Color { get; set; } = "#000000";
}

// 2️⃣ Auditable Entity (ID + Audit Properties + Domain Events)
public class Category : BaseAuditableEntity<int>
{
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    
    // Automatic properties:
    // - CreatedAt, CreatedBy
    // - UpdatedAt, UpdatedBy
}

// 3️⃣ Full-Featured Entity (ID + Audit + Soft Delete + Domain Events)
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;
    public int CategoryId { get; set; }
    
    // Navigation property
    public Category Category { get; set; } = null!;
    
    // ISoftDelete properties (automatic)
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
    public string? DeletedBy { get; set; }
    
    // Factory method with domain events
    public static Product Create(string name, decimal price, string description, int categoryId)
    {
        var product = new Product
        {
            Name = name,
            Price = price,
            Description = description,
            CategoryId = categoryId
        };
        
        // Raise domain event
        product.AddDomainEvent(new ProductCreatedEvent(product.Id, name, price));
        
        return product;
    }
    
    public void UpdatePrice(decimal newPrice)
    {
        var oldPrice = Price;
        Price = newPrice;
        
        // Raise domain event
        AddDomainEvent(new ProductPriceChangedEvent(Id, oldPrice, newPrice));
    }
}
Base Entity Hierarchy
classDiagram
    class BaseEntity~TKey~ {
        +TKey Id
        +IReadOnlyCollection~IDomainEvent~ DomainEvents
        +AddDomainEvent(IDomainEvent event)
        +RemoveDomainEvent(IDomainEvent event)
        +ClearDomainEvents()
    }
    
    class BaseAuditableEntity~TKey~ {
        +DateTime CreatedAt
        +string? CreatedBy
        +DateTime? UpdatedAt
        +string? UpdatedBy
    }
    
    class ISoftDelete {
        +bool IsDeleted
        +DateTime? DeletedAt
        +string? DeletedBy
    }
    
    class ICreationAuditableEntity {
        +DateTime CreatedAt
        +string? CreatedBy
    }
    
    class IModificationAuditableEntity {
        +DateTime? UpdatedAt
        +string? UpdatedBy
    }
    
    BaseEntity~TKey~ <|-- BaseAuditableEntity~TKey~
    BaseAuditableEntity~TKey~ --|> ICreationAuditableEntity
    BaseAuditableEntity~TKey~ --|> IModificationAuditableEntity
    BaseAuditableEntity~TKey~ ..|> ISoftDelete : implements (optional)

πŸ›οΈ Repository Pattern

The library provides a generic repository implementation with advanced querying capabilities:

public interface IRepository<TEntity, TKey>
{
    // Basic CRUD
    Task<TEntity?> GetByIdAsync(TKey id);
    Task<IReadOnlyList<TEntity>> GetAllAsync();
    Task<TEntity> AddAsync(TEntity entity, bool saveChanges = false);
    Task UpdateAsync(TEntity entity, bool saveChanges = false);
    Task DeleteAsync(TEntity entity, bool saveChanges = false);
    
    // Soft Delete Support
    Task RestoreAsync(TEntity entity, bool saveChanges = false);
    Task HardDeleteAsync(TEntity entity, bool saveChanges = false);
    
    // Advanced Querying
    Task<IReadOnlyList<TEntity>> GetWithIncludesAsync(
        Expression<Func<TEntity, bool>>? predicate = null,
        Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
        List<Expression<Func<TEntity, object>>>? includes = null);
    
    // Specification Pattern
    Task<IReadOnlyList<TEntity>> GetAsync(BaseSpecification<TEntity> spec);
    
    // Pagination
    Task<IPaginate<TEntity>> GetPagedAsync(int pageIndex, int pageSize);
    
    // Dynamic Filtering
    Task<IPaginate<TEntity>> GetPagedWithFilterAsync(FilterModel filter, int pageIndex, int pageSize);
    
    // Bulk Operations
    Task BulkInsertAsync(IEnumerable<TEntity> entities);
    Task BulkDeleteAsync(Expression<Func<TEntity, bool>> predicate);
    
    // Raw Querying
    IQueryable<TEntity> GetQueryable(bool disableTracking = true);
}
Repository Usage Examples
public class ProductService
{
    private readonly IUnitOfWork _unitOfWork;
    
    public ProductService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    // βœ… Basic CRUD Operations
    public async Task<Product> CreateProductAsync(string name, decimal price)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var product = Product.Create(name, price, "", 1);
        
        await repository.AddAsync(product);
        await _unitOfWork.SaveChangesAsync();
        
        return product;
    }
    
    // βœ… Advanced Querying with Includes
    public async Task<IReadOnlyList<Product>> GetProductsWithCategoryAsync()
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        
        return await repository.GetWithIncludesAsync(
            predicate: p => p.Price > 100,
            orderBy: query => query.OrderBy(p => p.Name),
            includes: new List<Expression<Func<Product, object>>> { p => p.Category }
        );
    }
    
    // βœ… Pagination
    public async Task<IPaginate<Product>> GetProductsPagedAsync(int page, int size)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        
        return await repository.GetPagedAsync(
            pageIndex: page,
            pageSize: size,
            predicate: p => !p.IsDeleted,
            orderBy: query => query.OrderByDescending(p => p.CreatedAt)
        );
    }
    
    // βœ… Soft Delete Operations
    public async Task DeleteProductAsync(int productId)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var product = await repository.GetByIdAsync(productId);
        
        if (product != null)
        {
            // Soft delete (sets IsDeleted = true, DeletedAt = now, DeletedBy = currentUser)
            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);
    }
    
    // βœ… Bulk Operations
    public async Task BulkCreateProductsAsync(List<ProductDto> productDtos)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var products = productDtos.Select(dto => Product.Create(dto.Name, dto.Price, dto.Description, dto.CategoryId));
        
        await repository.BulkInsertAsync(products, saveChanges: true);
    }
}

πŸ”„ Unit of Work Pattern

The Unit of Work pattern coordinates multiple repositories and manages transactions:

graph TB
    A[IUnitOfWork] --> B[GetRepository&lt;TEntity, TKey&gt;]
    A --> C[SaveChangesAsync]
    A --> D[BeginTransactionAsync]
    A --> E[CommitTransactionAsync]
    A --> F[RollbackTransactionAsync]
    A --> G[ExecuteInTransactionAsync]
    
    B --> H[Repository&lt;Product, int&gt;]
    B --> I[Repository&lt;Category, int&gt;]
    B --> J[Repository&lt;Order, int&gt;]
    
    H --> K[CRUD Operations]
    I --> K
    J --> K
    
    D --> L[Transaction Scope]
    E --> L
    F --> L
    G --> L
Unit of Work Usage Examples
public class OrderService
{
    private readonly IUnitOfWork _unitOfWork;
    
    public OrderService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    // βœ… Coordinating Multiple Repositories
    public async Task<Order> CreateOrderWithItemsAsync(CreateOrderRequest request)
    {
        var orderRepository = _unitOfWork.GetRepository<Order, int>();
        var productRepository = _unitOfWork.GetRepository<Product, int>();
        var orderItemRepository = _unitOfWork.GetRepository<OrderItem, int>();
        
        // Create order
        var order = Order.Create(request.CustomerId, request.OrderDate);
        await orderRepository.AddAsync(order);
        
        // Create order items
        foreach (var itemRequest in request.Items)
        {
            var product = await productRepository.GetByIdAsync(itemRequest.ProductId);
            if (product != null)
            {
                var orderItem = OrderItem.Create(order.Id, itemRequest.ProductId, itemRequest.Quantity, product.Price);
                await orderItemRepository.AddAsync(orderItem);
            }
        }
        
        // Save all changes in a single transaction
        await _unitOfWork.SaveChangesAsync();
        
        return order;
    }
    
    // βœ… Manual Transaction Management
    public async Task<Order> CreateOrderWithManualTransactionAsync(CreateOrderRequest request)
    {
        await _unitOfWork.BeginTransactionAsync();
        
        try
        {
            var orderRepository = _unitOfWork.GetRepository<Order, int>();
            var order = Order.Create(request.CustomerId, request.OrderDate);
            
            await orderRepository.AddAsync(order);
            await _unitOfWork.SaveChangesAsync();
            
            // Simulate external API call that might fail
            await CallExternalPaymentServiceAsync(order);
            
            await _unitOfWork.CommitTransactionAsync();
            return order;
        }
        catch
        {
            await _unitOfWork.RollbackTransactionAsync();
            throw;
        }
    }
    
    // βœ… Automatic Transaction Management
    public async Task<Order> CreateOrderWithAutoTransactionAsync(CreateOrderRequest request)
    {
        return await _unitOfWork.ExecuteInTransactionAsync(async () =>
        {
            var orderRepository = _unitOfWork.GetRepository<Order, int>();
            var order = Order.Create(request.CustomerId, request.OrderDate);
            
            await orderRepository.AddAsync(order);
            await _unitOfWork.SaveChangesAsync();
            
            // Any exception here will automatically rollback the transaction
            await CallExternalPaymentServiceAsync(order);
            
            return order;
        });
    }
}

πŸ“‹ Specification Pattern

The Specification pattern enables building reusable and composable query specifications:

// βœ… Base Specification Example
public class ActiveProductsSpecification : BaseSpecification<Product>
{
    public ActiveProductsSpecification()
    {
        AddCriteria(p => !p.IsDeleted);
        AddInclude(p => p.Category);
        ApplyOrderBy(p => p.Name);
    }
}

// βœ… Parameterized Specification
public class ProductsByCategorySpecification : BaseSpecification<Product>
{
    public ProductsByCategorySpecification(int categoryId, decimal? minPrice = null)
    {
        AddCriteria(p => p.CategoryId == categoryId && !p.IsDeleted);
        
        if (minPrice.HasValue)
        {
            AddCriteria(p => p.Price >= minPrice.Value);
        }
        
        AddInclude(p => p.Category);
        ApplyOrderByDescending(p => p.CreatedAt);
    }
}

// βœ… Paginated Specification
public class ExpensiveProductsSpecification : BaseSpecification<Product>
{
    public ExpensiveProductsSpecification(decimal minPrice, int page, int pageSize)
    {
        AddCriteria(p => p.Price >= minPrice && !p.IsDeleted);
        AddInclude(p => p.Category);
        ApplyOrderByDescending(p => p.Price);
        ApplyPaging((page - 1) * pageSize, pageSize);
    }
}

// βœ… Complex Specification with Multiple Includes
public class ProductDetailsSpecification : BaseSpecification<Product>
{
    public ProductDetailsSpecification(int productId)
    {
        AddCriteria(p => p.Id == productId);
        AddInclude(p => p.Category);
        AddInclude("OrderItems.Order.Customer"); // String-based include
    }
}
Using Specifications
public class ProductService
{
    private readonly IUnitOfWork _unitOfWork;
    
    public ProductService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    public async Task<IReadOnlyList<Product>> GetActiveProductsAsync()
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var spec = new ActiveProductsSpecification();
        
        return await repository.GetAsync(spec);
    }
    
    public async Task<IReadOnlyList<Product>> GetProductsByCategoryAsync(int categoryId, decimal? minPrice = null)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var spec = new ProductsByCategorySpecification(categoryId, minPrice);
        
        return await repository.GetAsync(spec);
    }
    
    public async Task<IReadOnlyList<Product>> GetExpensiveProductsAsync(decimal minPrice, int page, int pageSize)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var spec = new ExpensiveProductsSpecification(minPrice, page, pageSize);
        
        return await repository.GetAsync(spec);
    }
}

🎭 Domain Events

Domain Events enable loose coupling and implementing cross-cutting concerns in a clean way.

graph LR
    A[Entity] -->|Raises| B[Domain Event]
    B --> C[Event Dispatcher]
    C --> D[Event Handler 1]
    C --> E[Event Handler 2]
    C --> F[Event Handler N]
    
    D --> G[Send Email]
    E --> H[Update Cache]
    F --> I[Log Audit]

Domain Event Implementation

// βœ… 1. Define Domain Events
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; }
}

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; }
}

// βœ… 2. Create Event Handlers
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);
    }
}

// βœ… 3. 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);
    }
}

// βœ… 4. Multi-Event Handler
public class GeneralNotificationHandler : 
    IDomainEventHandler<ProductCreatedEvent>,
    IDomainEventHandler<ProductPriceChangedEvent>
{
    private readonly INotificationService _notificationService;
    
    public GeneralNotificationHandler(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }
    
    public async Task Handle(ProductCreatedEvent domainEvent, CancellationToken cancellationToken = default)
    {
        await _notificationService.SendNotificationAsync($"New product created: {domainEvent.ProductName}");
    }
    
    public async Task Handle(ProductPriceChangedEvent domainEvent, CancellationToken cancellationToken = default)
    {
        await _notificationService.SendNotificationAsync($"Product price changed from {domainEvent.OldPrice} to {domainEvent.NewPrice}");
    }
}

Domain Event Configuration

// βœ… Automatic Handler Discovery (Simplest)
services.AddFSEntityFramework<YourDbContext>()
    .WithDomainEvents()
        .UsingDefaultDispatcher()
        .WithAutoHandlerDiscovery() // Scans calling assembly
    .Complete()
    .Build();

// βœ… Multiple Assembly Discovery
services.AddFSEntityFramework<YourDbContext>()
    .WithDomainEvents()
        .UsingDefaultDispatcher()
        .WithAutoHandlerDiscovery(
            typeof(ProductCreatedEvent).Assembly,
            typeof(OrderCreatedEvent).Assembly)
    .Complete()
    .Build();

// βœ… Attribute-Based Discovery
services.AddFSEntityFramework<YourDbContext>()
    .WithDomainEvents()
        .UsingDefaultDispatcher()
        .WithAttributeBasedDiscovery(Assembly.GetExecutingAssembly())
    .Complete()
    .Build();

// βœ… Custom Handler Registration
services.AddFSEntityFramework<YourDbContext>()
    .WithDomainEvents()
        .UsingDefaultDispatcher()
        .WithHandler<ProductCreatedEvent, ProductCreatedEventHandler>()
        .WithHandler<ProductPriceChangedEvent, ProductPriceChangedEventHandler>()
    .Complete()
    .Build();

// βœ… Custom Dispatcher (e.g., 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);
        }
    }
}

services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
services.AddFSEntityFramework<YourDbContext>()
    .WithDomainEvents()
        .UsingCustomDispatcher<MediatRDomainEventDispatcher>()
        .WithAutoHandlerDiscovery()
    .Complete()
    .Build();

Using Domain Events in Entities

public class Product : BaseAuditableEntity<int>, ISoftDelete
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    
    // ISoftDelete properties
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
    public string? DeletedBy { get; set; }
    
    // βœ… Factory method with domain events
    public static Product Create(string name, decimal price, string description)
    {
        var product = new Product
        {
            Name = name,
            Price = price
        };
        
        // Domain event is automatically dispatched during SaveChanges
        product.AddDomainEvent(new ProductCreatedEvent(product.Id, product.Name, product.Price));
        
        return product;
    }
    
    public void UpdatePrice(decimal newPrice)
    {
        var oldPrice = Price;
        Price = newPrice;
        
        // Domain event for price change
        AddDomainEvent(new ProductPriceChangedEvent(Id, oldPrice, newPrice));
    }
    
    public void Delete()
    {
        // Domain event before deletion
        AddDomainEvent(new ProductDeletedEvent(Id, Name));
    }
}

πŸ“Š Audit Tracking

Automatic audit tracking for entity lifecycle events with flexible user context providers.

graph TB
    A[Entity Save Operation] --> B{Audit Enabled?}
    B -->|Yes| C[AuditInterceptor]
    B -->|No| D[Regular Save]
    
    C --> E{Entity State?}
    E -->|Added| F[Set CreatedAt, CreatedBy]
    E -->|Modified| G[Set UpdatedAt, UpdatedBy]
    E -->|Deleted| H{Soft Delete?}
    
    H -->|Yes| I[Set IsDeleted, DeletedAt, DeletedBy]
    H -->|No| J[Hard Delete]
    
    F --> K[Get Current User]
    G --> K
    I --> K
    
    K --> L[User Context Provider]
    L --> M[HttpContext]
    L --> N[Custom Service]
    L --> O[Static User]
    L --> P[Interface-Based]

Audit Configuration Examples

// βœ… 1. HttpContext-based User Tracking (Web Apps)
services.AddFSEntityFramework<YourDbContext>()
    .WithAudit()
        .UsingHttpContext() // Uses NameIdentifier claim by default
    .Build();

// βœ… 2. Custom Claim Type
services.AddFSEntityFramework<YourDbContext>()
    .WithAudit()
        .UsingHttpContext("custom-user-id-claim")
    .Build();

// βœ… 3. Custom User Provider
services.AddFSEntityFramework<YourDbContext>()
    .WithAudit()
        .UsingUserProvider(provider =>
        {
            var userService = provider.GetService<ICurrentUserService>();
            return userService?.GetCurrentUserId();
        })
    .Build();

// βœ… 4. 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<IUserContext, ApplicationUserContext>();
services.AddFSEntityFramework<YourDbContext>()
    .WithAudit()
        .UsingUserContext<IUserContext>()
    .Build();

// βœ… 5. Static User (Testing Scenarios)
services.AddFSEntityFramework<YourDbContext>()
    .WithAudit()
        .UsingStaticUser("system-user")
    .Build();

// βœ… 6. Custom Time Provider
services.AddFSEntityFramework<YourDbContext>()
    .WithAudit()
        .UsingUserProvider(
            provider => provider.GetService<ICurrentUserService>()?.GetCurrentUserId(),
            provider => provider.GetService<ITimeService>()?.GetCurrentTime() ?? DateTime.UtcNow)
    .Build();

Audit Interfaces and Properties

// βœ… Creation Audit
public interface ICreationAuditableEntity
{
    DateTime CreatedAt { get; set; }
    string? CreatedBy { get; set; }
}

// βœ… Modification Audit
public interface IModificationAuditableEntity
{
    DateTime? UpdatedAt { get; set; }
    string? UpdatedBy { get; set; }
}

// βœ… Soft Delete Audit
public interface ISoftDelete
{
    bool IsDeleted { get; set; }
    DateTime? DeletedAt { get; set; }
    string? DeletedBy { get; set; }
}

// βœ… Example Entity with Full Audit Support
public class AuditableProduct : BaseAuditableEntity<int>, ISoftDelete
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    
    // Automatic audit properties from BaseAuditableEntity:
    // - CreatedAt: Set when entity is first saved
    // - CreatedBy: Set to current user when entity is first saved
    // - UpdatedAt: Set when entity is modified
    // - UpdatedBy: Set to current user when entity is modified
    
    // ISoftDelete properties (automatically managed):
    public bool IsDeleted { get; set; }       // Set to true on delete
    public DateTime? DeletedAt { get; set; }  // Set to current time on delete
    public string? DeletedBy { get; set; }    // Set to current user on delete
}

Audit Usage Examples

public class ProductService
{
    private readonly IUnitOfWork _unitOfWork;
    
    public ProductService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    public async Task<Product> CreateProductAsync(string name, decimal price)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var product = new Product { Name = name, Price = price };
        
        await repository.AddAsync(product);
        await _unitOfWork.SaveChangesAsync();
        
        // After saving:
        // - product.CreatedAt = DateTime.UtcNow
        // - product.CreatedBy = "current-user-id"
        
        return product;
    }
    
    public async Task UpdateProductPriceAsync(int productId, decimal newPrice)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var product = await repository.GetByIdAsync(productId);
        
        if (product != null)
        {
            product.Price = newPrice;
            await repository.UpdateAsync(product);
            await _unitOfWork.SaveChangesAsync();
            
            // After saving:
            // - product.UpdatedAt = DateTime.UtcNow
            // - product.UpdatedBy = "current-user-id"
        }
    }
    
    public async Task SoftDeleteProductAsync(int productId)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var product = await repository.GetByIdAsync(productId);
        
        if (product != null)
        {
            await repository.DeleteAsync(product, saveChanges: true);
            
            // After saving (if product implements ISoftDelete):
            // - product.IsDeleted = true
            // - product.DeletedAt = DateTime.UtcNow
            // - product.DeletedBy = "current-user-id"
        }
    }
}

πŸ—‘οΈ Soft Delete & Restore

Comprehensive soft delete implementation with restore functionality and query filters.

graph TB
    A[Entity Delete Operation] --> B{Implements ISoftDelete?}
    B -->|Yes| C[Soft Delete]
    B -->|No| D[Hard Delete]
    
    C --> E[Set IsDeleted = true]
    E --> F[Set DeletedAt = DateTime.UtcNow]
    F --> G[Set DeletedBy = CurrentUser]
    G --> H[Update Entity]
    
    D --> I[Remove from Database]
    
    J[Query Operations] --> K[Global Query Filter]
    K --> L[WHERE IsDeleted = false]
    
    M[Special Queries] --> N[IncludeDeleted]
    M --> O[OnlyDeleted]
    
    P[Restore Operation] --> Q[Set IsDeleted = false]
    Q --> R[Set DeletedAt = null]
    R --> S[Set DeletedBy = null]
    S --> T[Update Entity]

Soft Delete Configuration

// βœ… Enable Soft Delete with Global Query Filters
services.AddFSEntityFramework<YourDbContext>()
    .WithSoftDelete()  // Automatically applies global query filters
    .Build();

// βœ… Manual Configuration in DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    
    // Apply soft delete query filters to all ISoftDelete entities
    modelBuilder.ApplySoftDeleteQueryFilters();
}

Soft Delete Entity Design

// βœ… Entity with Soft Delete Support
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 managed by AuditInterceptor)
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
    public string? DeletedBy { get; set; }
    
    // Domain methods
    public void MarkAsDeleted()
    {
        // Add domain event before deletion
        AddDomainEvent(new ProductDeletedEvent(Id, Name));
    }
}

// βœ… Entity without Soft Delete (Hard Delete Only)
public class Category : BaseAuditableEntity<int>
{
    public string Name { get; set; } = string.Empty;
    // No ISoftDelete interface = hard delete only
}

Soft Delete Operations

public class ProductService
{
    private readonly IUnitOfWork _unitOfWork;
    
    public ProductService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    // βœ… Soft Delete Operations
    public async Task SoftDeleteProductAsync(int productId)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var product = await repository.GetByIdAsync(productId);
        
        if (product != null)
        {
            product.MarkAsDeleted(); // Add domain event if needed
            
            // This will soft delete (set IsDeleted=true, DeletedAt=now, DeletedBy=user)
            await repository.DeleteAsync(product, saveChanges: true);
        }
    }
    
    // βœ… Hard Delete (Permanent Removal)
    public async Task HardDeleteProductAsync(int productId)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var product = await repository.GetByIdAsync(productId);
        
        if (product != null)
        {
            // This will permanently remove from database
            await repository.HardDeleteAsync(product, saveChanges: true);
        }
    }
    
    // βœ… Restore Soft Deleted Entity
    public async Task RestoreProductAsync(int productId)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        
        // This will restore the soft-deleted product
        // (set IsDeleted=false, DeletedAt=null, DeletedBy=null)
        await repository.RestoreAsync(productId, saveChanges: true);
    }
    
    // βœ… Check if Entity Supports Soft Delete
    public async Task<bool> CanRestoreProductAsync(int productId)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        
        try
        {
            await repository.RestoreAsync(productId);
            return true;
        }
        catch (InvalidOperationException)
        {
            // Entity doesn't implement ISoftDelete
            return false;
        }
    }
}

Querying Soft Deleted Entities

public class ProductQueryService
{
    private readonly IUnitOfWork _unitOfWork;
    
    public ProductQueryService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    // βœ… Normal Queries (Automatically Exclude Soft Deleted)
    public async Task<IReadOnlyList<Product>> GetActiveProductsAsync()
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        
        // Global query filter automatically excludes soft-deleted entities
        return await repository.GetAllAsync();
    }
    
    // βœ… Include Soft Deleted Entities
    public async Task<IReadOnlyList<Product>> GetAllProductsIncludingDeletedAsync()
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        
        return await repository.GetQueryable()
            .IncludeDeleted()  // Ignores global query filter
            .ToListAsync();
    }
    
    // βœ… Get Only Soft Deleted Entities
    public async Task<IReadOnlyList<Product>> GetDeletedProductsAsync()
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        
        return await repository.GetQueryable()
            .OnlyDeleted()  // WHERE IsDeleted = true
            .ToListAsync();
    }
    
    // βœ… Advanced Soft Delete Queries
    public async Task<IReadOnlyList<Product>> GetProductsDeletedByUserAsync(string userId)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        
        return await repository.GetQueryable()
            .IncludeDeleted()
            .Where(p => p.IsDeleted && p.DeletedBy == userId)
            .OrderByDescending(p => p.DeletedAt)
            .ToListAsync();
    }
    
    // βœ… Soft Delete Statistics
    public async Task<SoftDeleteStatsDto> GetSoftDeleteStatsAsync()
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        
        var totalCount = await repository.GetQueryable()
            .IncludeDeleted()
            .CountAsync();
            
        var activeCount = await repository.CountAsync();
        
        var deletedCount = await repository.GetQueryable()
            .OnlyDeleted()
            .CountAsync();
        
        return new SoftDeleteStatsDto
        {
            TotalCount = totalCount,
            ActiveCount = activeCount,
            DeletedCount = deletedCount
        };
    }
}

public class SoftDeleteStatsDto
{
    public int TotalCount { get; set; }
    public int ActiveCount { get; set; }
    public int DeletedCount { get; set; }
}

Advanced Soft Delete Scenarios

// βœ… Bulk Soft Delete
public async Task BulkSoftDeleteExpiredProductsAsync()
{
    var repository = _unitOfWork.GetRepository<Product, int>();
    var cutoffDate = DateTime.UtcNow.AddDays(-365);
    
    // This will soft delete all products older than 1 year
    await repository.BulkDeleteAsync(p => p.CreatedAt < cutoffDate, saveChanges: true);
}

// βœ… Restore with Validation
public async Task<bool> RestoreProductWithValidationAsync(int productId)
{
    var repository = _unitOfWork.GetRepository<Product, int>();
    
    // First, check if the product exists and is soft deleted
    var product = await repository.GetQueryable()
        .IncludeDeleted()
        .FirstOrDefaultAsync(p => p.Id == productId);
    
    if (product == null)
        return false; // Product doesn't exist
        
    if (!product.IsDeleted)
        return false; // Product is not deleted
    
    // Additional business validation
    if (await IsProductCategoryActiveAsync(product.CategoryId))
    {
        await repository.RestoreAsync(product, saveChanges: true);
        return true;
    }
    
    return false; // Cannot restore due to business rules
}

// βœ… Cascade Soft Delete
public async Task CascadeSoftDeleteCategoryAsync(int categoryId)
{
    var categoryRepository = _unitOfWork.GetRepository<Category, int>();
    var productRepository = _unitOfWork.GetRepository<Product, int>();
    
    // First, soft delete all products in the category
    await productRepository.BulkDeleteAsync(p => p.CategoryId == categoryId, saveChanges: false);
    
    // Then, soft delete the category itself (if it implements ISoftDelete)
    var category = await categoryRepository.GetByIdAsync(categoryId);
    if (category != null && category is ISoftDelete)
    {
        await categoryRepository.DeleteAsync(category, saveChanges: false);
    }
    
    // Save all changes in a single transaction
    await _unitOfWork.SaveChangesAsync();
}

πŸ” Dynamic Filtering

Powerful dynamic filtering system that allows building complex queries from filter models at runtime.

graph TB
    A[FilterModel] --> B[SearchTerm]
    A --> C[FilterItems]
    
    C --> D[Field Name]
    C --> E[Operator]
    C --> F[Value]
    
    E --> G[equals]
    E --> H[contains]
    E --> I[startswith]
    E --> J[endswith]
    E --> K[greaterthan]
    E --> L[lessthan]
    E --> M[greaterthanorequal]
    E --> N[lessthanorequal]
    E --> O[notequals]
    
    B --> P[Expression Builder]
    D --> P
    E --> P
    F --> P
    
    P --> Q[LINQ Expression]
    Q --> R[Database Query]

Filter Model Structure

// βœ… Filter Model Classes
public class FilterModel
{
    public string? SearchTerm { get; set; }
    public List<FilterItem> Filters { get; set; } = new();
}

public class FilterItem
{
    public string Field { get; set; } = string.Empty;
    public string Operator { get; set; } = "equals";
    public string Value { get; set; } = string.Empty;
}

// βœ… Supported Operators
public static class FilterOperators
{
    public const string Equals = "equals";
    public const string NotEquals = "notequals";
    public const string Contains = "contains";
    public const string StartsWith = "startswith";
    public const string EndsWith = "endswith";
    public const string GreaterThan = "greaterthan";
    public const string GreaterThanOrEqual = "greaterthanorequal";
    public const string LessThan = "lessthan";
    public const string LessThanOrEqual = "lessthanorequal";
}

Dynamic Filtering Usage

public class ProductFilterService
{
    private readonly IUnitOfWork _unitOfWork;
    
    public ProductFilterService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    // βœ… Simple Search Term Filtering
    public async Task<IPaginate<Product>> SearchProductsAsync(string searchTerm, int page, int size)
    {
        var filter = new FilterModel
        {
            SearchTerm = searchTerm // Searches across all string properties
        };
        
        var repository = _unitOfWork.GetRepository<Product, int>();
        return await repository.GetPagedWithFilterAsync(filter, page, size);
    }
    
    // βœ… Complex Multi-Field Filtering
    public async Task<IPaginate<Product>> FilterProductsAsync(ProductFilterRequest request)
    {
        var filter = new FilterModel
        {
            SearchTerm = request.SearchTerm,
            Filters = new List<FilterItem>()
        };
        
        // Price range filtering
        if (request.MinPrice.HasValue)
        {
            filter.Filters.Add(new FilterItem
            {
                Field = nameof(Product.Price),
                Operator = FilterOperators.GreaterThanOrEqual,
                Value = request.MinPrice.Value.ToString()
            });
        }
        
        if (request.MaxPrice.HasValue)
        {
            filter.Filters.Add(new FilterItem
            {
                Field = nameof(Product.Price),
                Operator = FilterOperators.LessThanOrEqual,
                Value = request.MaxPrice.Value.ToString()
            });
        }
        
        // Category filtering
        if (request.CategoryId.HasValue)
        {
            filter.Filters.Add(new FilterItem
            {
                Field = nameof(Product.CategoryId),
                Operator = FilterOperators.Equals,
                Value = request.CategoryId.Value.ToString()
            });
        }
        
        // Name filtering
        if (!string.IsNullOrEmpty(request.NameContains))
        {
            filter.Filters.Add(new FilterItem
            {
                Field = nameof(Product.Name),
                Operator = FilterOperators.Contains,
                Value = request.NameContains
            });
        }
        
        // Date range filtering
        if (request.CreatedAfter.HasValue)
        {
            filter.Filters.Add(new FilterItem
            {
                Field = nameof(Product.CreatedAt),
                Operator = FilterOperators.GreaterThanOrEqual,
                Value = request.CreatedAfter.Value.ToString("yyyy-MM-dd")
            });
        }
        
        var repository = _unitOfWork.GetRepository<Product, int>();
        
        return await repository.GetPagedWithFilterAsync(
            filter,
            request.Page,
            request.PageSize,
            orderBy: query => query.OrderByDescending(p => p.CreatedAt),
            includes: new List<Expression<Func<Product, object>>> { p => p.Category }
        );
    }
    
    // βœ… API Controller Integration
    [HttpPost("filter")]
    public async Task<ActionResult<IPaginate<Product>>> FilterProducts([FromBody] ProductFilterRequest request)
    {
        var result = await FilterProductsAsync(request);
        return Ok(result);
    }
}

// βœ… Filter Request DTO
public class ProductFilterRequest
{
    public string? SearchTerm { get; set; }
    public decimal? MinPrice { get; set; }
    public decimal? MaxPrice { get; set; }
    public int? CategoryId { get; set; }
    public string? NameContains { get; set; }
    public DateTime? CreatedAfter { get; set; }
    public DateTime? CreatedBefore { get; set; }
    public int Page { get; set; } = 1;
    public int PageSize { get; set; } = 10;
}

Advanced Dynamic Filtering

// βœ… Generic Filter Builder
public class FilterBuilder<T>
{
    private readonly FilterModel _filter = new();
    
    public FilterBuilder<T> WithSearchTerm(string searchTerm)
    {
        _filter.SearchTerm = searchTerm;
        return this;
    }
    
    public FilterBuilder<T> Where<TProperty>(Expression<Func<T, TProperty>> property, string op, object value)
    {
        var propertyName = GetPropertyName(property);
        _filter.Filters.Add(new FilterItem
        {
            Field = propertyName,
            Operator = op,
            Value = value?.ToString() ?? string.Empty
        });
        return this;
    }
    
    public FilterBuilder<T> Equal<TProperty>(Expression<Func<T, TProperty>> property, TProperty value)
        => Where(property, FilterOperators.Equals, value);
    
    public FilterBuilder<T> Contains(Expression<Func<T, string>> property, string value)
        => Where(property, FilterOperators.Contains, value);
    
    public FilterBuilder<T> GreaterThan<TProperty>(Expression<Func<T, TProperty>> property, TProperty value)
        => Where(property, FilterOperators.GreaterThan, value);
    
    public FilterBuilder<T> LessThan<TProperty>(Expression<Func<T, TProperty>> property, TProperty value)
        => Where(property, FilterOperators.LessThan, value);
    
    public FilterBuilder<T> Between<TProperty>(Expression<Func<T, TProperty>> property, TProperty min, TProperty max)
    {
        GreaterThanOrEqual(property, min);
        LessThanOrEqual(property, max);
        return this;
    }
    
    public FilterBuilder<T> GreaterThanOrEqual<TProperty>(Expression<Func<T, TProperty>> property, TProperty value)
        => Where(property, FilterOperators.GreaterThanOrEqual, value);
    
    public FilterBuilder<T> LessThanOrEqual<TProperty>(Expression<Func<T, TProperty>> property, TProperty value)
        => Where(property, FilterOperators.LessThanOrEqual, value);
    
    public FilterModel Build() => _filter;
    
    private static string GetPropertyName<TProperty>(Expression<Func<T, TProperty>> property)
    {
        if (property.Body is MemberExpression member)
            return member.Member.Name;
        
        throw new ArgumentException("Expression must be a property access", nameof(property));
    }
}

// βœ… Usage with FilterBuilder
public async Task<IPaginate<Product>> GetExpensiveRecentProductsAsync(decimal minPrice, DateTime fromDate, int page, int size)
{
    var filter = new FilterBuilder<Product>()
        .GreaterThanOrEqual(p => p.Price, minPrice)
        .GreaterThanOrEqual(p => p.CreatedAt, fromDate)
        .Contains(p => p.Name, "premium")
        .Build();
    
    var repository = _unitOfWork.GetRepository<Product, int>();
    return await repository.GetPagedWithFilterAsync(filter, page, size);
}

// βœ… Custom Filter Extensions
public static class FilterExtensions
{
    public static FilterBuilder<Product> ForActiveProducts(this FilterBuilder<Product> builder)
    {
        return builder.Equal(p => p.IsDeleted, false);
    }
    
    public static FilterBuilder<Product> InPriceRange(this FilterBuilder<Product> builder, decimal min, decimal max)
    {
        return builder.Between(p => p.Price, min, max);
    }
    
    public static FilterBuilder<Product> InCategory(this FilterBuilder<Product> builder, int categoryId)
    {
        return builder.Equal(p => p.CategoryId, categoryId);
    }
}

// βœ… Usage with Extensions
public async Task<IPaginate<Product>> GetCategoryProductsInPriceRangeAsync(int categoryId, decimal minPrice, decimal maxPrice, int page, int size)
{
    var filter = new FilterBuilder<Product>()
        .ForActiveProducts()
        .InCategory(categoryId)
        .InPriceRange(minPrice, maxPrice)
        .Build();
    
    var repository = _unitOfWork.GetRepository<Product, int>();
    return await repository.GetPagedWithFilterAsync(filter, page, size);
}

πŸ“„ Pagination

Comprehensive pagination support with metadata and flexible query integration.

graph TB
    A[Pagination Request] --> B[PageIndex]
    A --> C[PageSize]
    A --> D[Query Options]
    
    D --> E[Predicate Filter]
    D --> F[OrderBy Function]
    D --> G[Include Expressions]
    
    B --> H[IPaginate Result]
    C --> H
    E --> H
    F --> H
    G --> H
    
    H --> I[Items Collection]
    H --> J[Page Metadata]
    
    J --> K[Count - Total Items]
    J --> L[Pages - Total Pages]
    J --> M[Index - Current Page]
    J --> N[Size - Items Per Page]
    J --> O[HasNext - Has Next Page]
    J --> P[HasPrevious - Has Previous Page]

Basic Pagination

public class ProductPaginationService
{
    private readonly IUnitOfWork _unitOfWork;
    
    public ProductPaginationService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    // βœ… Simple Pagination
    public async Task<IPaginate<Product>> GetProductsPagedAsync(int page, int size)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        
        return await repository.GetPagedAsync(page, size);
    }
    
    // βœ… Pagination with Filtering
    public async Task<IPaginate<Product>> GetActiveProductsPagedAsync(int page, int size)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        
        return await repository.GetPagedAsync(
            pageIndex: page,
            pageSize: size,
            predicate: p => !p.IsDeleted,
            orderBy: query => query.OrderBy(p => p.Name)
        );
    }
    
    // βœ… Pagination with Includes
    public async Task<IPaginate<Product>> GetProductsWithCategoryPagedAsync(int page, int size)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        
        return await repository.GetPagedAsync(
            pageIndex: page,
            pageSize: size,
            predicate: p => p.Price > 100,
            orderBy: query => query.OrderByDescending(p => p.CreatedAt),
            includes: new List<Expression<Func<Product, object>>> { p => p.Category }
        );
    }
    
    // βœ… Advanced Pagination with Dynamic Filtering
    public async Task<IPaginate<Product>> GetFilteredProductsPagedAsync(ProductFilterRequest request)
    {
        var filter = BuildFilterFromRequest(request);
        var repository = _unitOfWork.GetRepository<Product, int>();
        
        return await repository.GetPagedWithFilterAsync(
            filter: filter,
            pageIndex: request.Page,
            pageSize: request.PageSize,
            orderBy: GetOrderByExpression(request.SortBy, request.SortDirection),
            includes: new List<Expression<Func<Product, object>>> { p => p.Category }
        );
    }
}

Pagination Response Models

// βœ… IPaginate Interface (Built-in)
public interface IPaginate<T>
{
    int From { get; }          // Starting index (typically 0)
    int Index { get; }         // Current page index
    int Size { get; }          // Page size
    int Count { get; }         // Total number of items
    int Pages { get; }         // Total number of pages
    IList<T> Items { get; }    // Items in current page
    bool HasPrevious { get; }  // Has previous page
    bool HasNext { get; }      // Has next page
}

// βœ… Custom Pagination Response DTO
public class PaginatedResponse<T>
{
    public IList<T> Data { get; set; } = new List<T>();
    public PaginationMetadata Metadata { get; set; } = new();
    
    public static PaginatedResponse<T> FromIPaginate(IPaginate<T> paginate)
    {
        return new PaginatedResponse<T>
        {
            Data = paginate.Items,
            Metadata = new PaginationMetadata
            {
                CurrentPage = paginate.Index,
                PageSize = paginate.Size,
                TotalCount = paginate.Count,
                TotalPages = paginate.Pages,
                HasNextPage = paginate.HasNext,
                HasPreviousPage = paginate.HasPrevious
            }
        };
    }
}

public class PaginationMetadata
{
    public int CurrentPage { get; set; }
    public int PageSize { get; set; }
    public int TotalCount { get; set; }
    public int TotalPages { get; set; }
    public bool HasNextPage { get; set; }
    public bool HasPreviousPage { get; set; }
    public int? NextPage => HasNextPage ? CurrentPage + 1 : null;
    public int? PreviousPage => HasPreviousPage ? CurrentPage - 1 : null;
}

API Controller Integration

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly ProductPaginationService _paginationService;
    
    public ProductsController(ProductPaginationService paginationService)
    {
        _paginationService = paginationService;
    }
    
    // βœ… Simple Pagination Endpoint
    [HttpGet]
    public async Task<ActionResult<PaginatedResponse<ProductDto>>> GetProducts(
        [FromQuery] int page = 1,
        [FromQuery] int size = 10)
    {
        var result = await _paginationService.GetProductsPagedAsync(page, size);
        var response = PaginatedResponse<ProductDto>.FromIPaginate(
            result.MapTo<ProductDto>() // Assuming you have mapping
        );
        
        return Ok(response);
    }
    
    // βœ… Advanced Pagination with Filtering
    [HttpPost("search")]
    public async Task<ActionResult<PaginatedResponse<ProductDto>>> SearchProducts(
        [FromBody] ProductFilterRequest request)
    {
        var result = await _paginationService.GetFilteredProductsPagedAsync(request);
        var response = PaginatedResponse<ProductDto>.FromIPaginate(
            result.MapTo<ProductDto>()
        );
        
        return Ok(response);
    }
    
    // βœ… Pagination with Response Headers
    [HttpGet("with-headers")]
    public async Task<ActionResult<IList<ProductDto>>> GetProductsWithHeaders(
        [FromQuery] int page = 1,
        [FromQuery] int size = 10)
    {
        var result = await _paginationService.GetProductsPagedAsync(page, size);
        
        // Add pagination metadata to response headers
        Response.Headers.Add("X-Pagination-Current-Page", result.Index.ToString());
        Response.Headers.Add("X-Pagination-Total-Pages", result.Pages.ToString());
        Response.Headers.Add("X-Pagination-Total-Count", result.Count.ToString());
        Response.Headers.Add("X-Pagination-Page-Size", result.Size.ToString());
        Response.Headers.Add("X-Pagination-Has-Next", result.HasNext.ToString());
        Response.Headers.Add("X-Pagination-Has-Previous", result.HasPrevious.ToString());
        
        return Ok(result.Items.MapTo<ProductDto>());
    }
}

Advanced Pagination Scenarios

// βœ… Cursor-based Pagination (for large datasets)
public class CursorPaginationService
{
    private readonly IUnitOfWork _unitOfWork;
    
    public CursorPaginationService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    public async Task<CursorPaginatedResult<Product>> GetProductsCursorPagedAsync(
        int? lastId = null,
        int size = 10)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var query = repository.GetQueryable();
        
        if (lastId.HasValue)
        {
            query = query.Where(p => p.Id > lastId.Value);
        }
        
        var items = await query
            .OrderBy(p => p.Id)
            .Take(size + 1) // Take one extra to check if there are more
            .ToListAsync();
        
        var hasNext = items.Count > size;
        if (hasNext)
        {
            items = items.Take(size).ToList();
        }
        
        return new CursorPaginatedResult<Product>
        {
            Items = items,
            HasNext = hasNext,
            NextCursor = hasNext ? items.LastOrDefault()?.Id : null
        };
    }
}

public class CursorPaginatedResult<T>
{
    public IList<T> Items { get; set; } = new List<T>();
    public bool HasNext { get; set; }
    public int? NextCursor { get; set; }
}

// βœ… Custom Pagination with Sorting
public class SortablePaginationService
{
    private readonly IUnitOfWork _unitOfWork;
    
    public SortablePaginationService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    public async Task<IPaginate<Product>> GetProductsSortedPagedAsync(
        int page,
        int size,
        string sortBy = "name",
        string sortDirection = "asc")
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        
        var orderBy = GetOrderByExpression(sortBy, sortDirection);
        
        return await repository.GetPagedAsync(
            pageIndex: page,
            pageSize: size,
            orderBy: orderBy
        );
    }
    
    private static Func<IQueryable<Product>, IOrderedQueryable<Product>> GetOrderByExpression(
        string sortBy, 
        string sortDirection)
    {
        return sortBy.ToLower() switch
        {
            "name" => sortDirection.ToLower() == "desc" 
                ? query => query.OrderByDescending(p => p.Name)
                : query => query.OrderBy(p => p.Name),
            "price" => sortDirection.ToLower() == "desc"
                ? query => query.OrderByDescending(p => p.Price)
                : query => query.OrderBy(p => p.Price),
            "createdat" => sortDirection.ToLower() == "desc"
                ? query => query.OrderByDescending(p => p.CreatedAt)
                : query => query.OrderBy(p => p.CreatedAt),
            _ => query => query.OrderBy(p => p.Id)
        };
    }
}

πŸ”‘ ID Generation

Modular ID generation system supporting pluggable strategies including GUID V7, ULID, and custom generators.

graph TB
    A[Entity Creation] --> B[IdGenerationInterceptor]
    B --> C{Has Default ID?}
    C -->|Yes| D[IIdGeneratorFactory]
    C -->|No| E[Skip Generation]
    
    D --> F{Generator Registered?}
    F -->|Yes| G[Generate New ID]
    F -->|No| H[Use Default Value]
    
    G --> I[Set Entity ID]
    I --> J[Continue Save Operation]
    
    K[Extension Packages] --> L[FS.EntityFramework.Library.GuidV7]
    K --> M[FS.EntityFramework.Library.Ulid]
    K --> N[Custom Generators]
    
    L --> O[IIdGenerator&lt;Guid&gt;]
    M --> P[IIdGenerator&lt;Ulid&gt;]
    N --> Q[IIdGenerator&lt;TKey&gt;]

Core ID Generation Setup

// βœ… Basic ID Generation Setup
services.AddFSEntityFramework<YourDbContext>()
    .WithIdGeneration()
    .Complete()
    .Build();

// βœ… Custom ID Generator Registration
services.AddFSEntityFramework<YourDbContext>()
    .WithIdGeneration()
        .WithGenerator<Guid, MyCustomGuidGenerator>()
        .WithGenerator<string, MyStringIdGenerator>()
    .Complete()
    .Build();

GUID Version 7 Integration

// βœ… Install Extension Package
// dotnet add package FS.EntityFramework.Library.GuidV7

// βœ… Basic GUID V7 Setup (.NET 9+)
services.AddFSEntityFramework<YourDbContext>()
    .WithGuidV7()  // Automatic GUID V7 generation
    .Build();

// βœ… GUID V7 with Custom Timestamp
services.AddFSEntityFramework<YourDbContext>()
    .WithGuidV7(() => DateTimeOffset.UtcNow.AddHours(1)) // Custom timestamp
    .Build();

// βœ… GUID V7 with Service-based Timestamp
services.AddFSEntityFramework<YourDbContext>()
    .WithGuidV7(provider => 
    {
        var timeService = provider.GetService<ITimeService>();
        return timeService?.GetCurrentTime() ?? DateTimeOffset.UtcNow;
    })
    .Build();

// βœ… Entity with GUID V7
public class User : BaseAuditableEntity<Guid>
{
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    
    // ID will be automatically generated as GUID V7
}

// βœ… Database Configuration for GUID V7
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    
    // Configure GUID V7 properties for optimal performance
    modelBuilder.Entity<User>()
        .ConfigureAsGuidV7(u => u.Id);
}

ULID Integration

// βœ… Install Extension Package
// dotnet add package FS.EntityFramework.Library.Ulid

// βœ… Basic ULID Setup
services.AddFSEntityFramework<YourDbContext>()
    .WithUlid()  // Automatic ULID generation
    .Build();

// βœ… ULID with Custom Timestamp
services.AddFSEntityFramework<YourDbContext>()
    .WithUlid(() => DateTimeOffset.UtcNow)
    .Build();

// βœ… Entity with ULID
public class Order : BaseAuditableEntity<Ulid>, ISoftDelete
{
    public string OrderNumber { get; set; } = string.Empty;
    public decimal TotalAmount { get; set; }
    
    // ISoftDelete properties
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
    public string? DeletedBy { get; set; }
    
    // ID will be automatically generated as ULID
}

// βœ… Database Configuration for ULID
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    
    // Configure ULID properties
    modelBuilder.Entity<Order>()
        .ConfigureAsUlid(o => o.Id);
    
    // Or apply to all ULID entities
    modelBuilder.ConfigureUlidEntities();
}

Custom ID Generators

// βœ… Custom String ID Generator
public class CustomStringIdGenerator : IIdGenerator<string>
{
    public Type KeyType => typeof(string);
    
    public string Generate()
    {
        // Custom logic for string ID generation
        return $"CUST_{DateTime.UtcNow:yyyyMMdd}_{Guid.NewGuid():N}";
    }
    
    object IIdGenerator.Generate()
    {
        return Generate();
    }
}

// βœ… Custom Integer ID Generator (for specific scenarios)
public class CustomIntegerIdGenerator : IIdGenerator<int>
{
    private readonly ISequenceService _sequenceService;
    
    public CustomIntegerIdGenerator(ISequenceService sequenceService)
    {
        _sequenceService = sequenceService;
    }
    
    public Type KeyType => typeof(int);
    
    public int Generate()
    {
        return _sequenceService.GetNextValue();
    }
    
    object IIdGenerator.Generate()
    {
        return Generate();
    }
}

// βœ… Time-based ID Generator
public class TimestampIdGenerator : IIdGenerator<long>
{
    public Type KeyType => typeof(long);
    
    public long Generate()
    {
        // Generate timestamp-based ID
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        var random = Random.Shared.Next(1000, 9999);
        return timestamp * 10000 + random;
    }
    
    object IIdGenerator.Generate()
    {
        return Generate();
    }
}

// βœ… Register Custom Generators
services.AddFSEntityFramework<YourDbContext>()
    .WithIdGeneration()
        .WithGenerator<string, CustomStringIdGenerator>()
        .WithGenerator<int, CustomIntegerIdGenerator>()
        .WithGenerator<long, TimestampIdGenerator>()
    .Complete()
    .Build();

Advanced ID Generation Scenarios

// βœ… Mixed ID Strategies in Single Application
public class Customer : BaseAuditableEntity<Guid>    // GUID V7
{
    public string Name { get; set; } = string.Empty;
}

public class Order : BaseAuditableEntity<Ulid>       // ULID
{
    public Guid CustomerId { get; set; }
    public decimal Amount { get; set; }
}

public class Product : BaseAuditableEntity<int>      // Auto-increment (database)
{
    public string Name { get; set; } = string.Empty;
}

public class LogEntry : BaseAuditableEntity<string>  // Custom string
{
    public string Message { get; set; } = string.Empty;
}

// Configuration for mixed strategies
services.AddFSEntityFramework<YourDbContext>()
    .WithGuidV7()                                    // For Guid entities
    .WithUlid()                                      // For Ulid entities
    .WithIdGeneration()
        .WithGenerator<string, CustomStringIdGenerator>() // For string entities
    .Complete()
    // int entities use database auto-increment (no generator needed)
    .Build();

// βœ… Conditional ID Generation
public class ConditionalIdGenerator : IIdGenerator<Guid>
{
    private readonly IEnvironmentService _environmentService;
    
    public ConditionalIdGenerator(IEnvironmentService environmentService)
    {
        _environmentService = environmentService;
    }
    
    public Type KeyType => typeof(Guid);
    
    public Guid Generate()
    {
        // Use different generation strategy based on environment
        return _environmentService.IsDevelopment() 
            ? Guid.NewGuid()           // Regular GUID in development
            : Guid.CreateVersion7();   // GUID V7 in production
    }
    
    object IIdGenerator.Generate()
    {
        return Generate();
    }
}

// βœ… Factory-based ID Generation
services.AddFSEntityFramework<YourDbContext>()
    .WithIdGeneration()
        .WithGenerator<Guid>(provider =>
        {
            var config = provider.GetService<IConfiguration>();
            var useGuidV7 = config.GetValue<bool>("UseGuidV7");
            
            return useGuidV7 
                ? new GuidV7Generator() 
                : new StandardGuidGenerator();
        })
    .Complete()
    .Build();

// βœ… Validation and Error Handling
public class ValidatingIdGenerator<TKey> : IIdGenerator<TKey> where TKey : IEquatable<TKey>
{
    private readonly IIdGenerator<TKey> _innerGenerator;
    private readonly ILogger<ValidatingIdGenerator<TKey>> _logger;
    
    public ValidatingIdGenerator(IIdGenerator<TKey> innerGenerator, ILogger<ValidatingIdGenerator<TKey>> logger)
    {
        _innerGenerator = innerGenerator;
        _logger = logger;
    }
    
    public Type KeyType => typeof(TKey);
    
    public TKey Generate()
    {
        try
        {
            var id = _innerGenerator.Generate();
            
            // Validation logic
            if (id == null || id.Equals(default(TKey)))
            {
                _logger.LogWarning("Generated ID is null or default value");
                throw new InvalidOperationException("Failed to generate valid ID");
            }
            
            return id;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error generating ID of type {KeyType}", typeof(TKey).Name);
            throw;
        }
    }
    
    object IIdGenerator.Generate()
    {
        return Generate();
    }
}

ID Generation Performance Considerations

// βœ… Optimized ID Generation for High Throughput
public class HighPerformanceUlidGenerator : IIdGenerator<Ulid>
{
    private static readonly ThreadLocal<Random> ThreadLocalRandom = new(() => new Random());
    
    public Type KeyType => typeof(Ulid);
    
    public Ulid Generate()
    {
        // Use thread-local random for better performance in high-concurrency scenarios
        return Ulid.NewUlid(DateTimeOffset.UtcNow, ThreadLocalRandom.Value);
    }
    
    object IIdGenerator.Generate()
    {
        return Generate();
    }
}

// βœ… Cached ID Generation
public class CachedIdGenerator : IIdGenerator<string>
{
    private readonly IMemoryCache _cache;
    private readonly IIdGenerator<string> _innerGenerator;
    
    public CachedIdGenerator(IMemoryCache cache, IIdGenerator<string> innerGenerator)
    {
        _cache = cache;
        _innerGenerator = innerGenerator;
    }
    
    public Type KeyType => typeof(string);
    
    public string Generate()
    {
        // For scenarios where ID generation is expensive and IDs can be pre-generated
        var cacheKey = $"pregenerated_ids_{Thread.CurrentThread.ManagedThreadId}";
        
        if (_cache.TryGetValue(cacheKey, out Queue<string> cachedIds) && cachedIds.Count > 0)
        {
            return cachedIds.Dequeue();
        }
        
        return _innerGenerator.Generate();
    }
    
    object IIdGenerator.Generate()
    {
        return Generate();
    }
}

πŸ“š Advanced Usage

Custom DbContext Integration

// βœ… Using FSDbContext (Automatic Configuration)
public class ApplicationDbContext : FSDbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IServiceProvider serviceProvider) 
        : base(options, serviceProvider)
    {
    }
    
    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }
    public DbSet<Order> Orders { get; set; }
    
    // FS.EntityFramework configurations are automatically applied
}

// βœ… Manual Integration with Existing DbContext
public class ExistingDbContext : DbContext
{
    private readonly IServiceProvider _serviceProvider;
    
    public ExistingDbContext(DbContextOptions options, IServiceProvider serviceProvider) 
        : base(options)
    {
        _serviceProvider = serviceProvider;
    }
    
    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        
        // Apply FS.EntityFramework configurations
        modelBuilder.ApplyFSEntityFrameworkConfigurations(_serviceProvider);
        
        // Configure ULID entities
        modelBuilder.ConfigureUlidEntities();
        
        // Configure GUID V7 entities
        modelBuilder.Entity<User>().ConfigureAsGuidV7(u => u.Id);
    }
    
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
        
        // Apply logging configuration
        optionsBuilder.ApplyFSEntityFrameworkLogging(_serviceProvider);
    }
}

Custom Repository Implementation

// βœ… Custom Repository Interface
public interface IProductRepository : IRepository<Product, int>
{
    Task<IReadOnlyList<Product>> GetTopSellingProductsAsync(int count);
    Task<IReadOnlyList<Product>> GetProductsByTagAsync(string tag);
    Task<decimal> GetAveragePriceInCategoryAsync(int categoryId);
}

// βœ… Custom Repository Implementation
public class ProductRepository : BaseRepository<Product, int>, IProductRepository
{
    public ProductRepository(DbContext context) : base(context)
    {
    }
    
    public async Task<IReadOnlyList<Product>> GetTopSellingProductsAsync(int count)
    {
        return await DbSet
            .OrderByDescending(p => p.SalesCount)
            .Take(count)
            .ToListAsync();
    }
    
    public async Task<IReadOnlyList<Product>> GetProductsByTagAsync(string tag)
    {
        return await DbSet
            .Where(p => p.Tags.Contains(tag))
            .Include(p => p.Category)
            .ToListAsync();
    }
    
    public async Task<decimal> GetAveragePriceInCategoryAsync(int categoryId)
    {
        return await DbSet
            .Where(p => p.CategoryId == categoryId && !p.IsDeleted)
            .AverageAsync(p => p.Price);
    }
}

// βœ… Register Custom Repository
services.AddFSEntityFramework<ApplicationDbContext>()
    .WithCustomRepository<Product, int, ProductRepository>()
    .Build();

// βœ… Use Custom Repository
public class ProductService
{
    private readonly IUnitOfWork _unitOfWork;
    
    public ProductService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    public async Task<IReadOnlyList<Product>> GetTopSellingProductsAsync(int count)
    {
        // GetRepository returns the custom ProductRepository
        var repository = _unitOfWork.GetRepository<IProductRepository>();
        return await repository.GetTopSellingProductsAsync(count);
    }
}

Advanced Transaction Scenarios

public class ComplexBusinessService
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly IExternalApiService _externalApiService;
    private readonly IEmailService _emailService;
    
    public ComplexBusinessService(
        IUnitOfWork unitOfWork,
        IExternalApiService externalApiService,
        IEmailService emailService)
    {
        _unitOfWork = unitOfWork;
        _externalApiService = externalApiService;
        _emailService = emailService;
    }
    
    // βœ… Complex Transaction with External Services
    public async Task<Order> ProcessOrderWithPaymentAsync(CreateOrderRequest request)
    {
        return await _unitOfWork.ExecuteInTransactionAsync(async () =>
        {
            // 1. Create order
            var orderRepository = _unitOfWork.GetRepository<Order, int>();
            var order = Order.Create(request.CustomerId, request.Items);
            await orderRepository.AddAsync(order);
            
            // 2. Update inventory
            var productRepository = _unitOfWork.GetRepository<Product, int>();
            foreach (var item in request.Items)
            {
                var product = await productRepository.GetByIdAsync(item.ProductId);
                product?.UpdateStock(item.Quantity);
            }
            
            // 3. Save database changes first
            await _unitOfWork.SaveChangesAsync();
            
            // 4. Process payment (external service)
            var paymentResult = await _externalApiService.ProcessPaymentAsync(order.TotalAmount, request.PaymentInfo);
            if (!paymentResult.Success)
            {
                throw new PaymentException("Payment processing failed");
            }
            
            // 5. Update order with payment info
            order.UpdatePaymentInfo(paymentResult.TransactionId);
            await _unitOfWork.SaveChangesAsync();
            
            // 6. Send confirmation email (non-transactional)
            _ = Task.Run(async () =>
            {
                try
                {
                    await _emailService.SendOrderConfirmationAsync(order);
                }
                catch (Exception ex)
                {
                    // Log email failure but don't affect transaction
                    // Consider using background job for better reliability
                }
            });
            
            return order;
        });
    }
    
    // βœ… Nested Transactions
    public async Task ProcessBulkOrdersAsync(List<CreateOrderRequest> requests)
    {
        await _unitOfWork.ExecuteInTransactionAsync(async () =>
        {
            foreach (var request in requests)
            {
                // Each order processing might have its own error handling
                try
                {
                    await ProcessSingleOrderAsync(request);
                }
                catch (Exception ex)
                {
                    // Log error but continue with other orders
                    // Or implement compensation logic
                }
            }
            
            return true;
        });
    }
    
    // βœ… Distributed Transaction with Saga Pattern
    public async Task ProcessDistributedOrderAsync(CreateOrderRequest request)
    {
        var saga = new OrderProcessingSaga();
        
        try
        {
            await _unitOfWork.ExecuteInTransactionAsync(async () =>
            {
                // Step 1: Reserve inventory
                await saga.ReserveInventoryAsync(request.Items);
                
                // Step 2: Create order
                var order = await saga.CreateOrderAsync(request);
                
                // Step 3: Process payment
                await saga.ProcessPaymentAsync(order, request.PaymentInfo);
                
                // Step 4: Confirm inventory
                await saga.ConfirmInventoryAsync(request.Items);
                
                return order;
            });
        }
        catch (Exception)
        {
            // Compensate for any completed steps
            await saga.CompensateAsync();
            throw;
        }
    }
}

Advanced Specification Patterns

// βœ… Composite Specification
public abstract class CompositeSpecification<T> : BaseSpecification<T>
{
    protected void And(BaseSpecification<T> specification)
    {
        if (Criteria == null)
        {
            AddCriteria(specification.Criteria);
        }
        else if (specification.Criteria != null)
        {
            var parameter = Expression.Parameter(typeof(T));
            var left = Expression.Invoke(Criteria, parameter);
            var right = Expression.Invoke(specification.Criteria, parameter);
            var combined = Expression.AndAlso(left, right);
            AddCriteria(Expression.Lambda<Func<T, bool>>(combined, parameter));
        }
    }
    
    protected void Or(BaseSpecification<T> specification)
    {
        if (Criteria == null)
        {
            AddCriteria(specification.Criteria);
        }
        else if (specification.Criteria != null)
        {
            var parameter = Expression.Parameter(typeof(T));
            var left = Expression.Invoke(Criteria, parameter);
            var right = Expression.Invoke(specification.Criteria, parameter);
            var combined = Expression.OrElse(left, right);
            AddCriteria(Expression.Lambda<Func<T, bool>>(combined, parameter));
        }
    }
}

// βœ… Business Rule Specifications
public class PremiumProductSpecification : CompositeSpecification<Product>
{
    public PremiumProductSpecification()
    {
        var expensiveSpec = new ExpensiveProductSpecification(1000);
        var highRatedSpec = new HighRatedProductSpecification(4.5m);
        
        And(expensiveSpec);
        And(highRatedSpec);
        
        AddInclude(p => p.Category);
        AddInclude(p => p.Reviews);
        ApplyOrderByDescending(p => p.Rating);
    }
}

// βœ… Dynamic Specification Builder
public class SpecificationBuilder<T>
{
    private readonly List<BaseSpecification<T>> _specifications = new();
    
    public SpecificationBuilder<T> Add(BaseSpecification<T> specification)
    {
        _specifications.Add(specification);
        return this;
    }
    
    public SpecificationBuilder<T> AddIf(bool condition, BaseSpecification<T> specification)
    {
        if (condition)
        {
            _specifications.Add(specification);
        }
        return this;
    }
    
    public BaseSpecification<T> Build()
    {
        if (_specifications.Count == 0)
            return new EmptySpecification<T>();
        
        var result = _specifications.First();
        
        foreach (var spec in _specifications.Skip(1))
        {
            // Combine specifications using AND logic
            result = new CombinedSpecification<T>(result, spec);
        }
        
        return result;
    }
}

// βœ… Cached Specification Results
public class CachedSpecificationRepository<TEntity, TKey> : BaseRepository<TEntity, TKey>
    where TEntity : BaseEntity<TKey>
    where TKey : IEquatable<TKey>
{
    private readonly IMemoryCache _cache;
    private readonly TimeSpan _cacheExpiry = TimeSpan.FromMinutes(5);
    
    public CachedSpecificationRepository(DbContext context, IMemoryCache cache) 
        : base(context)
    {
        _cache = cache;
    }
    
    public override async Task<IReadOnlyList<TEntity>> GetAsync(BaseSpecification<TEntity> spec, CancellationToken cancellationToken = default)
    {
        var specKey = GenerateSpecificationKey(spec);
        var cacheKey = $"{typeof(TEntity).Name}_{specKey}";
        
        if (_cache.TryGetValue(cacheKey, out IReadOnlyList<TEntity> cachedResult))
        {
            return cachedResult;
        }
        
        var result = await base.GetAsync(spec, cancellationToken);
        
        _cache.Set(cacheKey, result, _cacheExpiry);
        
        return result;
    }
    
    private static string GenerateSpecificationKey(BaseSpecification<TEntity> spec)
    {
        // Generate a unique key based on specification criteria
        // This is a simplified example - you might want more sophisticated caching
        return spec.GetHashCode().ToString();
    }
}

Performance Optimization

// βœ… Optimized Repository with Bulk Operations
public class HighPerformanceProductRepository : BaseRepository<Product, int>
{
    public HighPerformanceProductRepository(DbContext context) : base(context)
    {
    }
    
    // βœ… Bulk Update with Raw SQL for Performance
    public async Task BulkUpdatePricesAsync(Dictionary<int, decimal> priceUpdates)
    {
        const string sql = @"
            UPDATE Products 
            SET Price = @price, UpdatedAt = @updatedAt, UpdatedBy = @updatedBy
            WHERE Id = @id";
        
        var currentTime = DateTime.UtcNow;
        var currentUser = GetCurrentUser(); // Implement based on your user context
        
        foreach (var update in priceUpdates)
        {
            await Context.Database.ExecuteSqlRawAsync(sql,
                new SqlParameter("@id", update.Key),
                new SqlParameter("@price", update.Value),
                new SqlParameter("@updatedAt", currentTime),
                new SqlParameter("@updatedBy", currentUser));
        }
    }
    
    // βœ… Efficient Paging with Window Functions
    public async Task<IPaginate<Product>> GetPagedWithWindowFunctionAsync(int pageIndex, int pageSize)
    {
        const string sql = @"
            WITH PagedProducts AS (
                SELECT *, 
                       ROW_NUMBER() OVER (ORDER BY CreatedAt DESC) as RowNum,
                       COUNT(*) OVER() as TotalCount
                FROM Products 
                WHERE IsDeleted = 0
            )
            SELECT * FROM PagedProducts 
            WHERE RowNum BETWEEN @offset AND @limit";
        
        var offset = (pageIndex - 1) * pageSize + 1;
        var limit = pageIndex * pageSize;
        
        var products = await Context.Set<Product>()
            .FromSqlRaw(sql, 
                new SqlParameter("@offset", offset),
                new SqlParameter("@limit", limit))
            .ToListAsync();
        
        var totalCount = products.FirstOrDefault()?.TotalCount ?? 0;
        
        return new Paginate<Product>
        {
            Index = pageIndex,
            Size = pageSize,
            Count = totalCount,
            Items = products,
            Pages = (int)Math.Ceiling(totalCount / (double)pageSize)
        };
    }
    
    // βœ… Read-Only Queries with NoTracking
    public async Task<IReadOnlyList<ProductSummaryDto>> GetProductSummariesAsync()
    {
        return await DbSet
            .AsNoTracking()
            .Select(p => new ProductSummaryDto
            {
                Id = p.Id,
                Name = p.Name,
                Price = p.Price,
                CategoryName = p.Category.Name
            })
            .ToListAsync();
    }
}

// βœ… Query Optimization Extensions
public static class QueryOptimizationExtensions
{
    public static IQueryable<T> WithHint<T>(this IQueryable<T> query, string hint) where T : class
    {
        // Add SQL Server query hints for performance optimization
        return query.TagWith($"OPTION ({hint})");
    }
    
    public static IQueryable<T> ForceIndex<T>(this IQueryable<T> query, string indexName) where T : class
    {
        return query.TagWith($"WITH (INDEX({indexName}))");
    }
    
    public static IQueryable<T> WithReadUncommitted<T>(this IQueryable<T> query) where T : class
    {
        return query.TagWith("WITH (NOLOCK)");
    }
}

// βœ… Usage of Optimization Extensions
public async Task<IReadOnlyList<Product>> GetProductsOptimizedAsync()
{
    var repository = _unitOfWork.GetRepository<Product, int>();
    
    return await repository.GetQueryable()
        .WithHint("RECOMPILE")
        .ForceIndex("IX_Product_CategoryId_Price")
        .Where(p => p.CategoryId == 1 && p.Price > 100)
        .ToListAsync();
}

🎯 Best Practices

Entity Design Best Practices

// βœ… Well-Designed Entity
public class Product : BaseAuditableEntity<int>, ISoftDelete
{
    // Private constructor for Entity Framework
    private Product() { }
    
    // Factory method for creation (enforces business rules)
    public static Product Create(string name, decimal price, int categoryId, string description = "")
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("Product name cannot be empty", nameof(name));
        
        if (price <= 0)
            throw new ArgumentException("Product price must be positive", nameof(price));
        
        var product = new Product
        {
            Name = name,
            Price = price,
            CategoryId = categoryId,
            Description = description,
            IsActive = true
        };
        
        // Raise domain event
        product.AddDomainEvent(new ProductCreatedEvent(product.Id, name, price));
        
        return product;
    }
    
    // Properties with proper encapsulation
    public string Name { get; private set; } = string.Empty;
    public decimal Price { get; private set; }
    public string Description { get; private set; } = string.Empty;
    public int CategoryId { get; private set; }
    public bool IsActive { get; private set; }
    
    // Navigation properties
    public Category Category { get; private set; } = null!;
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();
    private readonly List<OrderItem> _orderItems = new();
    
    // ISoftDelete properties
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
    public string? DeletedBy { get; set; }
    
    // Business methods 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;
        
        AddDomainEvent(new ProductPriceChangedEvent(Id, oldPrice, newPrice));
    }
    
    public void Deactivate()
    {
        IsActive = false;
        AddDomainEvent(new ProductDeactivatedEvent(Id, Name));
    }
    
    public void UpdateDetails(string name, string description)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("Name cannot be empty", nameof(name));
        
        Name = name;
        Description = description;
        
        AddDomainEvent(new ProductDetailsUpdatedEvent(Id, name, description));
    }
}

Service Layer Best Practices

// βœ… Well-Structured Service
public interface IProductService
{
    Task<ProductDto> CreateProductAsync(CreateProductRequest request);
    Task<ProductDto> GetProductByIdAsync(int id);
    Task<IPaginate<ProductDto>> GetProductsPagedAsync(ProductFilterRequest request);
    Task UpdateProductPriceAsync(int id, decimal newPrice);
    Task DeactivateProductAsync(int id);
}

public class ProductService : IProductService
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly IMapper _mapper;
    private readonly IValidator<CreateProductRequest> _createValidator;
    private readonly ILogger<ProductService> _logger;
    
    public ProductService(
        IUnitOfWork unitOfWork,
        IMapper mapper,
        IValidator<CreateProductRequest> createValidator,
        ILogger<ProductService> logger)
    {
        _unitOfWork = unitOfWork;
        _mapper = mapper;
        _createValidator = createValidator;
        _logger = logger;
    }
    
    public async Task<ProductDto> CreateProductAsync(CreateProductRequest request)
    {
        // 1. Validate input
        var validationResult = await _createValidator.ValidateAsync(request);
        if (!validationResult.IsValid)
        {
            throw new ValidationException(validationResult.Errors);
        }
        
        // 2. Check business rules
        await ValidateProductCreationRulesAsync(request);
        
        // 3. Execute in transaction
        return await _unitOfWork.ExecuteInTransactionAsync(async () =>
        {
            var repository = _unitOfWork.GetRepository<Product, int>();
            
            // 4. Create entity using factory method
            var product = Product.Create(
                request.Name,
                request.Price,
                request.CategoryId,
                request.Description);
            
            // 5. Save entity
            await repository.AddAsync(product);
            await _unitOfWork.SaveChangesAsync();
            
            // 6. Log success
            _logger.LogInformation("Product created: {ProductId} - {ProductName}", 
                product.Id, product.Name);
            
            // 7. Return DTO
            return _mapper.Map<ProductDto>(product);
        });
    }
    
    public async Task<ProductDto> GetProductByIdAsync(int id)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var spec = new ProductWithCategorySpecification(id);
        
        var products = await repository.GetAsync(spec);
        var product = products.FirstOrDefault();
        
        if (product == null)
        {
            throw new NotFoundException($"Product with ID {id} not found");
        }
        
        return _mapper.Map<ProductDto>(product);
    }
    
    public async Task<IPaginate<ProductDto>> GetProductsPagedAsync(ProductFilterRequest request)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var filter = _mapper.Map<FilterModel>(request);
        
        var products = await repository.GetPagedWithFilterAsync(
            filter,
            request.Page,
            request.PageSize,
            orderBy: GetOrderByExpression(request.SortBy, request.SortDirection),
            includes: new List<Expression<Func<Product, object>>> { p => p.Category }
        );
        
        return products.Map(_mapper.Map<ProductDto>);
    }
    
    public async Task UpdateProductPriceAsync(int id, decimal newPrice)
    {
        var repository = _unitOfWork.GetRepository<Product, int>();
        var product = await repository.GetByIdAsync(id);
        
        if (product == null)
        {
            throw new NotFoundException($"Product with ID {id} not found");
        }
        
        // Use domain method (includes business rules and domain events)
        product.UpdatePrice(newPrice);
        
        await repository.UpdateAsync(product);
        await _unitOfWork.SaveChangesAsync();
        
        _logger.LogInformation("Product price updated: {ProductId} - {NewPrice}", 
            id, newPrice);
    }
    
    private async Task ValidateProductCreationRulesAsync(CreateProductRequest request)
    {
        var categoryRepository = _unitOfWork.GetRepository<Category, int>();
        var category = await categoryRepository.GetByIdAsync(request.CategoryId);
        
        if (category == null)
        {
            throw new BusinessRuleViolationException($"Category {request.CategoryId} does not exist");
        }
        
        if (!category.IsActive)
        {
            throw new BusinessRuleViolationException("Cannot create products in inactive categories");
        }
    }
    
    private static Func<IQueryable<Product>, IOrderedQueryable<Product>> GetOrderByExpression(
        string? sortBy, string? sortDirection)
    {
        return (sortBy?.ToLower(), sortDirection?.ToLower()) switch
        {
            ("name", "desc") => query => query.OrderByDescending(p => p.Name),
            ("name", _) => query => query.OrderBy(p => p.Name),
            ("price", "desc") => query => query.OrderByDescending(p => p.Price),
            ("price", _) => query => query.OrderBy(p => p.Price),
            ("createdat", "desc") => query => query.OrderByDescending(p => p.CreatedAt),
            ("createdat", _) => query => query.OrderBy(p => p.CreatedAt),
            _ => query => query.OrderBy(p => p.Id)
        };
    }
}

Configuration Best Practices

// βœ… Environment-Specific Configuration
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(
        this IServiceCollection services,
        IConfiguration configuration,
        IWebHostEnvironment environment)
    {
        // Database configuration
        services.AddDbContext<ApplicationDbContext>(options =>
        {
            options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"));
            
            if (environment.IsDevelopment())
            {
                options.EnableDetailedErrors();
                options.EnableSensitiveDataLogging();
                options.LogTo(Console.WriteLine, LogLevel.Information);
            }
        });
        
        // FS.EntityFramework configuration
        var fsBuilder = services.AddFSEntityFramework<ApplicationDbContext>();
        
        // Audit configuration
        fsBuilder.WithAudit()
            .UsingHttpContext();
        
        // Domain events configuration
        if (configuration.GetValue<bool>("Features:DomainEvents"))
        {
            fsBuilder.WithDomainEvents()
                .UsingDefaultDispatcher()
                .WithAutoHandlerDiscovery()
                .Complete();
        }
        
        // Soft delete configuration
        if (configuration.GetValue<bool>("Features:SoftDelete"))
        {
            fsBuilder.WithSoftDelete();
        }
        
        // ID generation configuration
        var idGenerationStrategy = configuration.GetValue<string>("IdGeneration:Strategy");
        switch (idGenerationStrategy?.ToLower())
        {
            case "guidv7":
                fsBuilder.WithGuidV7();
                break;
            case "ulid":
                fsBuilder.WithUlid();
                break;
            default:
                fsBuilder.WithIdGeneration()
                    .WithGenerator<Guid, StandardGuidGenerator>()
                    .Complete();
                break;
        }
        
        // Custom repositories
        fsBuilder.WithCustomRepository<Product, int, ProductRepository>()
            .WithRepositoriesFromAssembly(Assembly.GetExecutingAssembly());
        
        // Validation and build
        if (environment.IsDevelopment())
        {
            fsBuilder.ValidateConfiguration();
        }
        
        fsBuilder.Build();
        
        return services;
    }
}

// βœ… Configuration Options
public class FS EntityFrameworkOptions
{
    public const string SectionName = "FSEntityFramework";
    
    public bool EnableAudit { get; set; } = true;
    public bool EnableDomainEvents { get; set; } = true;
    public bool EnableSoftDelete { get; set; } = true;
    public string IdGenerationStrategy { get; set; } = "default";
    public bool ValidateConfiguration { get; set; } = false;
}

// βœ… Options-based Configuration
services.Configure<FSEntityFrameworkOptions>(
    configuration.GetSection(FSEntityFrameworkOptions.SectionName));

services.AddFSEntityFramework<ApplicationDbContext>()
    .When(options.EnableAudit, builder => builder.WithAudit().UsingHttpContext())
    .When(options.EnableDomainEvents, builder => 
        builder.WithDomainEvents()
            .UsingDefaultDispatcher()
            .WithAutoHandlerDiscovery()
        .Complete())
    .When(options.EnableSoftDelete, builder => builder.WithSoftDelete())
    .When(options.ValidateConfiguration, builder => builder.ValidateConfiguration())
    .Build();

Testing Best Practices

// βœ… Unit Test Setup
public class ProductServiceTests
{
    private readonly Mock<IUnitOfWork> _unitOfWorkMock;
    private readonly Mock<IRepository<Product, int>> _repositoryMock;
    private readonly Mock<IMapper> _mapperMock;
    private readonly Mock<ILogger<ProductService>> _loggerMock;
    private readonly ProductService _productService;
    
    public ProductServiceTests()
    {
        _unitOfWorkMock = new Mock<IUnitOfWork>();
        _repositoryMock = new Mock<IRepository<Product, int>>();
        _mapperMock = new Mock<IMapper>();
        _loggerMock = new Mock<ILogger<ProductService>>();
        
        _unitOfWorkMock
            .Setup(x => x.GetRepository<Product, int>())
            .Returns(_repositoryMock.Object);
        
        _productService = new ProductService(
            _unitOfWorkMock.Object,
            _mapperMock.Object,
            Mock.Of<IValidator<CreateProductRequest>>(),
            _loggerMock.Object);
    }
    
    [Fact]
    public async Task CreateProductAsync_ValidRequest_ReturnsProductDto()
    {
        // Arrange
        var request = new CreateProductRequest
        {
            Name = "Test Product",
            Price = 100,
            CategoryId = 1,
            Description = "Test Description"
        };
        
        var product = Product.Create(request.Name, request.Price, request.CategoryId, request.Description);
        var productDto = new ProductDto { Id = 1, Name = request.Name, Price = request.Price };
        
        _unitOfWorkMock
            .Setup(x => x.ExecuteInTransactionAsync(It.IsAny<Func<Task<ProductDto>>>()))
            .ReturnsAsync(productDto);
        
        _mapperMock
            .Setup(x => x.Map<ProductDto>(It.IsAny<Product>()))
            .Returns(productDto);
        
        // Act
        var result = await _productService.CreateProductAsync(request);
        
        // Assert
        Assert.NotNull(result);
        Assert.Equal(request.Name, result.Name);
        Assert.Equal(request.Price, result.Price);
        
        _repositoryMock.Verify(x => x.AddAsync(It.IsAny<Product>(), false, default), Times.Once);
        _unitOfWorkMock.Verify(x => x.SaveChangesAsync(), Times.Once);
    }
}

// βœ… Integration Test Setup
public class ProductServiceIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly ApplicationDbContext _context;
    private readonly IServiceScope _scope;
    
    public ProductServiceIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _scope = _factory.Services.CreateScope();
        _context = _scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
    }
    
    [Fact]
    public async Task CreateProduct_WithValidData_SavesProductToDatabase()
    {
        // Arrange
        using var scope = _factory.Services.CreateScope();
        var productService = scope.ServiceProvider.GetRequiredService<IProductService>();
        
        var request = new CreateProductRequest
        {
            Name = "Integration Test Product",
            Price = 150,
            CategoryId = 1,
            Description = "Integration test description"
        };
        
        // Act
        var result = await productService.CreateProductAsync(request);
        
        // Assert
        Assert.NotNull(result);
        Assert.True(result.Id > 0);
        
        // Verify in database
        var savedProduct = await _context.Products.FindAsync(result.Id);
        Assert.NotNull(savedProduct);
        Assert.Equal(request.Name, savedProduct.Name);
        Assert.Equal(request.Price, savedProduct.Price);
        Assert.NotNull(savedProduct.CreatedAt);
        Assert.NotNull(savedProduct.CreatedBy);
    }
    
    public void Dispose()
    {
        _scope.Dispose();
    }
}

// βœ… Test DbContext for Unit Tests
public class TestDbContext : DbContext
{
    public TestDbContext(DbContextOptions<TestDbContext> options) : base(options)
    {
    }
    
    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        
        // Apply soft delete filters
        modelBuilder.ApplySoftDeleteQueryFilters();
        
        // Seed test data
        modelBuilder.Entity<Category>().HasData(
            new Category { Id = 1, Name = "Test Category" }
        );
    }
}

// βœ… Test Configuration
public static class TestServiceCollectionExtensions
{
    public static IServiceCollection AddTestServices(this IServiceCollection services)
    {
        // Use in-memory database for tests
        services.AddDbContext<TestDbContext>(options =>
            options.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()));
        
        // Configure FS.EntityFramework for testing
        services.AddFSEntityFramework<TestDbContext>()
            .WithAudit()
                .UsingStaticUser("test-user")
            .WithSoftDelete()
            .Build();
        
        return services;
    }
}

πŸ”§ Troubleshooting

Common Issues and Solutions

1. DbContext Registration Issues
// ❌ Problem: DbContext not registered error
services.AddFSEntityFramework<MyDbContext>(); // Error: DbContext not found

// βœ… Solution: Register DbContext first
services.AddDbContext<MyDbContext>(options => 
    options.UseSqlServer(connectionString));
services.AddFSEntityFramework<MyDbContext>().Build();
2. Domain Event Handler Not Found
// ❌ Problem: Handler not discovered
public class ProductCreatedEventHandler : IDomainEventHandler<ProductCreatedEvent>
{
    // Handler exists but not found during auto-discovery
}

// βœ… Solution: Ensure correct assembly scanning
services.AddFSEntityFramework<MyDbContext>()
    .WithDomainEvents()
        .UsingDefaultDispatcher()
        .WithAutoHandlerDiscovery(typeof(ProductCreatedEventHandler).Assembly) // Specify assembly
    .Complete()
    .Build();

// βœ… Alternative: Manual registration
services.AddFSEntityFramework<MyDbContext>()
    .WithDomainEvents()
        .UsingDefaultDispatcher()
        .WithHandler<ProductCreatedEvent, ProductCreatedEventHandler>()
    .Complete()
    .Build();
3. Audit Properties Not Set
// ❌ Problem: CreatedBy, UpdatedBy properties remain null
public class Product : BaseAuditableEntity<int>
{
    // Properties not being set automatically
}

// βœ… Solution: Ensure audit configuration is correct
services.AddFSEntityFramework<MyDbContext>()
    .WithAudit()
        .UsingHttpContext() // Make sure HttpContext is available
    .Build();

// βœ… Alternative: Check user context availability
services.AddFSEntityFramework<MyDbContext>()
    .WithAudit()
        .UsingUserProvider(provider =>
        {
            var userService = provider.GetService<ICurrentUserService>();
            var userId = userService?.GetCurrentUserId();
            Console.WriteLine($"Current User: {userId}"); // Debug output
            return userId;
        })
    .Build();
4. Soft Delete Not Working
// ❌ Problem: Entities are hard deleted instead of soft deleted
public class Product : BaseAuditableEntity<int> // Missing ISoftDelete
{
    public bool IsDeleted { get; set; } // Properties exist but interface not implemented
}

// βœ… Solution: Implement ISoftDelete interface
public class Product : BaseAuditableEntity<int>, ISoftDelete
{
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
    public string? DeletedBy { get; set; }
}

// βœ… Ensure global query filters are applied
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    modelBuilder.ApplySoftDeleteQueryFilters(); // Required for automatic filtering
}
5. ID Generation Not Working
// ❌ Problem: IDs are not generated automatically
public class Product : BaseEntity<Guid>
{
    // ID remains default(Guid) after save
}

// βœ… Solution: Ensure ID generation is configured
services.AddFSEntityFramework<MyDbContext>()
    .WithGuidV7() // Or WithUlid(), or WithIdGeneration()
    .Build();

// βœ… Check entity inherits from BaseEntity
public class Product : BaseEntity<Guid> // Must inherit from BaseEntity<TKey>
{
    // Properties...
}

Debugging Techniques

// βœ… Enable Detailed Logging
services.AddFSEntityFramework<MyDbContext>()
    .WithDetailedLogging(enableSensitiveDataLogging: true)
    .ValidateConfiguration() // Throws helpful errors
    .Build();

// βœ… Custom Diagnostic Service
public class FSEntityFrameworkDiagnostics
{
    private readonly IServiceProvider _serviceProvider;
    
    public FSEntityFrameworkDiagnostics(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public DiagnosticResult RunDiagnostics()
    {
        var result = new DiagnosticResult();
        
        // Check if UnitOfWork is registered
        result.UnitOfWorkRegistered = _serviceProvider.GetService<IUnitOfWork>() != null;
        
        // Check if interceptors are registered
        result.AuditInterceptorRegistered = _serviceProvider.GetService<AuditInterceptor>() != null;
        result.DomainEventInterceptorRegistered = _serviceProvider.GetService<DomainEventInterceptor>() != null;
        
        // Check if ID generators are registered
        result.GuidGeneratorRegistered = _serviceProvider.GetService<IIdGenerator<Guid>>() != null;
        result.UlidGeneratorRegistered = _serviceProvider.GetService<IIdGenerator<Ulid>>() != null;
        
        // Check domain event handlers
        result.DomainEventHandlers = GetRegisteredDomainEventHandlers();
        
        return result;
    }
    
    private List<string> GetRegisteredDomainEventHandlers()
    {
        var handlers = new List<string>();
        var services = _serviceProvider.GetService<IServiceCollection>();
        
        if (services != null)
        {
            foreach (var service in services)
            {
                if (service.ServiceType.IsGenericType &&
                    service.ServiceType.GetGenericTypeDefinition() == typeof(IDomainEventHandler<>))
                {
                    handlers.Add($"{service.ServiceType.Name} -> {service.ImplementationType?.Name}");
                }
            }
        }
        
        return handlers;
    }
}

public class DiagnosticResult
{
    public bool UnitOfWorkRegistered { get; set; }
    public bool AuditInterceptorRegistered { get; set; }
    public bool DomainEventInterceptorRegistered { get; set; }
    public bool GuidGeneratorRegistered { get; set; }
    public bool UlidGeneratorRegistered { get; set; }
    public List<string> DomainEventHandlers { get; set; } = new();
    
    public void PrintToConsole()
    {
        Console.WriteLine("=== FS.EntityFramework Diagnostics ===");
        Console.WriteLine($"UnitOfWork: {(UnitOfWorkRegistered ? "βœ…" : "❌")}");
        Console.WriteLine($"Audit Interceptor: {(AuditInterceptorRegistered ? "βœ…" : "❌")}");
        Console.WriteLine($"Domain Event Interceptor: {(DomainEventInterceptorRegistered ? "βœ…" : "❌")}");
        Console.WriteLine($"GUID Generator: {(GuidGeneratorRegistered ? "βœ…" : "❌")}");
        Console.WriteLine($"ULID Generator: {(UlidGeneratorRegistered ? "βœ…" : "❌")}");
        Console.WriteLine($"Domain Event Handlers: {DomainEventHandlers.Count}");
        
        foreach (var handler in DomainEventHandlers)
        {
            Console.WriteLine($"  - {handler}");
        }
    }
}

Performance Troubleshooting

// βœ… Query Performance Analysis
public class QueryPerformanceAnalyzer
{
    private readonly ApplicationDbContext _context;
    private readonly ILogger<QueryPerformanceAnalyzer> _logger;
    
    public QueryPerformanceAnalyzer(ApplicationDbContext context, ILogger<QueryPerformanceAnalyzer> logger)
    {
        _context = context;
        _logger = logger;
    }
    
    public async Task AnalyzeQueryPerformanceAsync()
    {
        var stopwatch = Stopwatch.StartNew();
        
        // Analyze different query patterns
        await AnalyzePaginationPerformanceAsync();
        await AnalyzeFilteringPerformanceAsync();
        await AnalyzeIncludePerformanceAsync();
        
        stopwatch.Stop();
        _logger.LogInformation("Query performance analysis completed in {ElapsedMs}ms", stopwatch.ElapsedMilliseconds);
    }
    
    private async Task AnalyzePaginationPerformanceAsync()
    {
        var stopwatch = Stopwatch.StartNew();
        
        var repository = new BaseRepository<Product, int>(_context);
        var result = await repository.GetPagedAsync(1, 10);
        
        stopwatch.Stop();
        _logger.LogInformation("Pagination query: {ElapsedMs}ms, {Count} items", 
            stopwatch.ElapsedMilliseconds, result.Count);
    }
    
    private async Task AnalyzeFilteringPerformanceAsync()
    {
        var stopwatch = Stopwatch.StartNew();
        
        var filter = new FilterModel
        {
            Filters = new List<FilterItem>
            {
                new() { Field = "Price", Operator = "greaterthan", Value = "100" },
                new() { Field = "Name", Operator = "contains", Value = "test" }
            }
        };
        
        var repository = new BaseRepository<Product, int>(_context);
        var result = await repository.GetPagedWithFilterAsync(filter, 1, 10);
        
        stopwatch.Stop();
        _logger.LogInformation("Filtering query: {ElapsedMs}ms, {Count} items", 
            stopwatch.ElapsedMilliseconds, result.Count);
    }
    
    private async Task AnalyzeIncludePerformanceAsync()
    {
        var stopwatch = Stopwatch.StartNew();
        
        var repository = new BaseRepository<Product, int>(_context);
        var result = await repository.GetWithIncludesAsync(
            includes: new List<Expression<Func<Product, object>>> { p => p.Category }
        );
        
        stopwatch.Stop();
        _logger.LogInformation("Include query: {ElapsedMs}ms, {Count} items", 
            stopwatch.ElapsedMilliseconds, result.Count);
    }
}

πŸ“– API Reference

Core Interfaces

IUnitOfWork
public interface IUnitOfWork : IDisposable
{
    // Repository Management
    TRepository GetRepository<TRepository>() where TRepository : class;
    IRepository<TEntity, TKey> GetRepository<TEntity, TKey>() 
        where TEntity : BaseEntity<TKey> where TKey : IEquatable<TKey>;
    
    // Persistence Operations
    Task<int> SaveChangesAsync();
    Task<int> SaveChangesAsync(CancellationToken cancellationToken);
    
    // Transaction Management
    Task<IDbContextTransaction> BeginTransactionAsync();
    Task CommitTransactionAsync();
    Task RollbackTransactionAsync();
    Task<TKey> ExecuteInTransactionAsync<TKey>(Func<Task<TKey>> operation) where TKey : IEquatable<TKey>;
    
    // Change Tracking
    bool HasChanges { get; }
    void DetachAllEntities();
    EntityEntry<TEntity> Entry<TEntity>(TEntity entity) where TEntity : class;
}
IRepository<TEntity, TKey>
public interface IRepository<TEntity, TKey> 
    where TEntity : IEntity<TKey> where TKey : IEquatable<TKey>
{
    // Basic CRUD
    Task<TEntity?> GetByIdAsync(TKey id, bool disableTracking = false, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<TEntity>> GetAllAsync(bool disableTracking = true, CancellationToken cancellationToken = default);
    Task<TEntity> AddAsync(TEntity entity, bool saveChanges = false, CancellationToken cancellationToken = default);
    Task UpdateAsync(TEntity entity, bool saveChanges = false, CancellationToken cancellationToken = default);
    Task DeleteAsync(TEntity entity, bool saveChanges = false, CancellationToken cancellationToken = default);
    
    // Soft Delete & Restore
    Task RestoreAsync(TEntity entity, bool saveChanges = false, CancellationToken cancellationToken = default);
    Task HardDeleteAsync(TEntity entity, bool saveChanges = false, CancellationToken cancellationToken = default);
    
    // Advanced Querying
    Task<IReadOnlyList<TEntity>> GetWithIncludesAsync(/*parameters*/);
    Task<IReadOnlyList<TEntity>> GetAsync(BaseSpecification<TEntity> spec, CancellationToken cancellationToken = default);
    
    // Pagination
    Task<IPaginate<TEntity>> GetPagedAsync(/*parameters*/);
    Task<IPaginate<TEntity>> GetPagedWithFilterAsync(/*parameters*/);
    
    // Bulk Operations
    Task BulkInsertAsync(IEnumerable<TEntity> entities, bool saveChanges = false, CancellationToken cancellationToken = default);
    Task BulkDeleteAsync(Expression<Func<TEntity, bool>> predicate, bool saveChanges = false, CancellationToken cancellationToken = default);
    
    // Query Operations
    Task<TEntity?> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate, bool disableTracking = true, CancellationToken cancellationToken = default);
    Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
    Task<int> CountAsync(Expression<Func<TEntity, bool>>? predicate = null, CancellationToken cancellationToken = default);
    
    // Raw Querying
    IQueryable<TEntity> GetQueryable(bool disableTracking = true);
}

Configuration Interfaces

IFSEntityFrameworkBuilder
public interface IFSEntityFrameworkBuilder
{
    IServiceCollection Services { get; }
    Type DbContextType { get; }
}
IAuditConfigurationBuilder
public interface IAuditConfigurationBuilder
{
    IFSEntityFrameworkBuilder Builder { get; }
    IFSEntityFrameworkBuilder UsingUserProvider(Func<IServiceProvider, string?> getCurrentUser, Func<IServiceProvider, DateTime>? getCurrentTime = null);
    IFSEntityFrameworkBuilder UsingUserContext<TUserContext>() where TUserContext : class, IUserContext;
    IFSEntityFrameworkBuilder UsingHttpContext(string claimType = "...");
    IFSEntityFrameworkBuilder UsingStaticUser(string userId);
}
IDomainEventsConfigurationBuilder
public interface IDomainEventsConfigurationBuilder
{
    IFSEntityFrameworkBuilder Builder { get; }
    IDomainEventsConfigurationBuilder UsingDefaultDispatcher();
    IDomainEventsConfigurationBuilder UsingCustomDispatcher<TDispatcher>() where TDispatcher : class, IDomainEventDispatcher;
    IDomainEventsConfigurationBuilder WithAutoHandlerDiscovery();
    IDomainEventsConfigurationBuilder WithAutoHandlerDiscovery(Assembly assembly);
    IDomainEventsConfigurationBuilder WithHandler<TEvent, THandler>() where TEvent : class, IDomainEvent where THandler : class, IDomainEventHandler<TEvent>;
    IFSEntityFrameworkBuilder Complete();
}

Domain Events

IDomainEvent
public interface IDomainEvent
{
    Guid EventId { get; }
    DateTime OccurredOn { get; }
}
IDomainEventHandler<T>
public interface IDomainEventHandler<in TDomainEvent> where TDomainEvent : IDomainEvent
{
    Task Handle(TDomainEvent domainEvent, CancellationToken cancellationToken = default);
}
IDomainEventDispatcher
public interface IDomainEventDispatcher
{
    Task DispatchAsync(IDomainEvent domainEvent, CancellationToken cancellationToken = default);
    Task DispatchAsync(IEnumerable<IDomainEvent> domainEvents, CancellationToken cancellationToken = default);
}

ID Generation

IIdGenerator<TKey>
public interface IIdGenerator<out TKey> : IIdGenerator where TKey : IEquatable<TKey>
{
    new TKey Generate();
    Type KeyType { get; }
}
IIdGeneratorFactory
public interface IIdGeneratorFactory
{
    IIdGenerator<TKey>? GetGenerator<TKey>() where TKey : IEquatable<TKey>;
    IIdGenerator? GetGeneratorForType(Type keyType);
}

Pagination

IPaginate<T>
public interface IPaginate<T>
{
    int From { get; }
    int Index { get; }
    int Size { get; }
    int Count { get; }
    int Pages { get; }
    IList<T> Items { get; }
    bool HasPrevious { get; }
    bool HasNext { get; }
}

Audit Interfaces

ICreationAuditableEntity
public interface ICreationAuditableEntity
{
    DateTime CreatedAt { get; set; }
    string? CreatedBy { get; set; }
}
IModificationAuditableEntity
public interface IModificationAuditableEntity
{
    DateTime? UpdatedAt { get; set; }
    string? UpdatedBy { get; set; }
}
ISoftDelete
public interface ISoftDelete
{
    bool IsDeleted { get; set; }
    DateTime? DeletedAt { get; set; }
    string? DeletedBy { get; set; }
}

🀝 Contributing

We welcome contributions! This project is open source and benefits from community involvement.

How to Contribute

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Development Guidelines

  • Follow existing code patterns and conventions
  • Add comprehensive tests for new features
  • Update documentation for any public API changes
  • Ensure backward compatibility when possible
  • Use the Fluent Configuration API for new features

Areas for Contribution

  • πŸ”Œ Additional domain event dispatchers (Mass Transit, NServiceBus, etc.)
  • ⚑ Performance optimizations
  • πŸ“‹ Additional specification implementations
  • πŸ“š Documentation improvements
  • 🎯 Example projects
  • πŸ§ͺ Test coverage improvements
  • πŸ”‘ New ID generation strategies

Code Style

  • Use meaningful variable names
  • Follow C# naming conventions
  • Add XML documentation for public APIs
  • Include unit tests for new functionality
  • Follow SOLID principles

Submitting Issues

When submitting issues, please include:

  • Clear description of the problem
  • Steps to reproduce the issue
  • Expected vs actual behavior
  • Environment details (OS, .NET version, package versions)
  • Sample code if applicable

πŸ“„ License

This project is licensed under the MIT License. See the LICENSE file for details.


🌟 Acknowledgments

  • Thanks to all contributors who have helped make this library better
  • Inspired by clean architecture principles and domain-driven design
  • Built on top of the excellent Entity Framework Core

πŸ“ž Support

If you encounter any issues or have questions:

  1. Check the troubleshooting section
  2. Search existing GitHub issues
  3. Create a new issue with detailed information
  4. Join our community discussions

Happy coding! πŸš€


Made with ❀️ by Furkan Sarıkaya

GitHub LinkedIn Medium

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
9.0.7 273 7/20/2025
9.0.6.9 133 7/6/2025
9.0.6.8 126 7/6/2025
9.0.6.7 126 7/6/2025

GUID Version 7 Extension v9.0.6.7 - RFC 9562 Implementation

           NEW FEATURES:
           - 🆕 Automatic GUID Version 7 generation for BaseEntity<Guid> entities
           - 🆕 RFC 9562 compliant implementation using .NET 9 native support
           - 🆕 Fluent configuration: WithGuidV7() extension method
           - 🆕 Custom timestamp provider support for GUID generation
           - 🆕 Zero external dependencies - uses built-in .NET 9 Guid.CreateVersion7()

           IMPROVEMENTS:
           - βœ… Optimal database performance with timestamp-based ordering
           - βœ… Universal GUID ecosystem compatibility
           - βœ… Microsoft native implementation benefits
           - βœ… Industry standard RFC compliance
           - βœ… Perfect for microservice architectures

           REQUIREMENTS:
           - Requires .NET 9+ for native Guid.CreateVersion7() support

           USAGE:
           services.AddFSEntityFramework<DbContext>()
           .WithGuidV7()  // Automatic GUID V7 generation
           .Build();

           public class User : BaseAuditableEntity<Guid> { }

           Compatible with FS.EntityFramework.Library v9.0.6.7+