Skip to content

ARC0006: Command-scoped read model can be missing

Command-scoped read models can be missing. A [Key] or event source id tells Arc which read model instance to resolve, but it does not prove that the projection instance exists. A projection may never have been created, may have been removed, may be temporarily unavailable during projection rebuild, or may not be the source of truth for the invariant being checked.

This rule is a warning to make that choice explicit. Declare the parameter nullable when the command handles missing projected state as part of normal behavior. Keep it non-nullable when missing state should fail as a required dependency.

This rule reports non-nullable read model parameters in:

  • constructors of CommandValidator<TCommand> types
  • public instance Provide() methods on [Command] types
  • public instance Handle() methods on [Command] types

The analyzer recognizes read models marked with [ReadModel] and read models discovered through Chronicle IProjectionFor<TReadModel> projections.

Warning

using Cratis.Arc.Commands;
using Cratis.Arc.Commands.ModelBound;
using Cratis.Arc.Queries.ModelBound;
using Cratis.Chronicle.Keys;
[Command]
public record RemoveContact([Key] Guid CustomerId, Guid ContactId);
public class RemoveContactValidator : CommandValidator<RemoveContact>
{
public RemoveContactValidator(Customer customer)
{
}
}
[ReadModel]
public class Customer
{
}

When Customer is registered for command-scoped resolution and the command has a usable key, an absent instance produces ReadModelDoesNotExistForCommand — a validation failure (HTTP 400). Arc cannot inject a value into the non-nullable validator parameter.

Use a nullable read model dependency when missing projected state is a valid command outcome. For example, a registration command can use the absence of a Customer read model to allow registration, and reject only when the customer already exists:

using Cratis.Arc.Commands;
using Cratis.Arc.Commands.ModelBound;
using Cratis.Chronicle.Keys;
[Command]
public record RegisterCustomer([Key] Guid CustomerId, string Name);
public class RegisterCustomerValidator : CommandValidator<RegisterCustomer>
{
public RegisterCustomerValidator(Customer? customer)
{
RuleFor(_ => customer)
.Null()
.WithMessage("Customer is already registered");
}
}

Keep the parameter non-nullable when the command requires the read model to exist and absence should reject the command. For a registered read model and a usable key, Arc reports ReadModelDoesNotExistForCommand (HTTP 400), not a programming or configuration error. No code change is needed; the warning exists so the required-state choice is reviewed deliberately.

For example, a command that submits an existing order may require the order projection before any state rules can be evaluated:

using Cratis.Arc.Commands;
using Cratis.Arc.Commands.ModelBound;
using Cratis.Chronicle.Keys;
[Command]
public record SubmitOrder([Key] Guid OrderId);
public class SubmitOrderValidator : CommandValidator<SubmitOrder>
{
public SubmitOrderValidator(OrderReadModel order)
{
RuleFor(_ => order.Status)
.Equal(OrderStatus.ReadyForSubmission)
.WithMessage("Only orders that are ready for submission can be submitted");
RuleFor(_ => order.Lines)
.NotEmpty()
.WithMessage("Order must have at least one line");
}
}

The same rule applies if a command’s Provide() or Handle() method takes a command-scoped read model parameter. Make the parameter nullable when missing projected state is part of the command’s valid behavior. Keep it non-nullable when missing state should fail as a required dependency.

Chronicle read models represent current projected state. Missing projected state can be meaningful: an entity may not be registered yet, may already have been removed, or may not have reached a state represented by that projection. It can also be exceptional: the command may target an event source that should already have a projection.

Before the command runs, Arc resolves command-scoped read models from the command context. For a registered read model with a usable key, nullable parameters allow validators, Provide(), and Handle() to receive null and make an explicit decision; non-nullable parameters instead produce ReadModelDoesNotExistForCommand (HTTP 400).

An unusable resolved key produces UnableToResolveReadModelFromCommandContext (HTTP 400), even for a nullable dependency. A Chronicle command that declares no identity is different: it receives a generated key for creating an entity, so missing state for that new key follows the normal nullable/required rules.

An unregistered required dependency still produces CannotResolveCommandDependency or CannotResolveValidatorDependency as a server error. An unregistered nullable dependency can receive null without any lookup occurring. Verify the read model’s provider registration rather than using nullability to hide missing wiring.

If correctness depends on source-of-truth state, prefer aggregate or event-stream state over a read-model validator. Read models are best used for projected-state checks where the projection is the right input to the decision.