Skip to content

Refusals under read protection

Use this page when a command fails under read protection On and you want to know why and what to change. Read protection fails closed: when it cannot guard a read exactly, it refuses the command instead of running it unguarded.

KindStatusWhat it meansWhat to do
The read changed400Someone wrote an event that matches what the command read, before it committed. Nothing was writtenSubmit the command again. The client reads fresh state and decides again
The code cannot be guarded500A configuration error in the command. Retrying does not helpChange the code. The message names the read and the fix

Arc and Chronicle do not re-run the command; the fold may retry up to three times. Keep the user’s input, read fresh state, and offer a deliberate resubmission.

FailureWhenMessage
ReadChangedA guarded read changed before commitBookStock 'isbn-1' changed after BorrowBook read it (BookBorrowed at 1042). Nothing from this command was written; submit it again.
ReadIncompleteThe fold of the read model did not reach the log position it needed after three attempts, because events arrived while it ranBookStock 'isbn-1' could not be read completely. Nothing was written; submit it again.

Both arrive as a validation result with the reason concurrencyViolation. The detail is ReadChanged or ReadIncomplete, so a client can tell them from other concurrency violations. A ReadChanged result also carries the read model and key in its state.

When Chronicle reports the first event that matched the guard after the read, the message names its event type and sequence number, and the state adds conflictingEventTypeId, conflictingEventType and conflictingEventSequenceNumber. Reads that share a guard are merged, so the event can belong to another read under the same guard. When Chronicle does not report it, the message ends at read it. and the state has only the read model and key.

A [ProtectedDecision] command outside On words the message differently: BookStock 'isbn-1' changed after it was read. Read it again and resubmit.

FailureCauseFix
ReadCannotBeProtectedA parameter’s model has a shape Chronicle cannot guard exactlySee the reasons below
AmbiguousReadModelLookupSeveral command properties have the model’s Id type, and no [LookUpBy] chose oneAdd [LookUpBy(nameof(Property))] to the parameter
InvalidLookUpBy[LookUpBy] names a property the command does not have, or sits on a parameter that is not a read modelName an existing public property

ReadCannotBeProtected has this message. The description and the fix change with the cause:

LendBook reads 'stock' (BookStock) through a reducer that filters its events, so read protection cannot guard it. If it is only context, mark it [Unprotected("why")]. Otherwise remove the reducer's event filter or read an unfiltered projection.
DescriptionFix
through a reducer whose handler or reader implementation does not support protected replayUse Chronicle’s built-in reducer reader with a discovered reducer handler
through a reducer with constructor dependenciesRemove dependencies other than ILogger<T>; self-reducing models are exempt from this constructor restriction
through a reducer that filters its eventsRemove the reducer’s event filter, or read an unfiltered projection
with joins or childrenRead the parts as separate models
keyed by an event propertyAppend a fact to the entity the model is keyed by, or use a constraint
using Pinned generation deliveryUse Compatibility delivery for this projection
with an unsupported key typeGive the model a string or Guid Id
keyed by a streamUse IDecisionReads.Get with an explicit source, stream type and stream id in Provide()
from another event sequenceRead a model of the event log
without exactly one backing definitionRegister exactly one projection or admitted reducer for the model
with a mismatched registered definitionRegister the current definition before executing commands
through IMongoCollectionInject the read model instead of its collection

Unfiltered reducers with no constructor dependencies other than ILogger<T> are admitted. Self-reducing models are exempt from the constructor restriction, not from the filter restriction.

ARCCHR0024 reports the statically visible cases at build time.

FailureCauseFix
ReadRefusedWhileReadsAreProtectedA read through IReadModels, IEventStore, IEventLog or an event sequenceTake a read model as a parameter, use IDecisionReads.Get in Provide(), or mark the method [Unprotected("why")]
ReadModelInjectedOutsideDecisionA read model injected through dependency injection anywhere other than Handle() and validator constructors, for example into a command filter or a serviceTake the model as a parameter of Handle() or of a CommandValidator<T> constructor, and move the rule there. If it is only context, mark the read-model type [Unprotected("why")]
DetachedReadWhileReadsAreProtectedIDecisionReads.GetDetachedUse Get in Provide()
ImmediateAppendWhileReadsAreProtectedAn append through IEventLog.Append, AppendMany, Revise, Redact or CompleteStream that writes at onceReturn the event from Handle(), or use IEventLog.Transactional.Append. Return CompleteStream instead of calling it
ReadAfterProtectedCommitA protected read after an aggregate’s Commit() completed the unitRead before committing, and return the result of Commit() last
ReadOfSourceWithStagedEventsA read of a source after the command staged events for itDecide once in the outer command and pass the value down
ProtectedUnitOfWorkRequiresOwnerCommand code called Commit(), Rollback() or Dispose() on the unitReturn events. Arc owns the unit

ARCCHR0028, ARCCHR0012 and ARCCHR0035 report the statically visible cases.

An immediate append is refused for the whole invocation, including after an aggregate’s Commit(). An immediate append is written before the commit and stays if the commit conflicts, so it cannot be part of a guarded decision.

FailureCauseFix
ReadAndAggregateOnSameSourceAn aggregate and a plain read of the same event source in one commandDecide from the aggregate’s state, read another source, or mark the plain read [Unprotected("why")]
ExplicitScopeDoesNotCoverReadsThe command returns an exact concurrency scope for a source that is narrower than what it read, or captured after itWiden the scope and capture its boundary before reading, or remove it
DeclaredRoutingNotCoveredByExplicitScopeAn exact scope does not cover the concurrency the event source definition declaresWiden the scope, or remove the declared concurrency
ConflictingContributionsOnLabel, ConflictingExplicitScopesTwo different scopes or aggregates guard one event sourceKeep one
ReadProtectionRequiresTheDefaultConcurrencyStrategyA custom concurrency strategyUse the default strategy
ReadDependencyTargetMismatchA guard belongs to another event store, namespace or sequence than the unitRead and write in the same store and namespace
ReservedConcurrencyScopeLabelA scope label starts with $read-protection/Use another label
SourceLessReadNotSupportedA read with no event sourceRead a source-keyed model

ARCCHR0033 and ARCCHR0036 report the static cases of the first two rows.

FailureCauseFix
NestedEarlyCompletionUnderReadProtectionA nested command committed the outer command’s unit (an aggregate Commit())Commit in the outer command
ConcurrentNestedCommandsUnderReadProtectionA nested command started while another nested command of the same outer command was still running, and the outer command, the new command or any running one has read protection OnRun nested commands one after another
FailureCauseFix
CommandCannotBeBothProtectedAndUnprotected[ProtectedDecision] and [Unprotected] on one commandKeep one
ConflictingReadProtection[ReadProtection(On)] or Measure and [Unprotected] on one commandKeep one
DiscoverableValidatorRefusedInProtectedDecisionA validator under On that Arc did not construct for this invocationLet convention discovery register the validator

ARCCHR0029 and ARCCHR0031 report declaration errors at build time.

A 400 is a command result with a validation result, like any validator failure. A 500 carries the refusal text in exceptionMessages only when ArcOptions.ExposeExceptionDetails is true. That option defaults to true in Development and false elsewhere. When it is false, Arc replaces exception messages and removes stack traces; inspect server logs using the correlation id instead. Avoid exposing exception details in production.

When details are exposed, Arc replaces Chronicle’s generic subject “The command” with your command’s name. See the HTTP contract.

Chronicle documents the same refusals at the API level in Read protection in Chronicle.