Elarion

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.

On this page