Skip to content

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.

FailureCauseClient sees
UnableToResolveReadModelFromCommandContextThe command carries no usable keyHTTP 400 — “The command is missing the identifier required to load its current state.”
ReadModelDoesNotExistForCommandValid key, but a required (non-nullable) read model does not existHTTP 400 — “The command targets an entity that does not exist.”
ReadModelValidatorRequiresCommandPipelineThe validator ran through MVC model binding, before a command context existedRequest fails
CannotResolveCommandDependency / CannotResolveValidatorDependencyA required non-nullable dependency could not be resolvedDepends 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;
}

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:

SetupDeclare the key by
With Chroniclemarking 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 Chroniclemarking 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.

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.

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.