When read model resolution fails
Read-model resolution distinguishes invalid input from missing infrastructure. An unusable key or an absent required read model is a validation failure (HTTP 400). Missing service registrations and execution outside the command pipeline are configuration problems, not evidence that an entity does not exist.
| Failure | Cause | Client sees |
|---|---|---|
UnableToResolveReadModelFromCommandContext | The command carries no usable key | HTTP 400 — “The command is missing the identifier required to load its current state.” |
ReadModelDoesNotExistForCommand | Valid key, but a required (non-nullable) read model does not exist | HTTP 400 — “The command targets an entity that does not exist.” |
ReadModelValidatorRequiresCommandPipeline | The validator ran through MVC model binding, before a command context existed | Request fails |
CannotResolveCommandDependency / CannotResolveValidatorDependency | A required non-nullable dependency could not be resolved | Depends on the dependency |
The two validation failures above use generic client-facing messages. Do not generalize that to every exception: Arc’s exception-detail exposure is configurable, and enabling it can expose type names and other details. Keep production exception redaction and authorization configured independently.
UnableToResolveReadModelFromCommandContext
Section titled “UnableToResolveReadModelFromCommandContext”The resolved key is unusable, so there is nothing to resolve a read model by. With Chronicle, this means EventSourceId.Unspecified — for example, a declared key whose value is null, or an ICanProvideEventSourceId implementation that cannot compose an identity. Without Chronicle, it also includes commands on which no key is declared.
using Cratis.Arc.Commands.ModelBound;using Cratis.Chronicle.Keys;
[Command]public record CheckCustomer([Key] string? CustomerId){ // A null CustomerId leaves the declared key unspecified. // Making the read-model dependency nullable does not permit an unusable key. public bool Handle(Customer? customer) => customer is not null;}Keyless creation commands are different
Section titled “Keyless creation commands are different”When a Chronicle command declares no event-source key and does not implement ICanProvideEventSourceId, Chronicle generates a new identity so the command can create an entity. That is a usable key, not EventSourceId.Unspecified. If no read model exists for the generated identity, a nullable dependency receives null; a required dependency produces ReadModelDoesNotExistForCommand. A command targeting an existing entity must declare that entity’s identity instead of relying on this creation fallback.
For an unusable declared key, the failure is not “the entity does not exist” — the lookup cannot be performed, for a nullable and a non-nullable parameter alike. Making the parameter nullable does not suppress UnableToResolveReadModelFromCommandContext.
Fix: give the command a key. What counts as one depends on whether the application has Chronicle:
| Setup | Declare the key by |
|---|---|
| With Chronicle | marking a property or matching positional parameter with Cratis.Chronicle.Keys.KeyAttribute, using an EventSourceId or EventSourceId<T>-derived property, or implementing ICanProvideEventSourceId — see Resolving EventSourceId |
| Without Chronicle | marking a property with System.ComponentModel.DataAnnotations.KeyAttribute, or implementing ICanProvideKeyForCommand — see Declaring the key without Chronicle |
The data annotations attribute does not declare a Chronicle key. Unless another identity source is present, using it triggers the generated-ID fallback, not the missing-key error. ARCCHR0008 reports that attribute mismatch at build time.
ReadModelDoesNotExistForCommand
Section titled “ReadModelDoesNotExistForCommand”The command carried a valid key, but no read model exists for it — and the dependency was declared non-nullable, so Arc cannot inject anything.
This is the runtime counterpart of the choice ARC0006 asks you to make. Arc treats it as a rejected command rather than a server fault, because “you asked me to act on an entity that isn’t there” is invalid input.
Fix — pick the one that matches your intent:
-
Absence is a business condition. Make the parameter nullable and write the rule around
null:public class RemoveContactValidator : CommandValidator<RemoveContact>{public RemoveContactValidator(Customer? customer) =>RuleFor(_ => customer).NotNull().WithMessage("Customer is not registered");}You get a specific message instead of the generic one, which is almost always the better experience.
-
The projection really is required. Leave it non-nullable — the HTTP 400 is the intended behavior, and nothing needs to change.
For an asynchronously materialized model, the projection may not have caught up yet: a command issued immediately after the creating event can arrive first. Passive models instead build state on demand; see backing differences. For an invariant that must hold regardless, use a Chronicle constraint instead of projected state.
ReadModelValidatorRequiresCommandPipeline
Section titled “ReadModelValidatorRequiresCommandPipeline”A CommandValidator<TCommand> that depends on a read model was constructed through the MVC controller model-validation path. MVC runs validation during model binding — before the command context, and therefore before the event source id, exists. The validator cannot be constructed, so the request fails.
This affects MVC controllers only. Minimal-API command endpoints (the Arc default) and direct ICommandPipeline execution both establish the command context first and work correctly.
Fix: expose the command through a minimal-API command endpoint, or move the read-model based check out of the validator and into the command’s Handle() method.
CannotResolveCommandDependency and CannotResolveValidatorDependency
Section titled “CannotResolveCommandDependency and CannotResolveValidatorDependency”The general case: Arc needed to invoke Provide(), Handle(), or construct a discoverable validator, and a required non-nullable parameter could not be resolved or resolved to null.
For a registered read model with a valid event source id, Arc classifies the failure as ReadModelDoesNotExistForCommand instead — so if you are seeing these types for a read model parameter, the cause is usually one of:
- No provider registered the read model for command-scope injection. Chronicle requires a backing projection or reducer;
[ReadModel]alone does not make Chronicle own it. MongoDB and Entity Framework Core supply their own registrations. See what makes a read model injectable and other providers. - The parameter is not a read model at all — an ordinary service that is not registered.
Arc deliberately leaves these as server errors rather than masking them as validation failures, so genuine misconfiguration is not hidden behind an HTTP 400. A nullable dependency can also receive null when its type is not registered at all: nullable DI alone does not prove that a read-model lookup took place. Verify provider registration before interpreting that null as an absent entity.
See also
Section titled “See also”- Read models in commands — declaring the dependency and choosing nullability.
- ARC0006 — the analyzer that surfaces the nullability choice at build time.
- Resolving EventSourceId — how the key is found in the first place.