Skip to content

Use current state in a command

Goal: your command’s decision depends on what’s already true. Can this order be submitted? Is this name taken? What’s the current balance? The state you need is already projected into a read model — you just need it inside the command.

You don’t query for it. If the command carries a key, Arc has already resolved the read model for that key and will hand it to you as a parameter.

Without it, a command that needs current state has to go get it: inject a repository or IReadModels, resolve the key by hand, await a lookup, null-check the result. That’s four lines of plumbing before the first line of the actual decision — repeated in the validator and the handler, where the two can drift apart and answer differently.

Arc removes the fetch entirely. Declare the read model as a parameter and it arrives:

[Command]
public record SettleLedger(LedgerId LedgerId)
{
public LedgerSettled Handle(LedgerBalance balance) => new(balance.Balance);
}

LedgerBalance is a read model built from ledger events. Arc resolved it for LedgerId, and the validator for this same command gets the same instance — one fetch per command, shared.

The key the command already uses to append events is the key the read model is resolved by. Nothing extra to configure.

Key · EventSourceId · ICanProvideEventSourceId

found

never projected or removed

Command record

event source id

resolve by key

read model instance

null

CommandValidator

Provide method

Handle method

Two things follow from that shape, and both matter:

  • A key proves which instance, not that it exists. The projection may never have been created, or may have been removed. Resolution can legitimately yield nothing.
  • All three positions share one instance. The validator, Provide(), and Handle() resolve from the same command scope, so they cannot disagree about the state they’re looking at.
You want to…Put the read model inBecause
Reject the command with a messageCommandValidator<TCommand>Rules stay with the command’s other rules; the message reaches the UI as a validation error
Feed a value into the decisionHandle()The event you produce is computed from the state
Combine it with fetched data firstProvide()Provide acquires, Handle decides — see Provide data to a command handler

A validator constructor takes the read model like any other dependency:

public class SettleLedgerValidator : CommandValidator<SettleLedger>
{
public SettleLedgerValidator(LedgerBalance balance) =>
RuleFor(command => command.LedgerId)
.Must(_ => balance.Balance > 0)
.WithMessage("Ledger has no funds to settle.");
}

The command never reaches Handle(), and the message surfaces in the UI through the generated proxy like any other validation error.

When the state is an input to the event rather than a gate on it, take it in the handler:

[Command]
public record WithdrawFunds(AccountId AccountId, decimal Amount)
{
public FundsWithdrawn Handle(AccountBalance balance) =>
new(Amount, balance.Balance - Amount);
}

This is the one decision the framework can’t make for you, so make it deliberately: nullable means you handle absence, non-nullable means you require existence.

Nullable — absence is a normal business condition, and the rule is written around it:

public class RegisterCustomerValidator : CommandValidator<RegisterCustomer>
{
public RegisterCustomerValidator(Customer? customer) =>
RuleFor(_ => customer)
.Null()
.WithMessage("Customer is already registered");
}

Non-nullable — the projection is required, and its absence is a fault rather than an outcome. Arc fails the command with ReadModelDoesNotExistForCommand (HTTP 400) before your code runs, so you write the rule against the state directly:

public class SubmitOrderValidator : CommandValidator<SubmitOrder>
{
public SubmitOrderValidator(OrderReadModel order) =>
RuleFor(_ => order.Status)
.Equal(OrderStatus.ReadyForSubmission)
.WithMessage("Only orders that are ready for submission can be submitted");
}

The analyzer warns (ARC0006) on every non-nullable read model parameter — not because it’s wrong, but so the choice is a decision rather than an oversight.

Any read model Chronicle can resolve by key — which means one with a Chronicle backing artifact:

You write the parameter identically in all three cases — the backing artifact is an implementation detail. Note that the [ReadModel] attribute alone does not make a type injectable: it’s an Arc query concept and can be backed by stores that have no key resolution. Backing decides, not the attribute.

Seed the state the command should observe, then execute. Either state the events behind it:

void Establish() =>
_scenario.Given
.ForEventSource(_accountId)
.Events(new MoneyDeposited(100m), new MoneyDeposited(50m));

…or pin the instance when the events aren’t the point:

void Establish() =>
_scenario.Given
.ForEventSource(_accountId)
.ReadModel(new AccountBalance(150m));

An unseeded event source resolves to null, exactly as in production. See Testing with Chronicle for the full harness.