Entity Framework Core
Optional source generation for DbSet properties and entity configuration application — interface-first, explicit, and AOT-friendly.
The EF Core package is optional. Use it when you want the same compile-time, explicit-convention
style for persistence wiring that the rest of Elarion uses — generated DbSet<T> properties and
direct IEntityTypeConfiguration<T> calls instead of reflection-based assembly scanning.
Its main purpose is to give you a generated application-context interface (IAppDbContext) that
handlers depend on directly. That interface is your data-access abstraction: application code in the
application layer queries DbSets and calls SaveChangesAsync without referencing the infrastructure
project that owns the concrete DbContext.
This is the recommended alternative to a per-entity repository layer. With full LINQ, projections,
and EF Core change tracking already available through IAppDbContext, a hand-written
IClientRepository/IInvoiceRepository per table usually adds indirection without adding value.
Introduce a dedicated abstraction only when a query genuinely needs to be reused or hidden behind a
domain operation. See Handlers → Accessing data.
Setup
Reference the Elarion.EntityFrameworkCore package — it bundles the EF Core source generator — in
every project that declares [DbEntity] entities or a [GenerateDbSets] context abstraction. If
your entities live in a separate domain project, that project needs this reference too, so its
generator emits the per-assembly entity manifest the context project reads:
<ItemGroup>
<PackageReference Include="Elarion.EntityFrameworkCore" Version="0.1.0" />
</ItemGroup>Marking entities
Entities opt in explicitly with [DbEntity]:
using Elarion.EntityFrameworkCore;
namespace MyApp.Domain.Entities;
[DbEntity]
public sealed class Invoice {
public Guid Id { get; set; }
}Generating DbSets
Annotate an application-level context interface with [GenerateDbSets]. This is the single source
of truth for generated DbSets:
using Microsoft.EntityFrameworkCore;
using Elarion.EntityFrameworkCore;
namespace MyApp.Application;
[GenerateDbSets]
public partial interface IAppDbContext {
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
DbContext AsDbContext();
}The generator emits DbSet<T> members into the interface and matching concrete members into any
partial DbContext class that implements it:
using Microsoft.EntityFrameworkCore;
using MyApp.Application;
namespace MyApp.Infrastructure.Data;
public sealed partial class AppDbContext(DbContextOptions<AppDbContext> options)
: DbContext(options), IAppDbContext {
public DbContext AsDbContext() => this;
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
ConfigureEntities(modelBuilder); // generated
}
}Do not add [GenerateDbSets] to the concrete context. Class generation is inferred from the
generated interface so applications have one declaration point.
Multiple contexts with scopes
For multiple contexts, use string-constant scopes so each context only sees the entities it owns:
public static class PersistenceScopes {
public const string Main = "main";
public const string AiAgent = "ai-agent";
}
[DbEntity(PersistenceScopes.Main)] public sealed class Invoice { /* ... */ }
[DbEntity(PersistenceScopes.AiAgent)] public sealed class ChatSession { /* ... */ }
[DbEntity(PersistenceScopes.Main, PersistenceScopes.AiAgent)] public sealed class User { /* ... */ }
[GenerateDbSets(PersistenceScopes.Main)] public partial interface IMainDbContext;
[GenerateDbSets(PersistenceScopes.AiAgent)] public partial interface IAiAgentDbContext;Scope behavior:
[GenerateDbSets]without scopes keeps global behavior and includes every[DbEntity].[GenerateDbSets("scope")]includes only entities whose[DbEntity(...)]scopes intersect.[DbEntity]without scopes participates only in unscoped/global generated interfaces.- Shared entities can list multiple scopes.
Entity configuration
The generator discovers IEntityTypeConfiguration<T> implementations in the current and referenced
assemblies and emits direct calls, equivalent to:
new InvoiceConfiguration().Configure(modelBuilder.Entity<Invoice>());Unscoped interfaces apply every discovered configuration, including schema-only configuration types
that are not exposed as DbSet<T>. Scoped interfaces filter configurations to the selected
[DbEntity] set, so unrelated scoped contexts do not configure each other's entities.
This avoids ApplyConfigurationsFromAssembly(...) reflection scanning and keeps model wiring
inspectable and AOT-friendly. The trade-off is explicit participation: entities must opt in with
[DbEntity], and scoped contexts require matching scopes.
Blob entities are not part of [DbEntity] / [GenerateDbSets]. A provider such as
Elarion.Blobs.PostgreSql configures its tables directly in your OnModelCreating (via
UsePostgreSqlBlobStorage()), so they join your context's model without opting into entity
generation — see Blob storage.
Pagination
List handlers page against IAppDbContext and return the shared, transport-neutral Page<T>
envelope (keyset cursors or offset totals). The keyset definition generator ([Keyset<TEntity>])
ships in the EF Core generators package alongside the DbSet generation, but the runtime helpers,
contracts, and the row-value seek note all live with the rest of pagination.