ASP.NET Core Identity
Optional ASP.NET Core Identity integration that composes onto a plain DbContext — no IdentityDbContext inheritance, snake_case-ready.
Elarion's ASP.NET Core Identity support is split across two optional packages:
Elarion.EntityFrameworkCore.Identity— the web-free Identity model: the[GenerateElarionIdentity]marker and theApplyElarionIdentityhelper. It depends only on EF Core + the Identity stores package, so the project that owns yourDbContextcan model Identity without referencing the ASP.NET shared framework.Elarion.AspNetCore.Identity— the host wiring:AddElarionIdentity, the IdentityICurrentUserclaim mapping, and the transport-neutral authorizer. This one pulls in ASP.NET Core.
Identity is one authentication provider among many — authorization itself
does not depend on either package. The distinguishing trait is composition over inheritance: your
application DbContext stays a plain DbContext and composes the Identity model, rather than deriving from
IdentityDbContext.
In a layered solution, reference Elarion.EntityFrameworkCore.Identity from the persistence/application
project (where the DbContext and [GenerateElarionIdentity] live) and Elarion.AspNetCore.Identity from
the web host (where AddElarionIdentity runs). The data layer composes the Identity model with no
web-framework dependency. In a single-project host, reference both.
Wiring
// Program.cs
builder.Services.AddElarionIdentity<ApplicationUser, ApplicationRole, AppDbContext>();
builder.Services.AddDbContext<AppDbContext>(o => o.UseNpgsql(connectionString));AddElarionIdentity<TUser, TRole, TDbContext>:
- registers
AddIdentity<TUser, TRole>()+AddEntityFrameworkStores<TDbContext>()(+ default token providers), - maps
ICurrentUserto the Identity claim types (NameIdentifier/Email/Role, overridable), and - registers the transport-neutral authorizer via
AddElarionAuthorization, so handler[Require*]attributes are enforced.
Cookie/authentication policy (ConfigureApplicationCookie, login endpoints, UseAuthentication,
UseElarionCurrentUser) stays the host's job — the package wires Identity and authorization, not your sign-in UX.
The context composes Identity
AddEntityFrameworkStores only needs the Identity entities mapped — it does not require
IdentityDbContext. Annotate a plain [GenerateDbSets] context (the [GenerateElarionIdentity] marker and
its generator come from Elarion.EntityFrameworkCore.Identity, so this assembly needs no web dependency):
[GenerateDbSets]
[GenerateElarionIdentity<ApplicationUser, ApplicationRole, Guid>(SnakeCase = true)]
public sealed partial class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options) {
protected override void OnModelCreating(ModelBuilder modelBuilder) => ConfigureEntities(modelBuilder);
}
public sealed class ApplicationUser : IdentityUser<Guid>;
public sealed class ApplicationRole : IdentityRole<Guid>;The bundled generator emits the seven Identity DbSets (db.Users, db.Roles, db.UserClaims, …) and
applies the full Identity model configuration — keys, composite keys, unique normalized indexes,
relationships, max-lengths — through the EF generator's per-feature model-configuration seam (one seam per
[GenerateElarion…] feature, so Identity composes with resource grants, idempotency keys, and any other feature
on the same context). The neutral OnEntitiesConfigured seam stays reserved for your hand-written model
configuration — implement it for a global query filter or extra mapping without colliding with Identity. So the
single ConfigureEntities(modelBuilder) call configures both your domain entities (from
[EntityConfiguration]) and Identity, with no <TUser, TRole, TKey> repeated at the call site and no
IdentityDbContext in the hierarchy. (Requires [GenerateDbSets] on the same context, else ELIDN001.)
snake_case
SnakeCase = true (the default) makes the generated Identity model use snake_case table, column, and
index names directly — users, roles, user_claims, normalized_user_name,
ix_users_normalized_user_name, … — instead of the ASP.NET AspNet* PascalCase defaults. It is
self-contained: no EFCore.NamingConventions dependency, and the names match what that convention would
produce, so an app already using it for domain tables sees a consistent schema. Set SnakeCase = false to
keep the Identity defaults.
Two more knobs place and name the tables: Schema puts all seven Identity tables in a non-default schema,
and TablePrefix prepends a verbatim prefix to every Identity table name (e.g. TablePrefix = "auth_" →
auth_users, auth_roles, …). Because Identity spans seven tables, the prefix is the table-name
override — there are no per-table name parameters — and the snake_case index names incorporate the prefixed
table name. The underlying primitive,
modelBuilder.ApplyElarionIdentity<TUser, TRole, TKey>(schema, tablePrefix, snakeCase)
(also in Elarion.EntityFrameworkCore.Identity), is public for advanced hosts that prefer to call it directly.
What stays in your app
The package is deliberately minimal. Role→permission policy, seeding, sign-in/registration endpoints, and the
user-management surface stay app-owned — they are domain decisions, not framework mechanics. Permissions are
typically issued as a permission claim (the default
AuthorizationOptions.PermissionClaimType) on roles, and enforced on handlers
with [RequirePermission]. You no longer hand-maintain the list of permissions, though: inject
IPermissionCatalog to enumerate every [RequirePermission]/[RequireRole]
across your modules when seeding:
// Seeding: every [RequirePermission] across enabled modules, no hand-kept Permissions.All list.
public sealed class RoleSeeder(IPermissionCatalog catalog, RoleManager<ApplicationRole> roles) {
public async Task SeedAdminAsync(ApplicationRole adminRole) {
foreach (var permission in catalog.Permissions) { // "{resource}.{verb}", e.g. "clients.read"
await roles.AddClaimAsync(adminRole, new Claim("permission", permission));
}
}
}See also
- Authorization — the attributes and the provider-neutral decision path.
- Current user — the
ICurrentUserabstraction Identity populates. - Why Elarion → Composition over inheritance.