Skip to content

Provide data to a command handler

Goal: your command’s Handle() needs data it doesn’t carry — usually data from another service, such as a credit bureau, pricing service, inventory lookup, risk model, or tenant configuration — and you don’t want that IO tangled into the decision logic, where it’s hard to test.

Add Provide() next to Handle() on C#, or provide() next to handle() on JVM and TypeScript. Provide fetches or computes the data the decision needs, and its return value is passed into Handle() as an argument. Handle can then be a pure function of its arguments. To separate the write side as well on the C# backend, return a command operation describing the required service work, or return events with the optional Chronicle integration. Provide does not make persistence automatic.

Provide runs before Handle, after validation and authorization pass. Its parameters are resolved from dependency injection just like Handle’s, and this is the command, so it can read the command’s own data.

The following are alternative domain fragments, not a complete loan system. LoanId, ApplicantId, CreditScore, RiskBand, LoanAssessment, and the bureau/risk/rates interfaces are application-owned types. Supply their declarations and DI implementations; import Cratis.Arc.Commands.ModelBound, Cratis.Arc.Validation, and Cratis.Monads for the Arc attributes and result examples. LoanAssessment is an ordinary response DTO, not a persisted approval or Chronicle event.

[Command]
public record AssessLoan(LoanId LoanId, ApplicantId Applicant)
{
public CreditScore Provide(ICreditBureau bureau) => bureau.GetScore(Applicant);
public LoanAssessment Handle(CreditScore creditScore) => new(LoanId, creditScore);
}

View C# snippet source on GitHub

Because the IO lives in Provide, testing the decision needs no mocking — construct the command and call the handler directly with the values it would have received:

[Fact] void should_return_the_assessment() =>
new AssessLoan(LoanId.New(), ApplicantId.New())
.Handle(new CreditScore(800))
.ShouldBeOfExactType<LoanAssessment>();

View C# snippet source on GitHub

Provide has a single job — acquire data — so it is easy to test in isolation too, with a stubbed credit bureau.

On C#, return a tuple; on Kotlin, return a Pair to feed several typed handler parameters. On TypeScript, return tuple(a, b) from provide() and mark each handle() parameter with @inject(provided(TypeA), provided(TypeB)); Arc matches the values by runtime type. Returning a single object for handle() to destructure also works:

public (CreditScore, RiskBand) Provide(ICreditBureau bureau, IRiskModel risk) =>
(bureau.GetScore(Applicant), risk.Band(Applicant));
public LoanAssessment Handle(CreditScore score, RiskBand band) => new(LoanId, score, band);

View C# snippet source on GitHub

In C#, Provide may be synchronous or async (Task<T> / ValueTask<T>). On the JVM it may be ordinary, suspend, or return a CompletionStage, and a Triple or CommandProvidedValues carries more than two values.

[Command]
public record AssessLoan(LoanId LoanId, ApplicantId Applicant)
{
public Task<CreditScore> Provide(ICreditBureau bureau, CancellationToken cancellationToken) =>
bureau.GetScore(Applicant, cancellationToken);
public Task<LoanAssessment> Handle(CreditScore creditScore, CancellationToken cancellationToken) =>
Task.FromResult(new LoanAssessment(LoanId, creditScore));
}

View C# snippet source on GitHub

Cancellation differs by backend. In C#, Provide and Handle can each take a CancellationToken and Arc supplies the current command execution token — the request-aborted token for HTTP endpoints, or the token you pass to ICommandPipeline. You do not register CancellationToken in the service container; Arc treats it as part of the command execution context.

TypeScript has no CancellationToken; inject the request’s AbortSignal with @inject(abortSignal()) on provide() or handle(), or read currentContext()?.signal, and pass it to cancellable work. See model-bound commands. On the JVM there is no token parameter at all: a suspend provide or handle already runs inside Arc’s command coroutine, so cancelling the request cancels everything it awaits. Check ensureActive() when a long computation has no suspension point of its own.

Provide can stop the command before Handle is ever called. The table below shows C# return shapes; TypeScript returns rejected(validation(...)) from provide() for a typed failure, as its tab demonstrates.

Return / doResult
ValidationResult.Error("…")command fails validation (HTTP 400)
an AuthorizationResult that is not authorizedcommand is unauthorized (HTTP 403)
throwcommand fails with the exception (HTTP 500)
a valueflows into Handle as an argument

Use Result<,> to reject or proceed when the data acquisition itself determines whether the command can continue — return the error to stop, or the value to continue:

public Result<CreditScore, ValidationResult> Provide(ICreditBureau bureau)
{
var score = bureau.GetScore(Applicant);
return score is null
? ValidationResult.Error("No credit history", [nameof(Applicant)])
: score;
}

View C# snippet source on GitHub

C# states the alternative in the return type with Result<,>. Arc.Kotlin recognizes the control value at runtime instead: a provide declared to return Any may hand back either the value the handler consumes or a ValidationResult, AuthorizationResult, or CommandResult, and Arc short-circuits on the control value. TypeScript uses an Outcome such as rejected(validation(...)) instead of the C# Result<,> shape.

A provided value that no handler parameter consumes is almost always a mistake. C#‘s ARC0005 analyzer flags it; TypeScript’s arc-core/arc0005 ESLint rule flags a direct provide() return when handle() takes no parameters.

For ordinary command validation, including read-model existence checks, prefer a CommandValidator<>. Use Provide() when Handle() needs acquired data as an input to the decision.

Provider-owned state can already be available

Section titled “Provider-owned state can already be available”

With a configured by-key read-model provider and a usable command key, Provide need not fetch that model again. Arc resolves it as a parameter alongside the services it uses — handy when existing state determines what to fetch. Without those prerequisites, inject your application service or context and fetch explicitly:

public Task<ShippingQuote> Provide(OrderReadModel? order, IShippingRates rates) =>
order is null
? Task.FromResult(ShippingQuote.None)
: rates.Quote(order.Destination, order.TotalWeight);

View C# snippet source on GitHub

See Use current state in a command for how that resolution works and what a nullable parameter means.

Provide is per-command. For a value that many handlers need — the current tenant’s settings, say — register it as a scoped service and take it as a Handle parameter. Each backend resolves declared handler or provider dependencies from its service container; on TypeScript, use @inject(...) or generated metadata for erased parameter bindings.

If Handle already has everything it needs from the command and its injected services, you don’t need Provide. Reach for it specifically when a decision depends on data you must fetch — most often external or application-service data — where moving the fetch out buys you the testability.