CHR0037: Event type migration generations must belong to one event type
Rule Description
Section titled “Rule Description”A class deriving from EventTypeMigration<TUpgrade, TPrevious> references two event type generations that don’t resolve to the same event type id. Chronicle keys generations of the same event by their event type id, so both generations must resolve to the same id and differ only by their generation number.
This is checked for both ways of declaring a previous generation:
[EventTypeGenerationFor<T>](recommended) — the rule verifies thatTreally is the migration’sTUpgrade. Referencing any other type here is always a mistake: the migration would upcast a generation that doesn’t belong to the event type it claims to.[EventType]on both generations (the older, explicit-id style) — the rule verifies both ids are present and identical. An absent id (silently defaulting to the CLR type name) or a mismatched id makes Chronicle treat the two records as unrelated event types.
Severity
Section titled “Severity”Warning
Example
Section titled “Example”Violation
Section titled “Violation”using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Migrations;
// Warning CHR0037: Event type generations 'Chr0037ViolationCustomerRegisteredV2' and// 'Chr0037ViolationCustomerRegisteredV1' referenced by migration// 'Chr0037ViolationCustomerRegisteredMigration' must resolve to the same event type// and differ only by generation.[EventType("Customer.Registered", generation: 1)]public record Chr0037ViolationCustomerRegisteredV1(string Name);
[EventType("Customer.Renamed", generation: 2)]public record Chr0037ViolationCustomerRegisteredV2(string FirstName, string LastName);
public class Chr0037ViolationCustomerRegisteredMigration : EventTypeMigration<Chr0037ViolationCustomerRegisteredV2, Chr0037ViolationCustomerRegisteredV1>{ public override void Upcast(IEventMigrationBuilder<Chr0037ViolationCustomerRegisteredV2, Chr0037ViolationCustomerRegisteredV1> builder) { } public override void Downcast(IEventMigrationBuilder<Chr0037ViolationCustomerRegisteredV1, Chr0037ViolationCustomerRegisteredV2> builder) { }}using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Migrations;
[EventType("Customer.Renamed", generation: 2)]public record Chr0037FixCustomerRegisteredV2(string FirstName, string LastName);
[EventTypeGenerationFor<Chr0037FixCustomerRegisteredV2>(1)]public record Chr0037FixCustomerRegisteredV1(string Name);
public class Chr0037FixCustomerRegisteredMigration : EventTypeMigration<Chr0037FixCustomerRegisteredV2, Chr0037FixCustomerRegisteredV1>{ public override void Upcast(IEventMigrationBuilder<Chr0037FixCustomerRegisteredV2, Chr0037FixCustomerRegisteredV1> builder) { } public override void Downcast(IEventMigrationBuilder<Chr0037FixCustomerRegisteredV1, Chr0037FixCustomerRegisteredV2> builder) { }}How To Fix It
Section titled “How To Fix It”- Preferred: mark the previous generation with
[EventTypeGenerationFor<TUpgrade>(N)]instead of giving it its own[EventType]. The id is then resolved fromTUpgradedirectly, so the two generations can never drift apart. - If you keep the
[EventType]-on-both style: give both generations the exact same explicit id string, and vary onlygeneration:.
Why This Rule Exists
Section titled “Why This Rule Exists”When an event’s schema changes after events of the old shape are already stored, you add a new generation of the same event type and an EventTypeMigration<TUpgrade, TPrevious> that upcasts the stored events into the new shape. Chronicle recognizes the two records as generations of one event type by a shared event type id — not by their C# type names.
If the two generations resolve to different ids, the migration never applies: stored events of the previous generation are never upcast, and consumers read the old shape. Because both records compile and the migration is discovered by convention, nothing surfaces the mistake without this rule — and the same mismatch is caught again, one layer later and much louder, by MigrationGenerationsMustShareEventTypeId thrown from the migration’s constructor at runtime.
[EventTypeGenerationFor<T>] was added specifically to make this class of mistake structurally impossible: with no independent id to type on the previous generation, there is nothing left to mismatch. This rule keeps checking the older style for callers who haven’t migrated to it yet, and additionally verifies that [EventTypeGenerationFor<T>] points at the right type — see CHR0049 for what happens when it doesn’t point at an event type at all.