Event Type Migrations
Event type migrations enable you to evolve your event schemas over time while maintaining compatibility with existing events. When an event type changes, you can define upcasters and downcasters that automatically transform events between different generations.
Why Migrations?
Section titled “Why Migrations?”In evolving systems, event schemas naturally change:
- Properties are added or removed
- Properties are renamed
- Complex properties are split or combined
Chronicle’s migration system allows you to:
- Define declarative transformation rules
- Automatically store all generations of an event when appending
- Read events in any generation format
Defining Migrations
Section titled “Defining Migrations”To define a migration, implement the IEventTypeMigrationFor<TEvent> interface:
public record AuthorRegisteredV1(string Name);
[EventType("author-registered", 2)]public record AuthorRegistered(string FirstName, string LastName);
public class AuthorRegisteredMigrator : IEventTypeMigrationFor<AuthorRegistered>{ public EventTypeGeneration From => 1; public EventTypeGeneration To => 2;
public void Upcast(IEventMigrationBuilder builder) { builder.Properties(pb => { pb.Split("FirstName", "Name", " ", 0); // FirstName from first part of Name pb.Split("LastName", "Name", " ", 1); // LastName from second part of Name }); }
public void Downcast(IEventMigrationBuilder builder) { builder.Properties(pb => { pb.Combine("Name", " ", "FirstName", "LastName"); // Combine back to Name }); }}Migration Operations
Section titled “Migration Operations”The migration builder supports the following operations:
Splits a source property into parts using a separator:
pb.Split("FirstName", "FullName", " ", 0); // Gets first partpb.Split("LastName", "FullName", " ", 1); // Gets second partCombine
Section titled “Combine”Combines multiple source properties into a single value using a separator:
pb.Combine("FullName", " ", "FirstName", "LastName"); // Joins with space separatorRename
Section titled “Rename”Renames a property from a previous name:
pb.RenamedFrom("NewPropertyName", "OldPropertyName");Default Value
Section titled “Default Value”Sets a default value for a new property:
pb.DefaultValue("RetryCount", 42);pb.DefaultValue("Description", "default string");How Migrations Work
Section titled “How Migrations Work”When an event is appended to the event store:
- Chronicle identifies the event’s current generation
- The migration system retrieves all registered migrations for the event type
- Upcasting: If there are higher generations, the event is transformed upward (1→2→3)
- Downcasting: If there are lower generations, the event is transformed downward (3→2→1)
- All generations are stored in the event sequence
This ensures that:
- Older consumers can still read events in their expected format
- Newer consumers can read events with the latest schema
- No data is lost during schema evolution
Registration
Section titled “Registration”Migrations are automatically discovered and registered when you connect to Chronicle.
Simply implement IEventTypeMigrationFor<TEvent> in your client application, and
Chronicle will:
- Discover all migrators via
IClientArtifactsProvider - Build migration definitions with JmesPath transformations
- Send the definitions to the kernel during event type registration
Best Practices
Section titled “Best Practices”- Incremental generations: Always migrate between consecutive generations (1→2, 2→3, not 1→3)
- Reversible transformations: Ensure downcast can recreate the original structure where possible
- Default values: Use
DefaultValue()for new properties that didn’t exist in older generations - Test migrations: Verify both upcast and downcast transformations work correctly
- Document changes: Keep track of what changed between generations in your event types