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.");
}

This validator fragment assumes the application’s SettleLedger command and LedgerBalance backing. Arc resolves the model using the command’s input-time event source id, then constructs the validator. A later returned identity cannot reload that dependency. The flow is:

  1. The command is bound, and its event source id is resolved — from [Key], from an EventSourceId or EventSourceId<T>-derived property, 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.
  • Freshness depends on backing. Materialized models can lag; passive models build state on demand. Neither snapshot alone guarantees an invariant 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.