Skip to content

Authorizing commands and queries

Goal: only certain users may perform an action or see certain data — “only a librarian can register an author,” “only an admin sees the audit list.” You want that enforced declaratively, not with if checks scattered through your logic.

Authorize at the boundary, not in the logic

Section titled “Authorize at the boundary, not in the logic”

Authorization is a cross-cutting concern: it belongs at the edge, applied as an attribute, so your Handle() methods and read models stay focused on behavior. Arc enforces role attributes on both commands and query methods.

Put [Roles(...)] on the [Command] record. Arc checks the caller’s roles before the command runs:

[Command]
[Roles(nameof(UserRole.Librarian))]
public record RegisterAuthor(AuthorId Id, AuthorName Name)
{
public Task Handle(IMongoCollection<Author> authors) =>
authors.InsertOneAsync(new Author(Id, Name));
}

Query methods on a read model take the same attribute, so the read side is gated too:

[ReadModel]
public record Author([property: Key] AuthorId Id, AuthorName Name)
{
[Roles(nameof(UserRole.Librarian))]
public static ISubject<IEnumerable<Author>> AllAuthors(IMongoCollection<Author> collection) =>
collection.Observe();
}

Roles come from the authenticated identity. Arc integrates with standard ASP.NET Core authentication and can enrich the identity with application-specific details (roles, tenant, preferences) through IProvideIdentityDetails. See the Identity section for setting that up, and for generating a principal during local development so you can exercise authorized endpoints without a full login.

  • Multi-tenancy narrows access further: combine roles with tenancy so a user only ever sees their tenant’s data.
  • The generated TypeScript proxies respect the same rules — an unauthorized call fails the same way it would from any client.
  • Identity — authentication, identity details, and local-dev principals.
  • Tenancy — isolating data per tenant.
  • Commands and Queries — the full model.