Clean data access abstraction supporting EF Core with generic repository interfaces. Provides type-safe CRUD operations, querying, pagination, and change tracking integration.
services.AddAetherNpgsql<MyDbContext>(configuration.GetConnectionString("Default"));
// This registers:
// - IRepository<TEntity, TKey> for each entity
// - IReadOnlyRepository<TEntity, TKey>
// - IAmbientUnitOfWorkAccessor
// - AuditInterceptorpublic class ProductService
{
private readonly IRepository<Product, Guid> _repository;
public ProductService(IRepository<Product, Guid> repository)
{
_repository = repository;
}
public async Task<Product> GetAsync(Guid id)
{
return await _repository.GetAsync(id);
}
[UnitOfWork]
public async Task CreateAsync(CreateProductDto dto)
{
var product = new Product(dto.Name, dto.Price);
await _repository.InsertAsync(product);
}
}public interface IRepository<TEntity, TKey> : IReadOnlyRepository<TEntity, TKey>
where TEntity : class, IEntity<TKey>
{
Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken ct = default);
Task InsertManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken ct = default);
Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken ct = default);
Task UpdateManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken ct = default);
Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken ct = default);
Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken ct = default);
Task DeleteManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken ct = default);
Task DeleteDirectAsync(Expression<Func<TEntity, bool>> predicate, bool saveChanges = true, CancellationToken ct = default);
}public interface IReadOnlyRepository<TEntity, TKey>
where TEntity : class, IEntity<TKey>
{
Task<TEntity> GetAsync(TKey id, bool includeDetails = true, CancellationToken ct = default);
Task<TEntity?> FindAsync(TKey id, bool includeDetails = true, CancellationToken ct = default);
Task<List<TEntity>> GetListAsync(bool includeDetails = false, CancellationToken ct = default);
Task<List<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> predicate, bool includeDetails = false, CancellationToken ct = default);
Task<long> GetCountAsync(CancellationToken ct = default);
Task<IQueryable<TEntity>> GetQueryableAsync();
}// Get by ID (throws if not found)
var product = await _repository.GetAsync(id);
// Find by ID (returns null if not found)
var product = await _repository.FindAsync(id);
// Get list with predicate
var products = await _repository.GetListAsync(p => p.Category == "Electronics");
// Insert
await _repository.InsertAsync(product);
// Update
await _repository.UpdateAsync(product);
// Delete by entity
await _repository.DeleteAsync(product);
// Delete by ID
await _repository.DeleteAsync(id);
// Bulk delete with predicate
await _repository.DeleteDirectAsync(p => p.IsExpired);public async Task<PagedList<Product>> GetPagedAsync(int page, int pageSize)
{
var query = await _repository.GetQueryableAsync();
return await query
.OrderBy(p => p.Name)
.ToPagedListAsync(page, pageSize);
}public async Task<List<Product>> GetTopSellingAsync(int count)
{
var query = await _repository.GetQueryableAsync();
return await query
.Where(p => p.IsActive)
.OrderByDescending(p => p.SalesCount)
.Take(count)
.ToListAsync();
}// Default: Changes tracked, saved with UoW commit
await _repository.InsertAsync(product);
// Immediate save (bypasses UoW)
await _repository.InsertAsync(product, autoSave: true);public class MyDbContext : AetherDbContext<MyDbContext>
{
public DbSet<Product> Products { get; set; }
public DbSet<Order> Orders { get; set; }
public MyDbContext(DbContextOptions<MyDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(MyDbContext).Assembly);
}
}- Use IRepository for writes - Full CRUD operations
- Use IReadOnlyRepository for queries - Query-only scenarios
- Avoid autoSave: true - Let UoW manage transaction boundaries
- Use GetQueryableAsync for complex queries - Access LINQ directly
- Inject repository, not DbContext - Maintains abstraction
- Unit of Work - Transaction management
- DDD - Entity base classes
- Application Services - Service layer