AndreGoepel.Marten.Identity
2.0.1
dotnet add package AndreGoepel.Marten.Identity --version 2.0.1
NuGet\Install-Package AndreGoepel.Marten.Identity -Version 2.0.1
<PackageReference Include="AndreGoepel.Marten.Identity" Version="2.0.1" />
<PackageVersion Include="AndreGoepel.Marten.Identity" Version="2.0.1" />
<PackageReference Include="AndreGoepel.Marten.Identity" />
paket add AndreGoepel.Marten.Identity --version 2.0.1
#r "nuget: AndreGoepel.Marten.Identity, 2.0.1"
#:package AndreGoepel.Marten.Identity@2.0.1
#addin nuget:?package=AndreGoepel.Marten.Identity&version=2.0.1
#tool nuget:?package=AndreGoepel.Marten.Identity&version=2.0.1
AndreGoepel.Marten.Identity
ASP.NET Core Identity stores backed by Marten (PostgreSQL). Provides event-sourced user and role stores, cookie login middleware, and DI extensions ready to drop into any ASP.NET Core application.
Requirements
- .NET 10
- Marten 8.x (
Marten.AspNetCore) - PostgreSQL
Installation
dotnet add package AndreGoepel.Marten.Identity
Usage
1. Configure Marten and wire up Identity stores
builder.Services.AddMartenIdentity();
builder.Services.AddMarten(options =>
{
options.Connection(connectionString);
options.InitializeIdentity(); // registers user, role, and user-role projections
options.AutoCreateSchemaObjects = AutoCreate.All;
})
.IntegrateWithWolverine(); // optional — only needed when using Wolverine
AddMartenIdentity accepts an optional Action<IdentityOptions> to customise password rules, lockout policy, etc.
2. Add middleware
Call UseMartenIdentityMiddleware() after UseAuthentication() / UseAuthorization():
app.UseAuthentication();
app.UseAuthorization();
app.UseMartenIdentityMiddleware();
This registers two middleware components:
| Middleware | Purpose |
|---|---|
SetupRedirectMiddleware |
Redirects to /Setup until an administrator exists, then blocks /Setup |
CookieLoginMiddleware |
Exchanges a one-time login key for an authentication cookie (used by the setup flow) |
First-run setup security
Before the first administrator exists, /Setup is reachable without
authentication — that is unavoidable, since no operator credentials exist yet.
Whoever completes setup first becomes the un-deletable root administrator, so a
freshly deployed instance is exposed until setup is finished. To close the
"race to setup" window:
- Do not expose a new instance to untrusted networks before completing setup. Provision behind a firewall / private network, finish setup, then open it up.
- Treat
SetupRedirectMiddlewareas the authoritative gate. Once an administrator holds theAdministratorrole, the middleware redirects/Setupaway unconditionally, so it cannot be re-run to mint a second root admin. Setup completion now requires a user that actually holds the role — not merely that the role and some user both exist. - For internet-facing deployments, gate your host's
/Setuppage with an out-of-band bootstrap secret (e.g. an environment variable the operator must supply) so an attacker cannot claim the first admin even if they reach the instance first. - The
/Setupredirect uses request headers to detect page navigations; that heuristic is a UX convenience, not a security boundary. Keep[Authorize]on every administrative page — never rely on the redirect to protect them.
What's included
| Namespace | Contents |
|---|---|
AndreGoepel.Marten.Identity.Users |
User, UserId, UserStore, event-sourced UserProjection, passkey support |
AndreGoepel.Marten.Identity.Roles |
Role, RoleId, RoleStore, event-sourced RoleProjection, built-in Roles constants |
AndreGoepel.Marten.Identity.UserRoles |
UserRoleAssignment projection for efficient role queries |
AndreGoepel.Marten.Identity.Http |
SetupRedirectMiddleware, CookieLoginMiddleware |
AndreGoepel.Marten.Identity.Services |
ICurrentUserService / CurrentUserService |
User events (event sourcing)
UserStore persists user changes as fine-grained events on the user's Marten
event stream — one event per changed field, instead of a single full-state
event (since v2.0.0, #138):
| Event | Fired when |
|---|---|
UserCreated |
Account created |
EmailChanged |
Email address changed |
EmailConfirmationChanged |
Confirmation status changed without the address itself changing (e.g. a confirm-link click) |
UserNameChanged |
Username changed |
PhoneNumberChanged |
Phone number changed |
PasswordChanged |
Password hash changed |
SecurityStampRotated |
Security stamp rotated (invalidates previously issued auth cookies) |
TwoFactorChanged |
2FA enabled/disabled, or the authenticator key/recovery codes rotated |
LockedOut |
Account locked out (lockout end date set) |
LockoutCleared |
Lockout cleared |
AccessFailedCountChanged |
Failed-login counter changed |
LockoutEnablementChanged |
Whether the account participates in lockout changed |
DeletabilityChanged |
Whether the account may be deleted changed |
UserDeleted / UserRestored |
Account soft-deleted / restored |
Breaking change in v2.0.0 (runtime, not compile-time).
UserStoreno longer appendsUserUpdated. If you subscribe to identity events withIEventFilterable.IncludeType<UserUpdated>(), switch to the fine-grained types above — the old filter stops receiving events silently: no compiler error, no exception.UserUpdatedstays public and keeps replaying existing event streams unchanged, so no migration or projection rebuild is needed — only subscribers filtering by event type are affected.
Migrating a Wolverine/Marten subscription off UserUpdated
If you have a subscription like this:
public class MySubscription : IEventFilterable
{
public bool TimedOut { get; set; }
public void Filter(IEventFilterable filterable) => filterable.IncludeType<UserUpdated>();
}
Add the fine-grained type(s) you actually care about, alongside
UserUpdatedrather than instead of it, so streams written before the upgrade keep flowing:public void Filter(IEventFilterable f) { f.IncludeType<EmailChanged>(); f.IncludeType<UserUpdated>(); // legacy streams only — drop once none are relevant }Split your event handler between the two types — the fine-grained event only carries the one field it names (e.g.
EmailChanged.Email), not the full user snapshotUserUpdatedused to carry.Do not bump the subscription's version just for this change. A version bump triggers Wolverine/Marten to rewind and replay the subscription's entire history, which is usually unnecessary churn for a purely additive filter change — reserve version bumps for changes that need history reprocessed with new logic.
Once no stream you care about predates the upgrade, drop the
UserUpdatedbranch entirely.
License
MIT
| 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
- AndreGoepel.Core (>= 1.0.2)
- AndreGoepel.Marten.Configuration (>= 1.2.1)
- AndreGoepel.Marten.Identity.Abstractions (>= 2.0.1)
- Marten (>= 9.29.0)
- Marten.AspNetCore (>= 9.29.0)
- Quartz.Extensions.Hosting (>= 3.19.1)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on AndreGoepel.Marten.Identity:
| Package | Downloads |
|---|---|
|
AndreGoepel.Marten.Identity.Blazor
Blazor Server UI components for AndreGoepel.Marten.Identity — login, registration, 2FA, passkeys, and user/role administration pages built with Radzen. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.1 | 308 | 8/21/2026 |
| 2.0.0 | 94 | 8/19/2026 |
| 1.9.3 | 249 | 8/14/2026 |
| 1.9.2 | 95 | 8/14/2026 |
| 1.9.1 | 206 | 8/1/2026 |
| 1.9.0 | 214 | 7/27/2026 |
| 1.8.1 | 397 | 7/25/2026 |
| 1.8.0 | 334 | 7/25/2026 |
| 1.7.0 | 197 | 7/24/2026 |
| 1.6.0 | 201 | 7/23/2026 |
| 1.5.0 | 401 | 7/21/2026 |
| 1.4.0 | 174 | 7/19/2026 |
| 1.3.4 | 150 | 7/18/2026 |
| 1.3.3 | 185 | 7/18/2026 |
| 1.3.2 | 128 | 7/17/2026 |
| 1.3.1 | 401 | 7/12/2026 |
| 1.3.0 | 253 | 7/8/2026 |
| 1.2.0 | 131 | 7/5/2026 |
| 1.2.0-preview1 | 124 | 7/4/2026 |