Skip to content

Code analysis

These ARCCHR#### Roslyn diagnostics belong to Arc’s Chronicle integration, not to standalone Arc or every project that references pure Chronicle. They catch integration mistakes while you build, so a misplaced key, ambiguous identity, or unsupported handler shape is visible before a request reaches production.

RuleSeverityWhat it checks
ARCCHR0001ErrorRecognized aggregate event-handler candidates have supported signatures.
ARCCHR0002WarningMultiple candidate command identities without an explicit provider/recognized return exemption.
ARCCHR0003WarningReactor access to the default event log instead of returned side effects.
ARCCHR0004WarningRedundant explicit id on [EventType]: empty, or equal to the type name.
ARCCHR0005WarningChronicle usage and AddCratisArc appear in one project without integration setup.
ARCCHR0006WarningManual reactor command execution without method- or class-level [OnceOnly].
ARCCHR0007WarningA command Handle() or Provide() parameter is IEventLog or an implementing type.
ARCCHR0008WarningData annotations [Key] used where Chronicle key resolution applies.
ARCCHR0009WarningLikely secret command values lack audit exclusion metadata.
ARCCHR0010WarningA keyless command returns a raw Guid beside statically identifiable untargeted events.
ARCCHR0011InfoRecognizable legacy read-model parameters or IReadModels.GetInstanceById calls in event-producing command/validator code.
ARCCHR0012InfoDirect immediate IEventLog.Append* in a command taking a protected decision read.
ARCCHR0013InfoA command or aggregate root names an event source with [EventSourceType] and a definition with that name exists in the compilation.
ARCCHR0014WarningA reactor or reducer is filtered with [FromEventSource<T>(stream)] to a stream the definition does not declare.
ARCCHR0015WarningA model-bound command handler declares a nullable event, directly or in a supported awaitable/union branch.
ARCCHR0016Warningconcurrency: true on a stream-metadata attribute of a command whose Handle() returns EventsWithConcurrencyScopes.
ARCCHR0017ErrorAn [EventStreamId] template on a command names an unusable property or has malformed braces.
ARCCHR0018WarningAn [EventStreamId] template on a reactor, where it is used literally.

A command that has [EventStreamId(value)] and also implements ICanProvideEventStreamId is reported by Chronicle’s own analyzer as CHR0027, not by an Arc rule. [EventStreamId("")] and [EventStreamId(null)] declare no id, so ARCCHR0017 stays silent for them.

Use one identity candidate or implement ICanProvideEventSourceId. Positional Chronicle [Key] and the matching property count as one candidate. Recognized explicit identity/wrapper return signatures can exempt the command.

This analyzer’s candidate convention includes implicit conversions; runtime discovery does not generally do so. A conversion-only ConceptAs<Guid> is not a safe runtime identity declaration. Use EventSourceId<Guid> ancestry or a selected key. Neither a clean analyzer result nor a return exemption proves that input-time aggregate/read-model dependencies use your intended id. See identity timing.

ARCCHR0007: command handler injects IEventLog

Section titled “ARCCHR0007: command handler injects IEventLog”

Prefer returned events. This rule matches IEventLog (and implementing-type) parameters on Handle() and Provide(), including read-only use; it does not inspect whether they append. To close a stream, return CompleteStream instead of calling CompleteStream on the injected log; the message says so when the handler does. Deliberate exact-revision reads or advanced explicit transactional appends may warrant a narrow suppression. The API remains supported at runtime, with the boundaries in Transactional commands. A warning is not a runtime prohibition.

ARCCHR0011: unguarded legacy decision reads

Section titled “ARCCHR0011: unguarded legacy decision reads”

A plain Chronicle-backed read model in an event-producing command’s Handle, Provide, or convention-discovered CommandValidator<T> does not guard the resulting decision. A direct IReadModels.GetInstanceById in those methods is likewise advisory, including an application extension method named GetInstanceById on IReadModels (for example one taking a typed EventSourceId<T> identity) and a rule lambda over the whole command such as RuleFor(c => c).MustAsync(...). In Handle or Provide, mark the command [ProtectedDecision] and switch to DecisionRead<T> or IDecisionReads where the shape is admitted. A protected command refuses validators with constructor dependencies, so for a read in a validator the message advises moving it into Provide or Handle instead; in validators only constructor parameters are reported. [Unprotected] on the command acknowledges intentional legacy reads; on a parameter or method it only suppresses this diagnostic, not runtime behavior. A GetInstanceById call is reported wherever it appears in such a validator — constructor, rule lambda (for example inside MustAsync), helper method or field initializer — with the same validator advice. The analyzer recognizes event-producing handlers through the same wrappers the command pipeline unwraps: Task/ValueTask, Result<…>/OneOf<…> branches and tuple elements, at any nesting (for example Task<Result<EventsWithConcurrencyScopes, ValidationResult>>), and object[], IEnumerable<object> or IEnumerable<EventForEventSourceId> event sets. The analyzer only recognizes explicit event return types, aggregate parameters, model-bound Chronicle attributes (on the type, or on a property or record parameter, such as [ChildrenFrom<T>] or [Join<T>]), and source-declared projection/reducer artifacts. Aliases, erased return types, indirect service calls and projections defined in external assemblies can be missed. To decide over a single event stream, read it with IStreamReads and return StreamDecision.Append; see stream decisions. The diagnostic message points there. It does not certify a command safe when quiet.

ARCCHR0012: immediate append with a decision read

Section titled “ARCCHR0012: immediate append with a decision read”

A direct IEventLog.Append* (also via IEventStore.EventLog) in a command that takes DecisionRead<T> or IDecisionReads writes immediately and cannot be undone if owner commit later conflicts. Return events or use the explicit Transactional style. This Info diagnostic matches direct calls, not helper chains or aliases, and does not flag Transactional.Append. For a decision over one event stream, return StreamDecision.Append from Handle() instead; see stream decisions. It is advisory, not a substitute for the runtime ownership checks that refuse direct completion of a protected unit of work.

ARCCHR0013: use the event source definition

Section titled “ARCCHR0013: use the event source definition”

[EventSourceType("Account")] (and [EventStreamType("Transactions")]) name an event source with a string. When a type in the same compilation is declared as that event source with [EventSource] and, if you name a stream, declares that stream with [EventStream], the diagnostic suggests [EventSource<AccountEventSource>("Transactions")], so the source, its stream and its concurrency dimensions come from one definition. It only reports a command or aggregate root, and only when a definition has exactly that name. A string with no matching definition, a stream the definition does not declare, and a type that already declares [EventSource<T>] are left alone. The legacy attributes keep working, so this is a suggestion and never a requirement. There is no code fix, because the definition and the attributes can differ in the concurrency flags the attributes carry, and rewriting them would change which writers conflict.

ARCCHR0014: observed event stream is not declared

Section titled “ARCCHR0014: observed event stream is not declared”

[FromEventSource<AccountEventSource>("Transactions")] filters a reactor or reducer to one stream of an event source definition. When the definition does not declare that stream with [EventStream("Transactions")], no event can match and the observer would never be called, so the diagnostic reports it as a warning on the attribute. Only the definition’s own attributes are read, so a definition in a referenced assembly is checked the same way as one in your project. A stream argument that is not a constant string, and a definition that is not resolved yet, are left alone.

RuleFix
ARCCHR0008Rewrite to the fully qualified Chronicle Key attribute.
ARCCHR0010Compiler-checked local replacement with EventSourceId<Guid> for supported direct tuple syntax.
ARCCHR0015Compiler-checked replacement of a nullable event with an explicit event-or-validation result and rejection placeholders.
ARCCHR0016Remove concurrency: true from the attribute.

The other rules have no automatic fix. A fix can change behavior: in particular ARCCHR0010 changes the persistence target when the Guid was an ordinary response.

Install the optional Arc integration package:

Terminal window
dotnet add package Cratis.Arc.Chronicle

It brings in Cratis.Arc.Chronicle.CodeAnalysis, which supplies these analyzers and code fixes. Complete the integration setup to connect Arc’s command pipeline to Chronicle.

Pure Cratis.Chronicle provides event sourcing without Arc; referencing it alone does not install Arc’s integration diagnostics. If a diagnostic is missing, check that your application’s resolved dependencies include Cratis.Arc.Chronicle.CodeAnalysis.