CHR0045: Event stream metadata attribute on an event type has no effect
Rule Description
Section titled “Rule Description”[EventStreamType] or [EventSourceType] is applied to a type marked with [EventType], where it does nothing. An append resolves its event source type and event stream type from the arguments it is given — the appending command’s metadata, the reactor’s, or the explicit parameters — never from the CLR type of the event being appended. Nothing reads either attribute off an event type, so the value is not transmitted, never reaches the appended event’s context, and narrows nothing on the way back out.
Three placements work, and none of them is affected by this rule:
- A command, where both values tag the events its append produces and, with
concurrency: true, join the server-side concurrency scope. - An observer — a reactor or a reducer — where both values narrow which appended events are dispatched. See appended event metadata filters.
- An aggregate root, where
[EventStreamType]becomes the event stream type of every event that aggregate appends. Without the attribute the aggregate’s own type name is used, so deleting it does not error — it silently moves every subsequent append to a different stream type. ([EventSourceType]is not read off an aggregate root; the event source type is supplied when the aggregate is fetched.)
A type is allowed to be more than one thing. A [Command], reactor, reducer, or aggregate root that also carries [EventType] is not reported, because the attribute is live on it — it is read off that very type.
A projection is a placement that does nothing, reported separately as CHR0041.
Severity
Section titled “Severity”Warning
Example
Section titled “Example”using Cratis.Arc.Commands.ModelBound;using Cratis.Chronicle.Events;using Cratis.Chronicle.Reactors;
// Warning CHR0045: 'EventStreamTypeAttribute' on event type 'Chr0045AccountOpened' has no// effect - an append resolves its stream metadata from the append itself, never from the// event's type. Every append of this event still lands under the default stream type, and a// reactor filtering on "onboarding" observes none of them.[EventStreamType("onboarding")][EventType]public record Chr0045AccountOpened(string Name);
// The command that appends the event is a placement that works - here the value tags the// appended events and, with concurrency, joins the server-side concurrency scope.[EventStreamType("onboarding", concurrency: true)][Command]public record Chr0045OpenAccount(string Name){ public Chr0045AccountOpened Handle() => new(Name);}
// The observer is another - here the value narrows which appended events are dispatched. An// aggregate root is the third: [EventStreamType] there becomes the stream type of every event// the aggregate appends, and the aggregate's type name is used when it is absent.[EventStreamType("onboarding")]public class Chr0045AccountNotifier : IReactor{ public Task AccountOpened(Chr0045AccountOpened @event) => Task.CompletedTask;}Why This Rule Exists
Section titled “Why This Rule Exists”The attributes’ own documentation used to say an event type was a valid placement, and that is what an IDE tooltip shows at the moment of authoring. A developer who trusts it tags the event once at its declaration and expects every append of it to carry that stream identity. What happens instead is that events land under the default source and stream type, observers that filter on those values match nothing, and there is no build error, startup failure, or runtime signal anywhere to say why. The natural way to debug that — re-reading the attribute to confirm it is present and spelled right — confirms the wrong conclusion.
The placement is refused rather than honored, and deliberately so. Both values are persisted in every appended event’s context and are load-bearing keys for the concurrency scope and for observer filtering. Letting an event’s CLR type contribute them would retroactively change stream identity for every event type already declared, against events already written.
The same reasoning is why the rule keys on the role of the type rather than on the mere presence of [EventType]. Where the attribute is read — a command, an observer, an aggregate root — removing it changes real behavior, so a warning that says “move it” there would be advice with nowhere to go, and under warnings-as-errors it would break the build outright.
Related Rules
Section titled “Related Rules”- CHR0041: Event filter attribute on a projection has no effect — the same attributes on the other placement that reads as a filter and is not one.
- CHR0024: Read model property has no mapping source — another declaration that looks wired but silently does nothing.