Operation Context, Actor and tenant
Every Operation carries who caused it, the tenant it runs for and its chain of work, passed explicitly and never read from an ambient request.
What it is
The Operation Context is the facts about who and what caused an Operation, available wherever the Operation runs. In code it is OperationContext, plain data that names:
- the Actor, the person or system on whose authority the Operation runs, made with
Actor.PersonorActor.System; - the tenant, the customer or company the Operation runs for, or none where the application has no tenants;
- the Operation's own id, and the correlation and causation that place it in a chain of work.
There is no anonymous or default Actor
Every Operation names its Actor. Work no person caused runs as a system Actor with a name of its own. A tenant comes only from a verified claim, never from a host name or a route.
When it matters
- You start work: create a new context with its Actor and tenant and pass it to the Gateway, one context per Operation.
- You continue work an Operation caused: call
Continue()on its context. The Actor, tenant and correlation carry over, and the new Operation records the old one as its cause. - You read who or which tenant in a handler, an authorizer, a validator or a Domain Event Handler: inject
OperationContext.
At the HTTP boundary you create nothing: the Web Capability builds each request's context from the authenticated caller, and the endpoint passes it to the Gateway. The Kit restores the context of each Notification, Integration Event, Snapshot and delayed command it delivers, so its handler runs as a continuation of the Operation that caused it.
How it works
Inject the context and read it. This Integration Event Handler, from the Proving Sample's Inventory Module, opens a stock item for the tenant the delivery carries:
// Starts keeping stock of every product Catalog creates, for the tenant the event was delivered for, which GetStockItem
// answers. Only the effect: the Kit records the Receipt, commits and acknowledges, so a redelivered or concurrently
// duplicated ProductCreatedV1 opens one stock item.
internal sealed class OpenStockItemIntegrationEventHandler(
IRepository<StockItem, StockItemId> stockItems,
OperationContext context
) : IIntegrationEventHandler<ProductCreatedV1>
{
public async Task<Result> Handle(
ProductCreatedV1 integrationEvent,
CancellationToken cancellationToken
)
{
await stockItems.AddAsync(
StockItem.Open(integrationEvent.ProductId, integrationEvent.OccurredOn, context.Tenant),
cancellationToken
);
return Result.Success();
}
}Recurring work no person asks for runs as the system Actor its declaration names. Inventory's reconciliation records that Actor with each run:
// Records, at each occurrence, how many stock items Inventory keeps, and on whose authority: the system Actor the
// Composition Root declared, since no person asked for it. Only the effect: the Kit records the occurrence's Receipt,
// stores the next occurrence and commits, so an occurrence delivered twice records one reconciliation.
internal sealed class ReconcileStockCommandHandler(
IUnitOfWorkConnection unitOfWork,
OperationContext context
) : CommandHandler<ReconcileStockCommand>
{
public override async Task<Result> Handle(
ReconcileStockCommand command,
CancellationToken cancellationToken
)
{
var (connection, transaction) = unitOfWork.Join();
await connection.ExecuteAsync(
new CommandDefinition(
"insert into inventory.stock_reconciliations (occurrence, stock_items, reconciled_by) select @Occurrence, count(*), @ReconciledBy from inventory.stock_items",
new { command.Occurrence, ReconciledBy = context.Actor.Id },
transaction,
cancellationToken: cancellationToken
)
);
return Result.Success();
}
}A continuation the Kit restores for a delivery, or one a running, authorized Operation gives with Continue(), is a trusted continuation: its IsTrustedContinuation is true, and a permission its command or query declares is not checked again. A context your code builds is never trusted, whatever causation you give it. Recognise a trusted continuation by that flag, never by CausationId.
What the Kit guarantees
| Guarantee | Stated in |
|---|---|
| What a context holds, and how it starts and continues work | The Operation Context |
| How an HTTP caller becomes an Actor and a tenant | The caller and the Actor |
| What a trusted continuation may skip, and what still runs | Continuations |
| A tenant-scoped Module runs nothing without a tenant | Declaring a tenant-scoped Module |
| How a test passes a context of its own | The Operation Context in tests |