Skip to content

When read model resolution fails

Read-model resolution distinguishes invalid input from missing infrastructure. For command-property and convention lookups, an unusable key or an absent required read model is a validation failure (HTTP 400). A refused caller-derived key is an authorization failure instead. Missing service registrations and execution outside the command pipeline are configuration problems, not evidence that an entity does not exist.

Failure Cause Client sees
ReadModelKeyResolutionDenied The captured caller or its provider cannot supply a safe key Authorization failure, without provider exception details
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.

A generic [LookUpBy<TKeyProvider>] lookup could not obtain a safe key from the captured selected caller. The command result is an authorization failure (IsAuthorized == false), not a missing-model validation error. Application exception mappers do not replace this result; provider exceptions, stack traces and claim values are not exposed.

Resolution is refused when the caller frame is absent or its observable principal changes, the provider returns missing or ambiguous, the provider throws, or the resolved key text is empty, wildcard, unspecified, padded with whitespace or contains an ambiguous # separator. Nullable and [Unprotected] parameters do not bypass these checks. Arc never falls back to a client-supplied command key after refusal.

Supply the intended selected caller before entering the command pipeline and ensure the provider returns one usable key. Do not log claim values while diagnosing the refusal. CallerSubject requires an authenticated, non-system caller and reads exactly sub, excluding the actor chain. See caller-derived keys.

A valid key with no stored model is different: normal nullable-versus-required absence rules apply. Conflicting lookup forms, a Provide() value for a caller-keyed parameter or redirecting a per-stream model are declaration errors (InvalidLookUpBy), not evidence of missing state.

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:

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.

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.