NovaCore.Lib.Repositories
0.0.2
dotnet add package NovaCore.Lib.Repositories --version 0.0.2
NuGet\Install-Package NovaCore.Lib.Repositories -Version 0.0.2
<PackageReference Include="NovaCore.Lib.Repositories" Version="0.0.2" />
<PackageVersion Include="NovaCore.Lib.Repositories" Version="0.0.2" />
<PackageReference Include="NovaCore.Lib.Repositories" />
paket add NovaCore.Lib.Repositories --version 0.0.2
#r "nuget: NovaCore.Lib.Repositories, 0.0.2"
#:package NovaCore.Lib.Repositories@0.0.2
#addin nuget:?package=NovaCore.Lib.Repositories&version=0.0.2
#tool nuget:?package=NovaCore.Lib.Repositories&version=0.0.2
NovaCore.Lib.Repositories
Spring-Data-style repositories for EF Core, wired at compile time. No unit-of-work abstraction, no runtime proxying, no per-entity registration — and no reflection, so the package works in a trimmed or NativeAOT application.
Consumers inject IRepository<Customer> and never need to know whether an entity uses the generic
repository or an application-defined custom one.
Setup
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlServer(connectionString));
builder.Services.AddRepositories<AppDbContext>();
That is the whole configuration. A source generator ships with the package and replaces the
AddRepositories call with the exact registrations for your model, computed while your project
compiles. Nothing scans assemblies at run time.
Using a repository
public sealed class Handler(IRepository<Customer> customers, IRepository<Order> orders)
{
public async Task RunAsync(CancellationToken ct)
{
var customer = await customers.FindAsync(42, ct);
var open = await orders.ListAsync(o => o.Status == OrderStatus.Open, ct);
orders.Add(new Order { CustomerId = customer!.Id });
}
}
DbContext remains the unit of work. Repositories only stage changes; the application decides when
to commit:
await dbContext.SaveChangesAsync(ct);
Repositories for entities belonging to the same context share that scope's DbContext instance, so a
single SaveChangesAsync commits work staged through any of them. With several contexts registered,
each one is its own unit of work and is saved separately.
Custom repositories
Declare an interface deriving from IRepository<T> and a class deriving from EfRepository<T>:
public interface IOrderRepository : IRepository<Order>
{
Task<Order?> FindByExternalIdAsync(string id);
}
public sealed class OrderRepository(AppDbContext db) : EfRepository<Order>(db), IOrderRepository
{
public Task<Order?> FindByExternalIdAsync(string id)
=> Set.SingleOrDefaultAsync(x => x.ExternalId == id);
}
It is discovered automatically. Afterwards IRepository<Order> and IOrderRepository resolve to the
same instance, so callers that only need the standard operations are unaffected:
public Handler(IRepository<Order> orders) { } // gets OrderRepository
public Handler(IOrderRepository orders) { } // same instance, richer contract
Custom repositories may override any generic operation; overrides apply through both contracts.
The constructor parameter is what names the context a repository belongs to, which is what lets two
repositories for the same entity target different contexts.
EfRepository<T> exposes protected DbSet<T> Set and protected DbContext Db, which is the
sanctioned escape hatch for paging, Include, projections, AsNoTracking, ExecuteUpdateAsync and
anything else deliberately absent from IRepository<T>.
What gets discovered
Entities come from the context's DbSet<T> properties, following the same rules EF Core itself uses
— including non-public ones. Custom repositories are found in the project that calls
AddRepositories and in every assembly it references. There is nothing to configure and no
assembly list to pass.
An entity with no DbSet — a derived type in a hierarchy, or one configured only through
OnModelCreating or an IEntityTypeConfiguration<T> — is not discovered, and IRepository<T> for it
will not resolve. Claim it explicitly:
builder.Services.AddRepository<AppDbContext, ArchivedOrder>();
Call
AddRepositoriesfrom the project that references your repositories. The generator runs where the call site is, and sees that project's references — not the projects that reference it. IfAddInfrastructure()in a class library callsAddRepositories<AppDbContext>()but the repositories live in the application, they are invisible.NCR0011warns when this happens.
Multiple DbContexts
builder.Services.AddDbContext<WriteDbContext>(...);
builder.Services.AddDbContext<ReadDbContext>(...);
builder.Services.AddRepositories<WriteDbContext>();
builder.Services.AddRepositories<ReadDbContext>();
Each context contributes the entities it exposes. When the same entity appears in both — a
read/write split — the first call site owns the unqualified IRepository<T> contract; ordering is
by file path then position, so it does not vary between machines or builds. The other context's
repositories stay reachable through their own interfaces (IOrderReadRepository), which is normally
what you want. AddRepository<TDbContext, TEntity>() overrides the choice, and NCR0008 reports it.
Build-time diagnostics
Misconfigurations that used to throw at startup are now build errors, reported on the offending declaration.
| ID | Severity | Meaning |
|---|---|---|
NCR0001 |
Error | A repository targets more than one entity |
NCR0002 |
Error | Two custom repositories target the same entity for one context |
NCR0003 |
Error | Repository dependency cycle, direct or multi-hop, via contract or concrete type |
NCR0004 |
Error | <InterceptorsNamespaces> does not list the generated namespace, so nothing would be registered |
NCR0008 |
Info | An entity is exposed by more than one DbContext |
NCR0011 |
Warning | A custom repository was found but nothing in this project registers it |
NCR0012 |
Error | The registration call cannot be intercepted |
Decorating a repository is rejected. A class implementing IRepository<T> whose constructor also
takes IRepository<T> is a decorator, not a repository, and would deadlock the container. NCR0003
fails the build instead. Register decorators yourself after AddRepositories. Depending on a
different entity's repository is fine.
Notes and limits
- C# only. The mechanism is a C# interceptor; F# and VB consumers are not supported.
- The call must be a direct call. Invoking
AddRepositoriesthrough a delegate or by reflection is not intercepted and throws, because substitution happens at the call site. - Targets
Microsoft.Extensions.DependencyInjection. Registrations you add yourself always win, in either call order. If you replaceIRepository<T>afterAddRepositories, any custom interface for that entity reports the conflict rather than handing back an incompatible instance. - Call
AddRepositoriesafterAddDbContext. It works the other way round too, but a context registered afterwards cannot be checked for the transient lifetime described below. - A transient
DbContextis rejected at startup, not at build time: the lifetime is a run-time argument toAddDbContextand cannot be read from source. - A singleton
DbContextis accepted. It is an EF Core anti-pattern for its own reasons, but it does not break the guarantee that repositories in a scope share one unit of work. - Cycles introduced by your own registrations cannot be seen at build time. Where the container can detect them it does; where an alias hides the edge, resolution throws a named error rather than hanging.
- Two
AddRepositoriescalls in different projects cannot be arbitrated at compile time. The first to execute still wins at run time, but no diagnostic is possible. - Keyless and owned types have no meaningful
FindAsync/Add/Removebehaviour, as with EF Core.
API
| Member | Purpose |
|---|---|
IRepository<T> |
FindAsync, FirstOrDefaultAsync, ListAsync, AnyAsync, CountAsync, Add(Range), Update(Range), Remove(Range) |
EfRepository<T> |
Default implementation and the base class for custom repositories |
IRepositoryFactory |
For<TEntity>(), for dynamic selection where constructor injection will not do |
AddRepositories<TDbContext>() |
Registration and discovery |
AddRepository<TDbContext, TEntity>() |
Claim an entity the context does not expose as a DbSet |
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.EntityFrameworkCore (>= 10.0.11)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.11)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|