Code Analysis Rules
This section documents the code analysis rules provided by the Chronicle Code Analyzer for the .NET client.
All rules follow the identifier format CHR####. Numbers are assigned sequentially; an occasional id is reserved for a rule still in progress, so a gap in the published set is expected.
Rules Overview
Section titled “Rules Overview”| Rule ID | Title | Severity | Description |
|---|---|---|---|
| CHR0001 | Event type must have [EventType] attribute | Error | Types appended to event sequences must be marked with [EventType] attribute |
| CHR0002 | Declarative projection event type must have [EventType] attribute | Error | Declarative projection generic arguments must reference types with [EventType] attribute |
| CHR0003 | Model bound projection attribute must reference event type with [EventType] attribute | Error | Model bound projection attributes must reference types with [EventType] attribute |
| CHR0004 | Reactor method has a parameter that cannot be resolved | Warning | Reactor method dependencies must be resolvable from event context, read models, or services |
| CHR0005 | Reactor event parameter must have [EventType] attribute | Error | Event parameters in reactor methods must be marked with [EventType] attribute |
| CHR0006 | Reducer method signature must match allowed signatures | Warning | Reducer methods must follow allowed signatures |
| CHR0007 | Reducer event parameter must have [EventType] attribute | Error | Event parameters in reducer methods must be marked with [EventType] attribute |
| CHR0008 | Reactor event types must be from the same event store | Error | Reactor event types must all originate from the same event store |
| CHR0009 | Reducer event types must be from the same event store | Error | Reducer event types must all originate from the same event store |
| CHR0010 | Model-bound projection event types must be from the same event store | Error | Model-bound projection event types must all originate from the same event store |
| CHR0011 | Declarative projection event types must be from the same event store | Error | Declarative projection event types must all originate from the same event store |
| CHR0012 | Event types should avoid nullable properties | Warning | Nullable properties are supported on events but are often better modeled as separate event types |
| CHR0013 | Reactor cannot combine EventStore with explicit event sequence | Error | Reactors with [EventStore] must not also configure an explicit event sequence |
| CHR0014 | Reducer cannot combine EventStore with explicit event sequence | Error | Reducers with [EventStore] must not also configure an explicit event sequence |
| CHR0015 | Projection must not have side effects | Error | Projections must not inject ICommandPipeline or IEventLog |
| CHR0016 | Projection Define() must not contain imperative code | Error | Projection Define() must only contain builder calls, not imperative statements |
| CHR0017 | Constraint must not have side effects | Error | Constraints must not inject ICommandPipeline or IEventLog |
| CHR0018 | Constraint Define() must not contain imperative code | Error | Constraint Define() must only contain builder calls, not imperative statements |
| CHR0019 | Projection expression lambda must only access members | Error | Expression lambdas in projection builder methods must be simple member-access expressions |
| CHR0020 | Constraint expression lambda must only access members | Error | Expression lambdas in constraint builder methods must be simple member-access expressions |
| CHR0021 | Event types should be record types | Warning | Event types should be declared as record types for immutability |
| CHR0022 | Reactor methods returning event side effects must be marked with [OnceOnly] | Warning | Reactor methods that return events must be [OnceOnly] to avoid appending duplicates during replay |
| CHR0023 | Ambiguous parent key for [ChildrenFrom] collection | Warning | Parent-key inference is ambiguous when the child event has more than one property of the parent identifier type; specify parentKey |
| CHR0024 | Read model property has no mapping source | Warning | A model-bound read model property has no mapping attribute and no subscribed event carries a same-named property, so AutoMap can never populate it |
| CHR0025 | Explicitly sourced read model property may be overwritten by AutoMap | Info | A property set with [SetFrom]/[SetValue]/… collides by name with another referenced event that AutoMap writes on top; add [NoAutoMap] or accept the update |
| CHR0026 | [Key] or [Subject] on an EventSourceId<T> is redundant | Warning | An EventSourceId<T>-derived property already is the key and compliance subject, so [Key]/[Subject] on it is redundant — remove the attribute |
| CHR0027 | Ambiguous event stream id | Error | A type both implements ICanProvideEventStreamId and declares a non-null [EventStreamId]; this throws at startup |
| CHR0028 | Redundant .AutoMap() call | Warning | .AutoMap() has no effect because AutoMap is enabled by default on projection builders; remove the call |
| CHR0029 | Redundant .Set().To() with matching property names | Warning | A .Set(x => x.P).To(e => e.P) mapping with identical names duplicates what AutoMap already does; remove it |
| CHR0030 | [ChildrenFrom] child collection property auto-maps to nothing | Warning | A [ChildrenFrom] child collection property matching no event property and with no explicit mapping always projects empty; rename it or bridge with [SetFrom<T>] |
| CHR0031 | Reactor must not have mutable state | Warning | Reactors are re-created and replayed, so mutable instance state is unreliable; use readonly, primary-constructor-injected dependencies |
| CHR0032 | Reactor must not access storage directly | Warning | Injecting a storage primitive like IMongoCollection<T> couples the reactor to a sink; read state via a read model or IReadModels.GetInstanceById |
| CHR0034 | [PII] cannot be applied to an EventSourceId<T> | Error | The event source id is the encryption-key lookup identity, so it cannot be encrypted; [PII] on it throws PIINotSupportedOnEventSourceId at runtime |
| CHR0035 | Read model declares a reserved compliance subject property | Error | Chronicle reserves _subject, __subject, and __subjects for internal compliance-subject tracking; a same-named property collides with stored metadata |
| CHR0036 | Reducer must not have mutable state | Warning | Reducers are re-created and replayed, so mutable instance state or direct storage injection makes the fold non-deterministic; keep them stateless |
| CHR0037 | Event type migration generations must belong to one event type | Warning | The two generations referenced by an EventTypeMigration must resolve to the same event type id and differ only by generation, or the migration never applies — checked for both EventTypeGenerationFor<T> and the older explicit-id [EventType] style |
| CHR0039 | Assertion result is discarded and can never fail | Error | An awaitable-returning Should* assertion on a Cratis testing surface whose result is discarded throws on an awaitable nobody observes, so the assertion silently passes regardless of behavior and CS4014 does not fire outside an async method |
| CHR0040 | Several [SetFromContext] for the same event type on one member | Warning | They write the same property in the definition, so only the last declared is kept and the earlier capture is silently dropped; capturing from several different event types is the supported use and is not flagged |
| CHR0041 | Event filter attribute on a projection has no effect | Warning | A projection observes every event of the types it declares and cannot filter on event metadata — only reactors and reducers can — so [EventStreamType], [EventSourceType], and [FilterEventsByTag] on a projection are silently ignored |
| CHR0042 | A joined property is also written locally | Warning | A property written by both a local mapping and a join — model-bound or fluent — always ends up with the joined value regardless of arrival order, so a local write can never reset it; a flag latched from both sides silently sticks |
| CHR0043 | Key redirection carries a [PII] value across the compliance subject | Warning | A root key or child parent-key redirect carries PII onto a document whose resolved compliance subject is not provably the value owner’s |
| CHR0045 | Event stream metadata attribute on an event type has no effect | Warning | An append resolves its event source type and event stream type from the append itself, never from the event’s CLR type, so [EventStreamType] and [EventSourceType] on an [EventType] are read by nothing — declare them on the appending command, the observing reactor/reducer, or (for [EventStreamType]) the aggregate root whose appends they identify |
| CHR0046 | Passive read model redirects the document key away from the event’s own stream | Warning | A passive read model has no sink, so a read replays one event stream constrained to the requested key; a root key redirection points that replay at a stream the events were never appended to and the read returns a default-initialized model |
| CHR0047 | A clear declaration is never applied by projection construction | Retired | Both declarations it reported - a null [SetValue] and a member-level [ClearWith] - are a working scalar clear now, so the rule was removed; the id stays reserved and is never reused |
| CHR0048 | A clear is declared for a member that cannot hold null | Warning | Clearing means returning a member to no value; a non-nullable member has no such state, so the only thing the projection could write is the type default - a different fact the read model cannot tell apart from a real value |
| CHR0049 | EventTypeGenerationFor<T> must reference a type marked with [EventType] | Error | A type marked with EventTypeGenerationFor<T> references a type that is not itself marked with [EventType], so there is no id to resolve the previous generation’s identity from |
| CHR0050 | [EventType] and [EventTypeGenerationFor<T>] cannot be combined | Error | A type is either the current event type or a previous generation, never both |
| CHR0051 | Reducer current read model parameter must be nullable | Warning | Chronicle passes null as the current read model for the event that brings an instance into existence |
| CHR0052 | [Encrypted] cannot be applied to an EventSourceId<T> | Error | The event source id is used to correlate events and cannot be encrypted |
| CHR0053 | [PII] and [Encrypted] cannot both apply to the same value | Error | Combining them encrypts the value twice, under two different keys, and it cannot be released correctly |
| CHR0054 | Reactor method has an unsupported return type | Warning | A reactor method with an event parameter must return a shape Chronicle discovers as a handler |
| CHR0055 | Unknown event stream on typed event-source filter | Error | [FromEventSource<TSource>("stream")] must name a stream the event source definition declares |
| CHR0056 | Explicit routing contradicts typed event-source filter | Error | [EventSourceType] or [EventStreamType] on the same type may only repeat what [FromEventSource<TSource>] already says |
| CHR0057 | An artifact must target one read model | Warning | A class implementing multiple closed projection or reducer interfaces fails during default artifact discovery; split it into one artifact per read model |
| CHR0062 | Concurrency applies across event sources | Info | A Concurrency declaration on a definition or stream that omits EventSourceId is a cross-source condition, not a guard for the state a command read |
| CHR0063 | Scope stream does not match append | Warning | A definition-linked scope guards a different stream than the single-route append it is passed to |
| CHR0064 | Invalid read model event stream | Error | A per-stream read model must name one declared stream, keep the event source ID as its key, and cannot be reducer-backed |
| CHR0065 | Read model fold method will not be discovered | Warning | An event fold on a read model has the wrong name or is not static, so it cannot be discovered as a self reducer |
Explicit registration evidence
Section titled “Explicit registration evidence”CHR0001, CHR0002, CHR0003, CHR0005 and CHR0007 accept event types nominated by semantically bound ExplicitArtifacts.RegisterEventType<T>() or RegisterEventType(typeof(T), ...) calls anywhere in the same compilation, including another source file. Their existing diagnostic ids, severities and messages remain unchanged. Reactor return types and event sequences also use that evidence. Explicit RegisterReactor and RegisterReducer calls opt plain observers into the existing method-signature checks.
This is possible-registration evidence, not whole-program verification:
- Dead or conditional calls count even if never executed. A registration on one client does not prove membership on another client.
- Calls hidden in a referenced library, reflection, plugin, configuration or open-generic wrapper are not proven. A literal closed generic or
typeofcall in source is the supported evidence shape; a variable containing a Type is not evaluated. - Callbacks, supplied ids, generations, store ownership and control flow are not interpreted. Other analyzer families remain marker-based; this does not make every rule aware of explicit metadata.
- Plain migration method roles and endpoint relationships are validated at runtime; the migration-id analyzer does not prove them.
Validate the actual store configuration at runtime. If registration is correct but invisible to these analyzers, suppress only the relevant CHR0001/2/3/5/7 diagnostic at that use site rather than disabling the analyzers globally. See explicit artifact registration for the registration and snapshot contract.
Quick Fixes
Section titled “Quick Fixes”- CHR0001, CHR0002, CHR0003, CHR0005, and CHR0007 provide a code fix that adds the missing
[EventType]attribute to the referenced type. - CHR0026 provides a code fix that removes the redundant
[Key]/[Subject]attribute. - CHR0028 provides a code fix that removes the redundant
.AutoMap()call. - CHR0029 provides a code fix that removes the redundant
.Set(...).To(...)mapping.
Installation
Section titled “Installation”The rules ship in the Cratis.Chronicle.CodeAnalysis package. From 19.4.8, Cratis.Chronicle depends on it, so referencing Cratis.Chronicle — or a package that depends on it, such as Cratis.Chronicle.AspNetCore — runs the rules in your build with nothing else to add.
Before 19.4.8, Cratis.Chronicle did not carry that dependency, and no rule ran in a consuming build unless the project referenced the analyzer package itself. On an older version, add it at the same version as the client:
dotnet add package Cratis.Chronicle.CodeAnalysis --version <your Cratis.Chronicle version>The analyzers inspect C# only; the other Chronicle clients have no equivalent.