Skip to content

Validation with read models

Arc’s validation model — data annotations, ConceptValidator<T>, and CommandValidator<TCommand> — works unchanged when Chronicle is in the picture. What the integration adds is one thing: a validator can take the read model Chronicle projected for the command’s key as a constructor dependency, and validate against current state without a query.

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

Arc resolves LedgerBalance for the same event source id the command appends events to, then constructs the validator with it. The flow is:

  1. The command is bound, and its event source id is resolved — from [Key], from a property that converts to EventSourceId, or from ICanProvideEventSourceId. See Resolving EventSourceId, which contributes the value to the Command Context Values.
  2. The read model instance is loaded from Chronicle’s read model store by that id.
  3. Validators are constructed with it and their rules run — before Handle() is invoked.

Because the same command scope serves Provide() and Handle(), all three see the same instance.

  • A key does not prove existence. Declare the parameter nullable when a missing projection is a business condition, non-nullable when it is required. This is the central decision — see nullable versus required.
  • Read models are eventually consistent. They are the right input for gating on projected state, and the wrong one for an invariant that must hold under concurrent commands. Use a Chronicle constraint for those.
  • Validators need the Arc command pipeline. Read-model injection does not work through MVC controllers — see ReadModelValidatorRequiresCommandPipeline.