Identity resolution · Part 1 of 2

How does your domain know your user?

Almost every business application needs to answer a question that sounds simple: who is the current user? But “user” isn’t one thing — it’s three different questions that should not be mixed into a single object.

Authorization needs to know what you’re allowed to do. Business rules need to know who you are. Audit trails need to know who did this. These are three different questions that should not be mixed into a single “user” object.

So it helps to split the concept into two abstractions.

Identity — who are you?

public interface IIdentity;

It might be a registered user:

public sealed record UserIdentity(UserId Id, PersonName Name, Email Email) : IIdentity;

the system itself — a background job, or a handler reacting to an event from another bounded context:

public sealed record SystemIdentity : IIdentity;

or anonymous:

public sealed record AnonymousIdentity : IIdentity;

Principal — what are you allowed to do?

public interface IPrincipal
{
    public bool HasRole(Role role);
    public bool HasPermission(Permission permission);
}

A registered user has roles and permissions granted within a company:

public sealed record UserPrincipal(CompanyId CompanyId, IReadOnlySet<Role> Roles, IReadOnlySet<Permission> Permissions)
    : IPrincipal
{
    public bool HasRole(Role role) => Roles.Contains(role);
    public bool HasPermission(Permission permission) => Permissions.Contains(permission);
}

The system can do anything:

public sealed record SystemPrincipal : IPrincipal
{
    public bool HasRole(Role role) => true;
    public bool HasPermission(Permission permission) => true;
}

and an anonymous caller can do nothing:

public sealed record AnonymousPrincipal : IPrincipal
{
    public bool HasRole(Role role) => false;
    public bool HasPermission(Permission permission) => false;
}

This is a simple model, but it’s a powerful starting point for writing business logic. The following example is a DeleteEmployee command handler that uses both abstractions:

public sealed class DeleteEmployeeCommandHandler(OrganizationsDbContext dbContext)
{
    public async Task HandleAsync(DeleteEmployeeCommand command, CompanyId companyId, IPrincipal principal,
        IIdentity identity, CancellationToken cancellationToken)
    {
        principal.Require(EmployeePermissions.Remove);

        var employee = await dbContext.Employees.LoadAsync(new EmployeeId(command.Id), companyId, cancellationToken);
        if (identity is UserIdentity currentUser && employee.UserId == currentUser.Id)
        {
            throw new ValidationException("You cannot remove yourself from the company.");
        }

        employee.Delete();
        await dbContext.SaveChangesAsync(cancellationToken);
    }
}

The principal guarantees the authorization check. The identity lets us enforce a business rule the principal can’t express — you can’t delete yourself from the company. Two questions, two abstractions, both answered cleanly inside the handler.

Notice that the handler simply receives IPrincipal and IIdentity as parameters. It doesn’t reach into HttpContext, doesn’t call a static accessor, doesn’t know where the user came from. That’s deliberate — and it’s also where the hard part begins.

Because the handler takes identity and principal for granted. Something, somewhere, has to actually produce them. And in a real system, the same handler can run in any of several contexts:

  • an HTTP request from a browser or API client,
  • a command or query dispatched through Wolverine,
  • a background job,
  • an asynchronous handler reacting to a domain event from another module.

Each of these knows about “the current user” in a completely different way — or doesn’t know at all.

It gets worse from there, in three specific ways.

Some requests have no user, by design. Public endpoints are anonymous on purpose. For those, resolving identity and principal isn’t just unnecessary — it’s wasted database work we want to skip entirely. So resolution has to be lazy: nothing should hit the database to figure out who you are unless someone actually asks.

Some callers must run as the system, with full rights. A background job operating on behalf of the platform isn’t a user at all. We need a way to explicitly say “this runs as the system” and have it resolve to full access.

And then there’s the constraint — module boundaries. Modules are isolated — the Booking module that needs to know the current principal can’t just reach into the Organizations module’s tables. Fine, we accept a query across the boundary. But if you’re using Wolverine with an outbox and automatic transactions, you hit another problem: Wolverine starts the transaction automatically, and it won’t know which DbContext to open it on if resolution touches a different module’s context mid-handler. You can tell Wolverine which context to use for the transaction, but there’s a cleaner way out — resolution simply cannot happen inside the command or query handler. It has to happen before the handler’s transaction is ever opened.

That last constraint is the one that shapes everything. It means the clean handler signature you saw above — IPrincipal principal, IIdentity identity as plain parameters — is only possible if some layer above the handler has already resolved them, lazily, with the right answer for whichever of the four contexts we happen to be in, and made them available without the handler knowing any of it happened.

So how do you build that layer?

That’s what the next part is about: the resolvers, the two kinds of laziness that make it cheap, and the Wolverine handler policies, ASP.NET Core auth policies, and job filters that wire it into every entry point — without a single handler ever knowing the difference.