Validate a command
Goal: stop bad input from ever becoming an event. A blank name, a negative quantity, a duplicate email — you want the command rejected, with a clear reason, before Handle() runs.
Validation runs before the handler
Section titled “Validation runs before the handler”Arc runs validators before it invokes Handle(). A command that fails validation never appends anything and returns a CommandResult carrying the errors — and because the rules are extracted into the generated proxy, they also run on the client for instant feedback. There are three places a rule can live; reach for the narrowest one that fits.
-
A rule that’s true of a value everywhere → validate the value type. Write a
ConceptValidator<T>and it applies to every command carrying that concept:public class AuthorNameValidator : ConceptValidator<AuthorName>{public AuthorNameValidator() =>RuleFor(x => x.Value).NotEmpty().WithMessage("An author needs a name.");} -
A rule specific to one command → validate the command. Use a
CommandValidator<TCommand>(FluentValidation) for cross-field or command-only rules:public class RegisterAuthorValidator : CommandValidator<RegisterAuthor>{public RegisterAuthorValidator() =>RuleFor(c => c.Name).NotEmpty().MaximumLength(200);}For lightweight cases, data annotations like
[Required]on the command record work too. -
A rule that depends on existing state → inject the read model. Arc resolves the read model for this command’s key and hands it to whichever position asks for it — the validator,
Provide(), orHandle(). Where you put the rule depends on whether it must hold under concurrency.For an invariant that two simultaneous commands could both slip past — uniqueness is the classic one — guard it in
Handle(), closest to the append, and return a typed error:public Result<AuthorRegistered, ValidationResult> Handle(RegisteredAuthorName? existing) =>existing is not null && existing.Name != AuthorName.NotSet? ValidationResult.Error("An author with that name is already registered."): new AuthorRegistered(Name);Even that guard is a narrowing, not a guarantee — for a hard invariant, enforce it with a Chronicle constraint, which is checked at append time.
Most state-dependent rules aren’t races, though. “This order isn’t ready to submit”, “this account is frozen”, “this role doesn’t exist” — these are gates on projected state, and they belong in the validator, where they sit with the command’s other rules and reach the UI as ordinary validation errors:
public class SubmitOrderValidator : CommandValidator<SubmitOrder>{public SubmitOrderValidator(OrderReadModel? order){RuleFor(_ => order).NotNull().WithMessage("Order does not exist.");When(_ => order is not null, () =>RuleFor(_ => order!.Status).Equal(OrderStatus.ReadyForSubmission).WithMessage("Only orders that are ready for submission can be submitted."));}}The nullable parameter is how you say a missing projection is a business condition rather than a fault — Use current state in a command covers that choice and all three positions in full.
A validator can also reach outside the command’s own state. It’s resolved through dependency injection, so it can take a collaborator and check with FluentValidation’s
MustAsync:public class RegisterAuthorValidator : CommandValidator<RegisterAuthor>{public RegisterAuthorValidator(IAuthorsCatalog authors){RuleFor(c => c.Name).NotEmpty().MaximumLength(200);RuleFor(c => c).MustAsync(async (command, ct) => !await authors.IsRegistered(command.Name)).WithMessage("An author with that name is already registered.");}}A
MustAsyncrule runs on the server only — unlike the declarative rules, it can’t be extracted into the generated proxy.
Validators are discovered by convention — you never register them. The frontend surfaces the messages automatically; see Execute a command from React.
See also
Section titled “See also”- Command Validation and Validation — the full validation model.
- Make it trustworthy — the same ideas, taught step by step.
- Return a result or an error — the
Result<,>return shape used above. - Use current state in a command — injecting projected state into a validator,
Provide(), orHandle().