ARC0006: Command-scoped read model can be missing
A non-nullable read model parameter makes state a required dependency. Make the parameter nullable when the command handles absence as a normal outcome; retain a required parameter when absence should reject the command. Default severity: Warning.
The analyzer checks:
- constructors of
CommandValidator<TCommand>types, - public instance
Provide()methods on[Command]types, - public instance
Handle()methods on[Command]types.
It recognizes types marked [ReadModel] and, when Chronicle is referenced, types used by IProjectionFor<TReadModel> implementations in the current compilation. This compile-time recognition does not register a runtime provider or prove that an instance exists.
Diagnostic example
Section titled “Diagnostic example”This standalone Arc declaration deliberately produces ARC0006. Compile it in a project referencing Cratis.Arc.Core with nullable reference types enabled. It is not a runnable lookup: a real application must configure the read model provider and its key resolution.
#nullable enableusing Cratis.Arc.Commands;using Cratis.Arc.Commands.ModelBound;using Cratis.Arc.Queries.ModelBound;using FluentValidation;
[Command]public record SubmitOrder(string OrderId){ public string Handle() => OrderId;}
[ReadModel]public record OrderState(bool Ready);
public class SubmitOrderValidator : CommandValidator<SubmitOrder>{ public SubmitOrderValidator(OrderState order) { RuleFor(_ => order.Ready) .Equal(true) .WithMessage("Order must be ready for submission"); }}The command has a public handler and the read model is a record, so neither ARC0004 nor ARC0008 obscures the intended warning. The string identifier does not, by itself, configure command-scoped lookup.
Handle missing state explicitly
Section titled “Handle missing state explicitly”Replace the validator above with this nullable version when you want the validator itself to report absence:
public class SubmitOrderValidator : CommandValidator<SubmitOrder>{ public SubmitOrderValidator(OrderState? order) { RuleFor(_ => order) .NotNull() .WithMessage("Order does not exist");
When(_ => order is not null, () => { RuleFor(_ => order!.Ready) .Equal(true) .WithMessage("Order must be ready for submission"); }); }}This is a replacement fragment using the first block’s types and imports. The guard prevents dereferencing an absent instance. For creation commands, absence might instead be the desired condition; choose rules for your use case, not merely to silence the warning.
Require existing state
Section titled “Require existing state”Retain the non-nullable parameter when missing state should fail before the validator is constructed. Review or narrowly suppress the warning to record that deliberate choice. Nullable annotations do not fix provider registration or guarantee lookup success.
For a registered command-scoped read model provider:
| Resolution outcome | Current behavior |
|---|---|
| Usable key, instance absent, nullable parameter | Receives null; application logic decides what absence means |
| Usable key, instance absent, required parameter | ReadModelDoesNotExistForCommand, a validation failure mapped to HTTP 400 |
| Provider cannot resolve a usable key | UnableToResolveReadModelFromCommandContext, a validation failure mapped to HTTP 400, even for a nullable parameter |
| Required dependency is not registered | CannotResolveCommandDependency or CannotResolveValidatorDependency, an exception outcome rather than a normal missing-state rejection |
An unregistered nullable dependency can receive null without any lookup. Check registration before treating that as evidence that no state exists. The missing-state/key failures carry ValidationResultReason.DependencyUnavailable; testing assertions distinguish them from rules that actually ran.
Optional Chronicle behavior
Section titled “Optional Chronicle behavior”With the Chronicle integration, an event-source identifier or [Key] selects a projected instance but does not prove it exists. It may never have been created, may have been removed, or may be unavailable during a rebuild. A Chronicle command without a declared identity gets a generated key; that is different from an unusable resolved key.
Arc alone does not require projected state. For Chronicle-backed invariants that must hold under concurrency, decide whether projected state is sufficient or whether you need source-of-truth state instead.