Complexity: ⭐⭐ (Intermediate) | Time to Learn: 30–45 minutes
Demonstrates the same authorization rules enforced two ways — with and without CQRS — using Trellis.Authorization and Trellis.Mediator.
Actor— how to represent users with permissionsIAuthorize— static permission checks (AND logic)IAuthorizeResource<T>— resource-based authorization (owner checks with loaded resource)- Direct vs. Pipeline — same domain rules, different execution models
A document management system where:
- Any user can create documents
- Only the owner (or users with
Documents.EditAny) can edit - Only users with
Documents.Publishcan publish - Three actors: Alice (owner), Bob (no permissions), Charlie (admin)
cd Examples/AuthorizationExample
dotnet runBoth parts produce identical authorization outcomes:
Alice creates 'Design Doc' → ✅ Success
Alice edits her document → ✅ Success
Bob tries to edit Alice's document → ❌ Only the owner can edit this document
Charlie edits Alice's document → ✅ Success
Bob tries to publish → ❌ Missing required permission: Documents.Publish
Alice publishes her document → ✅ Published
Uses only Trellis.Authorization — no Mediator dependency. Authorization logic is mixed into each service method:
public Result<Document> EditDocument(Actor actor, string documentId, string newContent)
{
var doc = _store.Get(documentId);
// ⚠️ Auth logic mixed with business logic
if (actor.Id != doc.OwnerId && !actor.HasPermission("Documents.EditAny"))
return Result.Failure<Document>(Error.Forbidden("Only the owner can edit"));
var updated = doc with { Content = newContent };
_store.Update(updated);
return Result.Success(updated);
}Authorization is declared on commands and enforced by pipeline behaviors. Handlers contain only business logic:
// Command declares its auth requirements
public sealed record EditDocumentCommand(string DocumentId, string NewContent)
: ICommand<Result<Document>>, IAuthorizeResource<Document>
{
public IResult Authorize(Actor actor, Document document) =>
actor.Id == document.OwnerId || actor.HasPermission("Documents.EditAny")
? Result.Success()
: Result.Failure(Error.Forbidden("Only the owner can edit"));
}
// Handler has ZERO auth code
public sealed class EditDocumentHandler(DocumentStore store)
: ICommandHandler<EditDocumentCommand, Result<Document>>
{
public ValueTask<Result<Document>> Handle(
EditDocumentCommand command, CancellationToken cancellationToken)
{
var doc = store.Get(command.DocumentId);
var updated = doc! with { Content = command.NewContent };
store.Update(updated);
return new ValueTask<Result<Document>>(Result.Success(updated));
}
}The authorization rules are identical. The difference is where they execute:
| Approach | Auth lives in | Handlers contain auth? | Dependency |
|---|---|---|---|
| Direct Service | Service methods | Yes — mixed with business logic | Trellis.Authorization only |
| Mediator Pipeline | Command declarations + behaviors | No — zero auth code | Trellis.Mediator |
Actors.cs— Test actors with varying permissionsDocument.cs— Simple document record and in-memory storeDirectServiceExample.cs— Part 1: Manual authorization in service methodsMediatorExample.cs— Part 2: Declarative authorization with pipeline behaviorsProgram.cs— Runs both parts and compares results