Repository Implementation
The Problem
Section titled “The Problem”Each entity needs a repository that:
- wraps the correct
DbSet<T> - resolves the right
DbContextwhen multiple boundaries exist - applies filters on every read path
- performs soft-delete correctly
That is all infrastructure code. The generator should own it.
What the generator emits
Section titled “What the generator emits”For each entity, the generator emits a concrete repository class. That class implements IRepository<TEntity> and can also include convenience members such as:
- logic-key helpers
- include overloads
- bulk methods
See the core Repository guide for the distinction between stable interfaces and the concrete generated surface.
DbContext resolution
Section titled “DbContext resolution”Generated repositories resolve the boundary DbContext through keyed DI:
// Generated in {Namespace}.Order.Repository.g.cspublic partial class Order{ public partial class Repository : IRepository<Order> { public Repository( [FromKeyedServices(typeof(SalesBoundary))] DbContext db, [FromKeyedServices(typeof(SalesBoundary))] IUnitOfWork unitOfWork, IQueryFilterProvider? filterProvider = null, FilterMapComposer? filterMapComposer = null, ITenantContext? tenantContext = null, IQueryFilterToggle? filterToggle = null, IEnumerable<RollUpRule>? rollUpRules = null, TimeProvider? timeProvider = null, // only on [Auditable] / [SoftDelete] entities ICurrentUser? currentUser = null) // same { /* … */ } }}Two things follow from that constructor. Both the context and the unit of work are keyed on the boundary, and on the same key — so the repository saves through the very instance an invoker holds, rather than a second one over the same context. And every collaborator after them is optional: a container missing the filter services builds the repository happily and reads unfiltered.
Repository registration
Section titled “Repository registration”The generated repository registration method shape is:
// _Infra.Persistence.RepositoryRegistration.g.cs — one generic method for the whole assemblypublic static IServiceCollection AddPragmaticPersistenceRepositories<TDbContext>( this IServiceCollection services) where TDbContext : DbContext{ // per boundary services.AddKeyedScoped<DbContext>(typeof(SalesBoundary), (sp, _) => sp.GetRequiredService<TDbContext>()); services.AddKeyedScoped<IUnitOfWork>(typeof(SalesBoundary), (sp, _) => new EfCoreUnitOfWork(sp.GetRequiredService<TDbContext>(), /* logger */ null, /* dispatcher */ null));
// per entity services.AddScoped<Order.Repository>(); services.AddScoped<IRepository<Order>>(sp => sp.GetRequiredService<Order.Repository>()); services.AddScoped<IReadRepository<Order>>(sp => sp.GetRequiredService<Order.Repository>()); return services;}Inside a Pragmatic host you do not call it: the generated host emits the same registrations directly,
in RegisterAllRepositories().
Important correction:
- repositories are registered unkeyed for normal injection
- keyed resolution is used for the
DbContextandIUnitOfWork, not for the repository itself
Filter integration
Section titled “Filter integration”Generated repositories apply both root and navigation filtering:
private IQueryable<Order> ApplyFilters(IQueryable<Order> query){ var context = BuildFilterContext();
if (_filterProvider is not null) { var filter = _filterProvider.GetCombinedFilter<Order>( context, NavigationContext.Root<Order>());
if (filter is not null) query = query.Where(filter); }
if (_filterMapComposer is not null) query = _filterMapComposer.ApplyNavigationFilters(query, context);
return query;}If query-filter services are not registered, the repository still functions, but those filters are not applied.
Soft-delete behavior
Section titled “Soft-delete behavior”For entities with [SoftDelete], Remove() becomes a metadata update, not a physical delete:
public void Remove(Order entity){ entity.IsDeleted = true; entity.DeletedAt = _timeProvider.GetUtcNow(); entity.DeletedBy = _currentUser?.Id;}Entities without [SoftDelete] still use a physical EF Core remove.
Keyed IUnitOfWork
Section titled “Keyed IUnitOfWork”EfCoreUnitOfWork wraps the boundary DbContext and is registered keyed by boundary:
var uow = services.GetRequiredKeyedService<IUnitOfWork>(typeof(SalesBoundary));await uow.SaveChangesAsync(ct);BeginTransactionAsync(...) returns ITransaction, not plain IAsyncDisposable.