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
{
}

Customer is a command-scoped read model. If it does not exist for the command’s event source id, Arc cannot inject a value into the non-nullable validator parameter, so the command fails with a dependency-resolution error.

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 really requires the read model to exist and absence should fail as a programming, configuration, projection, or consistency error. In that case, 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. Nullable read model parameters allow validators, Provide(), and Handle() to receive null and make an explicit decision. Non-nullable parameters require Arc to throw CannotResolveCommandDependency or CannotResolveValidatorDependency when the read model cannot be resolved or resolves to null.

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.