-
-
Notifications
You must be signed in to change notification settings - Fork 0
EF Core Integration
EntityAxis integrates naturally with Entity Framework Core, allowing you to plug in generic CRUD logic and customize where needed.
The EntityAxis.EntityFramework package provides two base classes:
-
EntityFrameworkCommandService<TEntity, TKey>
ImplementsICreate,IUpdate, andIDelete. -
EntityFrameworkQueryService<TEntity, TKey>
ImplementsIGetById,IGetAll, andIGetPaged.
These classes abstract common EF Core behavior so you can focus on overrides and business rules.
To follow CQRS principles, split your command and query responsibilities into separate services:
public class ProductCommandService
: EntityFrameworkCommandService<Product, Guid>,
IProductCommandService
{
public ProductCommandService(IDbContextFactory<ProductDbContext> dbContextFactory)
: base(dbContextFactory) { }
}
public class ProductQueryService
: EntityFrameworkQueryService<Product, Guid>,
IProductQueryService
{
public ProductQueryService(IDbContextFactory<ProductDbContext> dbContextFactory)
: base(dbContextFactory) { }
}Then register them independently:
services.AddEntityAxisCommandService<IProductCommandService, ProductCommandService, Product, Guid>();
services.AddEntityAxisQueryService<IProductQueryService, ProductQueryService, Product, Guid>();Or register them by scanning the assembly:
services.AddEntityAxisCommandAndQueryServicesFromAssembly<ProductCommandService>();✅ This is the recommended approach for production scenarios and aligns with CQRS best practices.
You must register IDbContextFactory<TDbContext> for EF Core to function properly. EntityAxis.EntityFramework depends on the factory to ensure correct DbContext resolution across scenarios.
services.AddDbContextFactory<ProductDbContext>(options =>
{
options.UseInMemoryDatabase("ProductDb");
});ℹ️
EntityFrameworkCommandServiceandEntityFrameworkQueryServicerequireIDbContextFactory<T>to support unit-of-work boundaries in MediatR handlers and other async workflows. This follows Microsoft’s guidance for multiple units-of-work per request.
The EF Core services rely on AutoMapper to translate between your domain models (e.g., Product) and your EF Core persistence models (e.g., ProductDbEntity). You must configure mappings in both directions.
public class ProductProfile : Profile
{
public ProductProfile()
{
CreateMap<Product, ProductDbEntity>().ReverseMap();
}
}✅ This allows EF-based services to perform automated projections and persist domain objects without tightly coupling your domain to EF Core.
Register your profiles in Startup.cs or during host building:
services.AddAutoMapper(typeof(ProductProfile).Assembly);Need custom query logic beyond what the default service provides?
Instead of overriding methods, you can write your own IQueryService or ICommandService implementation using your own logic — and still benefit from EntityAxis registration and abstractions.
This approach promotes clean, testable code and avoids inheritance-based complexity.
💡 If you only need default behavior, the built-in EntityAxis services will likely be sufficient. But when you need more control, writing your own service gives you full flexibility without being boxed into a base class.
public class CustomProductQueryService : IQueryService<Product, Guid>
{
private readonly ProductDbContext _context;
private readonly IMapper _mapper;
public CustomProductQueryService(ProductDbContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<Product?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
var entity = await _context.Products
.Where(p => p.IsActive)
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
return _mapper.Map<Product>(entity);
}
public async Task<List<Product>> GetAllAsync(CancellationToken cancellationToken = default)
{
var entities = await _context.Products
.Where(p => p.IsActive)
.ToListAsync(cancellationToken);
return _mapper.Map<List<Product>>(entities);
}
public async Task<PagedResult<Product>> GetPagedAsync(int page, int pageSize, CancellationToken cancellationToken = default)
{
var query = _context.Products.Where(p => p.IsActive);
var totalItemCount = await query.CountAsync(cancellationToken);
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
var mapped = _mapper.Map<List<Product>>(items);
return new PagedResult<Product>(mapped, totalItemCount, page, pageSize);
}
}You can easily test your EF Core services using the UseInMemoryDatabase provider. This allows you to verify persistence logic without needing a real database.
var services = new ServiceCollection();
services.AddDbContextFactory<ProductDbContext>(options =>
{
options.UseInMemoryDatabase("TestDb");
});
services.AddAutoMapper(typeof(ProductProfile).Assembly);
services.AddTransient<IQueryService<Product, Guid>, ProductQueryService>();
var provider = services.BuildServiceProvider();
var service = provider.GetRequiredService<IQueryService<Product, Guid>>();
// Use service in your tests...This works well for basic scenarios but may not reflect real database behavior (e.g., relational constraints, transactions, or database-generated values).
For integration and end-to-end tests, use a real database in a containerized environment for reliable results.
We recommend the following libraries:
- Testcontainers for .NET: Spin up disposable PostgreSQL, SQL Server, or MySQL containers directly in your test suite.
- Respawn: Reset the database between tests quickly without re-creating the schema, ensuring clean state.
This provides realistic database behavior while keeping test runs isolated and repeatable.
💡 EntityAxis services work seamlessly with both approaches since they use
IDbContextFactory<T>.