Command validation
When input is incomplete, return useful feedback before performing work. Arc’s model-bound pipeline runs validation filters before Provide() and Handle(). Blocking results stop execution and appear in CommandResult.ValidationResults.
Validation is not authorization, and a pre-flight check cannot guarantee that state remains unchanged. Read severity filtering before relying on a rule as a non-overridable guard.
Data annotations
Section titled “Data annotations”For positional records, explicitly target the generated properties. Arc’s command filter uses object-property validation, not constructor-parameter metadata.
This complete command allocates a Guid; it does not persist a business record:
using System;using System.ComponentModel.DataAnnotations;using Cratis.Arc.Commands.ModelBound;
[Command]public record AllocateLabeledIdentifier( [property: Required] string Label, [property: Range(1, 100)] int Copies){ public Guid Handle() => Guid.NewGuid();}An empty Label or an out-of-range Copies produces validation errors before Handle(), at the default severity threshold. Constructor annotations such as [Required] string Label do not provide the property metadata this filter reads. MVC record binding has different metadata rules; do not apply this advice mechanically to controller-based commands.
FluentValidation
Section titled “FluentValidation”For richer rules, derive a validator from CommandValidator<T>. Arc discovers it with the application’s types; no per-validator registration is needed under normal Arc discovery.
This validator is an alternative to the annotations above, using the same command type:
using Cratis.Arc.Commands;using FluentValidation;
public class AllocateLabeledIdentifierValidator : CommandValidator<AllocateLabeledIdentifier>{ public AllocateLabeledIdentifierValidator() { RuleFor(command => command.Label).NotEmpty().MaximumLength(100); RuleFor(command => command.Copies).InclusiveBetween(1, 100); }}Remove duplicate annotations if you select FluentValidation for those same rules. The proxy generator extracts supported rules for early client feedback; server rules involving services or custom logic are not automatically equivalent client-side checks.
Validator dependencies
Section titled “Validator dependencies”Validators may accept registered services through their constructors. Arc resolves these from the command scope, also used for Provide() and Handle() dependencies. This application-level example supplies its complete policy dependency and makes no database assumptions:
using Cratis.Arc.Commands;using FluentValidation;
public record LabelPolicy(int MaximumLength);
public class LabelPolicyValidator : CommandValidator<AllocateLabeledIdentifier>{ public LabelPolicyValidator(LabelPolicy policy) { RuleFor(command => command.Label).MaximumLength(policy.MaximumLength); }}Host registration fragment, with Microsoft.Extensions.DependencyInjection imported:
builder.Services.AddSingleton(new LabelPolicy(80));Nullable dependency parameters may receive null when no value can be resolved. Non-nullable dependency parameters are required; resolution failure produces an exception outcome instead of silently constructing a validator with null. Use a nullable dependency when absence is a valid input to your rule, not to conceal a missing required service.
A service that reads storage must actually implement that lookup. Arc.Core does not assume projected event state or supply persistence for arbitrary dependency types. For provider-specific current-state resolution, see use current state in a command. Chronicle read-model injection is an optional integration, not the default validator dependency model.
Choosing a rejection phase
Section titled “Choosing a rejection phase”- Use a validator for input feedback before data provisioning and execution.
- Use
Provide()to fetch handler data after filters; it can return validation control values to short-circuit. See provide data to a command. - Use a singular
ValidationResultalternative fromHandle()for a decision made during handling. See response value handlers. A failure returned after a service write does not undo that write. - Use an authorization filter for access control, not an overridable validation rule.
For early feedback without invoking Provide() or Handle(), continue to pre-flight command validation.