Entity Framework Core Handlers
The Entity Framework Core handlers provide a comprehensive CQRS (Command Query Responsibility Segregation) implementation for Entity Framework Core, enabling you to perform standardized CRUD operations with built-in support for caching, auditing, validation, and security.
Overview
This package provides pre-built handlers that implement the standard entity operations:
Query Handlers
EntityIdentifierQueryHandler
- Retrieve a single entity by its identifierEntityIdentifiersQueryHandler
- Retrieve multiple entities by their identifiersEntityPagedQueryHandler
- Retrieve entities with pagination, filtering, and sortingEntitySelectQueryHandler
- Retrieve entities with custom filtering and sorting
Command Handlers
EntityCreateCommandHandler
- Create new entities with automatic audit trackingEntityUpdateCommandHandler
- Update existing entities with change trackingEntityUpsertCommandHandler
- Create or update entities (insert or update)EntityPatchCommandHandler
- Apply partial updates using JSON patch documentsEntityDeleteCommandHandler
- Delete entities with soft delete support
Installation
Install-Package Arbiter.CommandQuery.EntityFramework
OR
dotnet add package Arbiter.CommandQuery.EntityFramework
Prerequisites
Before using the Entity Framework handlers, ensure you have:
- DbContext: A configured Entity Framework Core
DbContext
- Entity Classes: Domain entities that implement
IHaveIdentifier<TKey>
- Model Classes: Data transfer objects for create, update, and read operations
- Mapper: Implementation of
IMapper
for object mapping - Validator (optional): Implementation of
IValidator
for business rule validation
Basic Setup
1. Configure Entity Framework Core
// Configure your DbContext
services.AddDbContext<TrackerContext>(options =>
options.UseSqlServer(connectionString)
);
2. Register Core Services
// Register Command Query services
services.AddCommandQuery();
// Register your mapper implementation
services.AddSingleton<IMapper, MyMapper>();
// Register your validator implementation (optional)
services.AddSingleton<IValidator, MyValidator>();
3. Register Entity Handlers
Complete CRUD Operations
// Register both queries and commands for an entity
services.AddEntityQueries<TrackerContext, Product, int, ProductReadModel>();
services.AddEntityCommands<TrackerContext, Product, int, ProductReadModel, ProductCreateModel, ProductUpdateModel>();
Individual Registration
// Register only specific operations
services.AddEntityCreateCommand<TrackerContext, Product, int, ProductReadModel, ProductCreateModel>();
services.AddEntityUpdateCommand<TrackerContext, Product, int, ProductReadModel, ProductUpdateModel>();
services.AddEntityDeleteCommand<TrackerContext, Product, int, ProductReadModel>();
services.AddEntityUpsertCommand<TrackerContext, Product, int, ProductReadModel, ProductUpdateModel>();
services.AddEntityPatchCommand<TrackerContext, Product, int, ProductReadModel>();
Entity Requirements
Basic Entity Interface
Your entities must implement IHaveIdentifier<TKey>
:
public class Product : IHaveIdentifier<int>
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
// ... other properties
}
Enhanced Entity Features
Audit Tracking
public class Product : IHaveIdentifier<int>, ITrackCreated, ITrackUpdated
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
// Audit fields
public DateTimeOffset Created { get; set; }
public string? CreatedBy { get; set; }
public DateTimeOffset Updated { get; set; }
public string? UpdatedBy { get; set; }
}
Soft Delete Support
public class Product : IHaveIdentifier<int>, ITrackDeleted
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public bool IsDeleted { get; set; }
// ... other properties
}
Multi-Tenant Support
public class Product : IHaveIdentifier<int>, IHaveTenant<int>
{
public int Id { get; set; }
public int TenantId { get; set; }
public string Name { get; set; } = string.Empty;
// ... other properties
}
Model Classes
Read Model
public class ProductReadModel
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset Updated { get; set; }
}
Create Model
public class ProductCreateModel
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
Update Model
public class ProductUpdateModel
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
Usage Examples
Query Operations
Get Single Entity by ID
var query = new EntityIdentifierQuery<int, ProductReadModel>(principal, productId);
var product = await mediator.Send(query);
Get Multiple Entities by IDs
var ids = new[] { 1, 2, 3 };
var query = new EntityIdentifiersQuery<int, ProductReadModel>(principal, ids);
var products = await mediator.Send(query);
Paged Query with Filtering
var entityQuery = new EntityQuery
{
Page = 1,
PageSize = 20,
Sort = new[] { new EntitySort { Name = "created", Direction = "desc" } },
Filter = new EntityFilter { Name = "price", Operator = ">=", Value = 100 }
};
var query = new EntityPagedQuery<ProductReadModel>(principal, entityQuery);
var result = await mediator.Send(query);
Select with Custom Filtering
var entitySelect = new EntitySelect
{
Filter = new EntityFilter
{
Name = "category",
Value = "electronics"
},
Sort = new[] { new EntitySort { Name = "name" } }
};
var query = new EntitySelectQuery<ProductReadModel>(principal, entitySelect);
var products = await mediator.Send(query);
Command Operations
Create Entity
var createModel = new ProductCreateModel
{
Name = "Gaming Laptop",
Price = 1299.99m
};
var command = new EntityCreateCommand<ProductCreateModel, ProductReadModel>(principal, createModel);
var newProduct = await mediator.Send(command);
Update Entity
var updateModel = new ProductUpdateModel
{
Name = "Updated Gaming Laptop",
Price = 1199.99m
};
var command = new EntityUpdateCommand<int, ProductUpdateModel, ProductReadModel>(principal, productId, updateModel);
var updatedProduct = await mediator.Send(command);
Upsert Entity (Create or Update)
var updateModel = new ProductUpdateModel
{
Name = "Gaming Laptop",
Price = 1299.99m
};
var command = new EntityUpsertCommand<int, ProductUpdateModel, ProductReadModel>(principal, productId, updateModel);
var product = await mediator.Send(command);
Patch Entity
var patchDocument = new JsonPatchDocument();
patchDocument.Replace("/price", 999.99m);
var command = new EntityPatchCommand<int, ProductReadModel>(principal, productId, patchDocument);
var patchedProduct = await mediator.Send(command);
Delete Entity
var command = new EntityDeleteCommand<int, ProductReadModel>(principal, productId);
var deletedProduct = await mediator.Send(command);
Pipeline Behaviors
The Entity Framework handlers automatically integrate with various pipeline behaviors:
Audit Behaviors
- Create Audit: Automatically sets
Created
,CreatedBy
,Updated
, andUpdatedBy
fields - Update Audit: Updates
Updated
andUpdatedBy
fields on modifications - Change Notifications: Publishes
EntityChangeNotification
events after successful operations
Cache Behaviors
- Memory Cache: Caches query results in memory for improved performance
- Distributed Cache: Caches query results in distributed cache (Redis, SQL Server, etc.)
- Hybrid Cache: Combines memory and distributed caching strategies
- Cache Invalidation: Automatically expires cache entries when entities are modified
Security Behaviors
- Multi-Tenant: Automatically filters entities by tenant ID
- Soft Delete: Filters out deleted entities from query results
- Principal Context: Provides security context for all operations
Validation Behaviors
- Entity Validation: Validates entities using registered
IValidator
implementations - Business Rules: Enforces domain-specific business rules
Advanced Configuration
Custom Behaviors
// Add custom behaviors to the pipeline
services.AddTransient<IPipelineBehavior<EntityCreateCommand<ProductCreateModel, ProductReadModel>, ProductReadModel>, CustomProductValidationBehavior>();
Cache Configuration
// Enable memory caching for entity queries
services.AddEntityQueryMemoryCache<int, ProductReadModel>();
// Enable distributed caching
services.AddEntityQueryDistributedCache<int, ProductReadModel>();
// Enable hybrid caching
services.AddEntityHybridCache<int, ProductReadModel>();
Multi-Tenant Setup
// Register tenant-aware behaviors
services.AddEntityQueries<TrackerContext, Product, int, ProductReadModel>();
// Tenant filtering is automatically applied if entity implements IHaveTenant<TKey>
Error Handling
The handlers provide consistent error handling:
- Entity Not Found: Returns
null
for queries, throws for commands requiring existing entities - Validation Errors: Throws
ValidationException
with detailed error information - Concurrency Conflicts: Throws
DbUpdateConcurrencyException
for concurrent modifications - Domain Exceptions: Propagates custom
DomainException
instances
Performance Considerations
Query Optimization
- Use
EntitySelectQuery
instead ofEntityPagedQuery
when you don't need pagination - Implement efficient filtering at the database level
- Consider using projection to read models to reduce data transfer
Caching Strategy
- Enable appropriate caching based on your read/write patterns
- Use memory cache for frequently accessed, small datasets
- Use distributed cache for larger datasets or multi-instance deployments
Database Optimization
- Ensure proper indexes for filtered and sorted columns
- Use connection pooling for high-throughput scenarios
- Consider read replicas for read-heavy workloads
Best Practices
- Keep Models Simple: Use separate models for different operations (create, update, read)
- Implement Proper Mapping: Ensure your
IMapper
implementation handles all property mappings correctly - Use Audit Interfaces: Implement audit interfaces for automatic tracking of changes
- Leverage Soft Deletes: Use
ITrackDeleted
for recoverable delete operations - Configure Caching Appropriately: Choose caching strategies based on your specific use cases
- Handle Validation: Implement comprehensive validation using the
IValidator
interface - Consider Multi-Tenancy: Use tenant interfaces if your application serves multiple tenants