PouryaKit
Concepts

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.Person or Actor.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:

OpenStockItemIntegrationEventHandler.cs
// 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:

ReconcileStockCommandHandler.cs
// 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

GuaranteeStated in
What a context holds, and how it starts and continues workThe Operation Context
How an HTTP caller becomes an Actor and a tenantThe caller and the Actor
What a trusted continuation may skip, and what still runsContinuations
A tenant-scoped Module runs nothing without a tenantDeclaring a tenant-scoped Module
How a test passes a context of its ownThe Operation Context in tests

Failure messages

Next steps

On this page