Framework EfCore
The EfCore layer provides robust Entity Framework Core implementations of the repository pattern with read/write separation, advanced querying capabilities, and optimized database operations for high-performance applications.
🎯 Purpose
The EfCore layer bridges your application services with the database:
- Repository pattern implementation with read/write separation
- Advanced querying with filtering, sorting, and pagination
- Transaction management through Unit of Work pattern
- Performance optimization with read-only contexts
- Database-agnostic design supporting multiple providers
📦 Installation
dotnet add package MasLazu.AspNet.Framework.EfCore
🏗️ Core Components
Architecture Overview
Repository Implementation
public class Repository<T, TContext> : IRepository<T>
where T : BaseEntity
where TContext : BaseDbContext
{
protected readonly TContext Context;
protected readonly DbSet<T> DbSet;
public Repository(TContext context)
{
Context = context;
DbSet = context.Set<T>();
}
public async Task<T> AddAsync(T entity, CancellationToken ct = default)
{
var entry = await DbSet.AddAsync(entity, ct);
return entry.Entity;
}
public async Task<T?> GetByIdAsync(Guid id, CancellationToken ct = default)
{
return await DbSet
.Where(e => e.DeletedAt == null)
.FirstOrDefaultAsync(e => e.Id == id, ct);
}
// ... additional methods
}
ReadRepository Implementation
public class ReadRepository<T, TContext> : IReadRepository<T>
where T : BaseEntity
where TContext : BaseReadDbContext
{
protected readonly TContext Context;
protected readonly DbSet<T> DbSet;
public ReadRepository(TContext context)
{
Context = context;
DbSet = context.Set<T>();
}
public async Task<PaginatedResult<T>> GetPaginatedAsync(
PaginationRequest request,
CancellationToken ct = default)
{
var query = DbSet.Where(e => e.DeletedAt == null);
// Apply filters
query = ApplyFilters(query, request.Filters);
// Apply sorting
query = ApplySorting(query, request.OrderBy, request.OrderDirection);
var totalCount = await query.CountAsync(ct);
var items = await query
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.ToListAsync(ct);
return new PaginatedResult<T>
{
Items = items,
TotalCount = totalCount,
Page = request.Page,
PageSize = request.PageSize,
TotalPages = (int)Math.Ceiling(totalCount / (double)request.PageSize)
};
}
}
🚀 Usage Examples
1. Basic DbContext Setup
// Write Context
public class ApplicationDbContext : BaseDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Product> Products { get; set; } = null!;
public DbSet<Order> Orders { get; set; } = null!;
public DbSet<Customer> Customers { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Apply configurations
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
// Global query filters for soft delete
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (typeof(BaseEntity).IsAssignableFrom(entityType.ClrType))
{
var parameter = Expression.Parameter(entityType.ClrType, "e");
var property = Expression.Property(parameter, nameof(BaseEntity.DeletedAt));
var condition = Expression.Equal(property, Expression.Constant(null));
var lambda = Expression.Lambda(condition, parameter);
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(lambda);
}
}
}
}
// Read Context (Optional - for read/write separation)
public class ApplicationReadDbContext : BaseReadDbContext
{
public ApplicationReadDbContext(DbContextOptions<ApplicationReadDbContext> options)
: base(options)
{
}
public DbSet<Product> Products { get; set; } = null!;
public DbSet<Order> Orders { get; set; } = null!;
public DbSet<Customer> Customers { get; set; } = null!;
}
2. Entity Configurations
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.ToTable("Products");
builder.HasKey(p => p.Id);
builder.Property(p => p.Name)
.HasMaxLength(100)
.IsRequired();
builder.Property(p => p.Description)
.HasMaxLength(1000);
builder.Property(p => p.Price)
.HasPrecision(18, 2);
builder.Property(p => p.Category)
.HasMaxLength(50)
.IsRequired();
// Indexes
builder.HasIndex(p => p.Name)
.IsUnique()
.HasFilter("[DeletedAt] IS NULL");
builder.HasIndex(p => p.Category);
builder.HasIndex(p => p.CreatedAt);
// Audit fields
builder.Property(p => p.CreatedAt)
.HasDefaultValueSql("GETUTCDATE()");
}
}
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("Orders");
builder.HasKey(o => o.Id);
builder.Property(o => o.OrderNumber)
.HasMaxLength(20)
.IsRequired();
builder.Property(o => o.TotalAmount)
.HasPrecision(18, 2);
// Relationships
builder.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasMany(o => o.Items)
.WithOne(i => i.Order)
.HasForeignKey(i => i.OrderId)
.OnDelete(DeleteBehavior.Cascade);
// Indexes
builder.HasIndex(o => o.OrderNumber)
.IsUnique();
builder.HasIndex(o => o.CustomerId);
builder.HasIndex(o => o.Status);
builder.HasIndex(o => o.CreatedAt);
}
}
3. Service Registration
// Program.cs or Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Database contexts
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString));
// Optional: Read context for read/write separation
services.AddDbContext<ApplicationReadDbContext>(options =>
options.UseNpgsql(readOnlyConnectionString));
// Framework registration
services.AddFrameworkEfCore();
// Auto-register repositories for all entities
services.AddRepositoriesForAllEntities<ApplicationDbContext>();
// Or register specific repositories
services.AddRepository<Product, ApplicationDbContext>();
services.AddRepository<Order, ApplicationDbContext>();
// Unit of Work
services.AddScoped<IUnitOfWork, UnitOfWork<ApplicationDbContext>>();
}
4. Advanced Querying
public class ProductService : CrudService<Product, ProductDto, CreateProductRequest, UpdateProductRequest>
{
public async Task<IEnumerable<ProductDto>> GetProductsByCategoryAsync(
Guid userId,
string category,
CancellationToken ct = default)
{
var products = await ReadRepository.GetManyAsync(
p => p.Category == category && p.Price > 0,
ct);
return products.Adapt<IEnumerable<ProductDto>>();
}
public async Task<PaginatedResult<ProductDto>> SearchProductsAsync(
Guid userId,
string searchTerm,
PaginationRequest pagination,
CancellationToken ct = default)
{
var request = pagination with
{
Filters = pagination.Filters.Concat(new[]
{
new FilterRequest
{
Field = "Name",
Operator = "contains",
Value = searchTerm
}
}).ToArray()
};
return await GetPaginatedAsync(userId, request, ct);
}
}
5. Unit of Work Pattern
public class OrderService : CrudService<Order, OrderDto, CreateOrderRequest, UpdateOrderRequest>
{
private readonly IRepository<Product> _productRepository;
private readonly IRepository<Inventory> _inventoryRepository;
public async Task<OrderDto> CreateOrderWithInventoryUpdateAsync(
Guid userId,
CreateOrderRequest request,
CancellationToken ct = default)
{
// Start transaction
using var transaction = await UnitOfWork.BeginTransactionAsync(ct);
try
{
// Create order
var order = await CreateAsync(userId, request, ct);
// Update inventory for each item
foreach (var item in request.Items)
{
var inventory = await _inventoryRepository.GetFirstAsync(
i => i.ProductId == item.ProductId, ct);
if (inventory == null || inventory.Quantity < item.Quantity)
throw new BusinessException($"Insufficient inventory for product {item.ProductId}");
inventory.Quantity -= item.Quantity;
await _inventoryRepository.UpdateAsync(inventory, ct);
}
// Save all changes
await UnitOfWork.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);
return order;
}
catch
{
await transaction.RollbackAsync(ct);
throw;
}
}
}
🎯 Key Features
1. Read/Write Separation
2. Advanced Filtering
// Supported filter operators
var filters = new[]
{
new FilterRequest { Field = "Name", Operator = "eq", Value = "Product A" },
new FilterRequest { Field = "Price", Operator = "gte", Value = "100" },
new FilterRequest { Field = "Category", Operator = "in", Value = "Electronics,Books" },
new FilterRequest { Field = "Description", Operator = "contains", Value = "premium" },
new FilterRequest { Field = "CreatedAt", Operator = "between", Value = "2024-01-01,2024-12-31" }
};
3. Cursor Pagination
public async Task<CursorPaginatedResult<ProductDto>> GetProductsCursorAsync(
Guid userId,
CursorPaginationRequest request,
CancellationToken ct = default)
{
// High-performance pagination for large datasets
return await GetCursorPaginatedAsync(userId, request, ct);
}
4. Soft Delete Support
// Automatic soft delete filtering
public async Task DeleteAsync(Guid userId, Guid id, CancellationToken ct = default)
{
var entity = await Repository.GetByIdAsync(id, ct);
if (entity == null)
throw new NotFoundException();
entity.DeletedAt = DateTime.UtcNow;
await Repository.UpdateAsync(entity, ct);
await UnitOfWork.SaveChangesAsync(ct);
}
// Include deleted entities when needed
public async Task<IEnumerable<ProductDto>> GetAllIncludingDeletedAsync(
Guid userId,
CancellationToken ct = default)
{
var products = await ReadRepository.GetManyAsync(
p => true, // No filter - includes deleted
includeDeleted: true,
ct);
return products.Adapt<IEnumerable<ProductDto>>();
}
📁 Project Structure
MasLazu.AspNet.Framework.EfCore/
├── Data/
│ ├── BaseDbContext.cs # Base write context
│ ├── BaseReadDbContext.cs # Base read context
│ └── UnitOfWork.cs # Transaction management
├── Repositories/
│ ├── Repository.cs # Write repository implementation
│ └── ReadRepository.cs # Read repository implementation
├── Services/
│ └── DatabaseService.cs # Database utilities
├── Extensions/
│ └── EntityFrameworkCoreRepositoryExtensions.cs # DI registration
├── Configurations/
│ └── BaseEntityConfiguration.cs # Base entity configuration
└── MasLazu.AspNet.Framework.EfCore.csproj
🔗 Dependencies
<PackageReference Include="MasLazu.AspNet.Framework.Application" />
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="Mapster" />
⚙️ Database Providers
The framework supports multiple database providers through additional packages:
PostgreSQL (Recommended)
dotnet add package MasLazu.AspNet.Framework.EfCore.Postgresql
SQL Server
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
SQLite (Development)
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
MySQL
dotnet add package Pomelo.EntityFrameworkCore.MySql
✅ Best Practices
✅ Do's
- Use entity configurations for complex mappings
- Implement global query filters for soft delete
- Create proper indexes for performance
- Use read/write separation for scalability
- Handle transactions with Unit of Work
- Use cancellation tokens for responsive operations
❌ Don'ts
- Don't use raw SQL unless absolutely necessary
- Don't ignore performance implications of queries
- Don't forget to handle concurrency conflicts
- Don't expose DbContext outside the repository layer
- Don't perform complex logic in entity configurations
🎯 Performance Tips
1. Efficient Querying
// Good: Use projection to limit data transfer
var products = await ReadRepository.GetManyAsync(
p => p.Category == "Electronics",
p => new ProductSummaryDto { Id = p.Id, Name = p.Name, Price = p.Price },
ct);
// Avoid: Loading full entities when only partial data is needed
var products = await ReadRepository.GetManyAsync(
p => p.Category == "Electronics", ct);
2. Proper Indexing
// Create composite indexes for common query patterns
builder.HasIndex(p => new { p.Category, p.Price });
builder.HasIndex(p => new { p.CreatedAt, p.DeletedAt });
3. Cursor Pagination for Large Datasets
// Use cursor pagination for better performance with large datasets
var request = new CursorPaginationRequest
{
Limit = 100,
OrderBy = "CreatedAt",
OrderDirection = "desc"
};
🎯 Next Steps
After setting up your EfCore layer:
- Framework.EfCore.Postgresql - PostgreSQL-specific features
- Framework.Endpoint - Create API endpoints
- Performance Optimization - Advanced optimization techniques
📚 Related Documentation
- Framework.Application - Business logic layer
- Framework.Domain - Domain entities
- Architecture Overview