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.
Lift the IO out of Handle
Section titled “Lift the IO out of Handle”Add a Provide() method next to Handle() on the command. Provide fetches or computes the data the decision needs, and its return value is passed into Handle() as an argument. Handle is left as a pure function of its arguments — given its inputs it returns events or results, with no IO to set up.
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.
[Command]public record ApproveLoan(LoanId LoanId, ApplicantId Applicant){ public CreditScore Provide(ICreditBureau bureau) => bureau.GetScore(Applicant);
public LoanApproved Handle(CreditScore creditScore) => new(LoanId, creditScore);}Why this matters: testability
Section titled “Why this matters: testability”Because the IO lives in Provide, testing the decision needs no mocking — construct the command and call Handle directly with the values it would have received:
[Fact] void should_produce_the_event() => new ApproveLoan(LoanId.New(), ApplicantId.New()) .Handle(new CreditScore(800)) .ShouldBeOfExactType<LoanApproved>();Provide has a single job — acquire data — so it is easy to test in isolation too, with a stubbed ICreditBureau.
Return more than one value
Section titled “Return more than one value”Return a tuple to feed several Handle parameters; each value is matched to a parameter by type:
public (CreditScore, RiskBand) Provide(ICreditBureau bureau, IRiskModel risk) => (bureau.GetScore(Applicant), risk.Band(Applicant));
public LoanApproved Handle(CreditScore score, RiskBand band) => new(LoanId, score, band);Provide may be synchronous or async (Task<T> / ValueTask<T>).
Use cancellation when the work can stop
Section titled “Use cancellation when the work can stop”Provide and Handle can both take a CancellationToken. Arc supplies the current command execution token. For HTTP command endpoints, that is the request-aborted token. For direct ICommandPipeline execution, pass the token to the pipeline call.
[Command]public record ApproveLoan(LoanId LoanId, ApplicantId Applicant){ public Task<CreditScore> Provide(ICreditBureau bureau, CancellationToken cancellationToken) => bureau.GetScore(Applicant, cancellationToken);
public Task<LoanApproved> Handle(CreditScore creditScore, CancellationToken cancellationToken) => Task.FromResult(new LoanApproved(LoanId, creditScore));}Use the token for IO or long-running work. You do not register CancellationToken in the service container; Arc treats it as part of the command execution context.
Short-circuit before Handle runs
Section titled “Short-circuit before Handle runs”Provide can stop the command before Handle is ever called:
| Return / do | Result |
|---|---|
ValidationResult.Error("…") | command fails validation (HTTP 400) |
an AuthorizationResult that is not authorized | command is unauthorized (HTTP 403) |
| throw | command fails with the exception (HTTP 500) |
| a value | flows 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;}A Provide value that no Handle parameter consumes is almost always a mistake, so the analyzer flags it (ARC0005).
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.
Cross-cutting values
Section titled “Cross-cutting values”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. Arc resolves any Handle or Provide parameter it can’t otherwise supply from the container.
When not to use it
Section titled “When not to use it”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.
See also
Section titled “See also”- Return a result or an error — the shapes
Handle()itself can return. - Test a command — testing a slice through the real pipeline.
- Validate a command — where
ValidationResult.Errorcomes from.