C# client usage
This guide covers how to declare event type migrations in a .NET client, the operations available to you, and what happens when your migrators are registered with the Chronicle Kernel.
Prerequisites
Section titled “Prerequisites”- A Chronicle-enabled .NET application
- An event type marked with
[EventType]that has evolved beyond generation 1
Marking event types with generations
Section titled “Marking event types with generations”Every [EventType] that has evolved past its first version must declare its current generation. You keep both the old and new record types in your codebase — Chronicle identifies them as the same event type by a shared event type id, not by C# class name.
using Cratis.Chronicle.Events;
// Generation 2 (current) — Name has been split into FirstName and LastName[EventType("dotnet-client-author-registered", generation: 2)]public record MigrationsDotnetClientAuthorRegistered(string FirstName, string LastName);
// Generation 1 (original) — marked as a previous generation of the current record above,// instead of carrying its own [EventType][EventTypeGenerationFor<MigrationsDotnetClientAuthorRegistered>(1)]public record MigrationsDotnetClientAuthorRegisteredV1(string Name);import io.cratis.chronicle.events.EventType
// Generation 2 (current) - Name has been split into firstName and lastName@EventType(id = "dotnet-client-author-registered", generation = 2)data class MigrationsDotnetClientAuthorRegistered(val firstName: String, val lastName: String)
// Generation 1 (original) - same explicit id as the current generation, only the generation differs@EventType(id = "dotnet-client-author-registered", generation = 1)data class MigrationsDotnetClientAuthorRegisteredV1(val name: String)import io.cratis.chronicle.events.EventType;
// Generation 2 (current) - name has been split into firstName and lastName@EventType(id = "dotnet-client-author-registered", generation = 2)record MigrationsDotnetClientAuthorRegistered(String firstName, String lastName) {}
// Generation 1 (original) - same explicit id as the current generation, only the generation differs@EventType(id = "dotnet-client-author-registered", generation = 1)record MigrationsDotnetClientAuthorRegisteredV1(String name) {}defmodule MyApp.Events.MigrationsDotnetClientAuthorRegisteredV1 do use Chronicle.Events.EventType, id: "dotnet-client-author-registered", generation: 1
defstruct [:name]end
defmodule MyApp.Events.MigrationsDotnetClientAuthorRegistered do use Chronicle.Events.EventType, id: "dotnet-client-author-registered", generation: 2
defstruct [:first_name, :last_name]endimport { eventType } from '@cratis/chronicle';
// Generation 2 (current) — Name has been split into FirstName and LastName@eventType('dotnet-client-author-registered', 2)class MigrationsDotnetClientAuthorRegistered { constructor(readonly firstName: string, readonly lastName: string) {}}
// Generation 1 (original) — same id, generation 1, kept only so the migration below can// upcast from it. It is not the "current" shape of the event any more.@eventType('dotnet-client-author-registered', 1)class MigrationsDotnetClientAuthorRegisteredV1 { constructor(readonly name: string) {}}The current generation, MigrationsDotnetClientAuthorRegistered, carries the real [EventType] with its explicit id. The previous generation, MigrationsDotnetClientAuthorRegisteredV1, carries [EventTypeGenerationFor<MigrationsDotnetClientAuthorRegistered>(1)] instead — it has no independent id of its own at all. Its event type id is resolved directly from the current generation’s [EventType], so the two can never end up with different ids no matter how the records are renamed later.
Defining a migrator
Section titled “Defining a migrator”Extend EventTypeMigration<TUpgrade, TPrevious> where TUpgrade is the newer generation and TPrevious is the older one:
using Cratis.Chronicle.Events.Migrations;
public class MigrationsDotnetClientAuthorRegisteredMigration : EventTypeMigration<MigrationsDotnetClientAuthorRegistered, MigrationsDotnetClientAuthorRegisteredV1>{ public override void Upcast(IEventMigrationBuilder<MigrationsDotnetClientAuthorRegistered, MigrationsDotnetClientAuthorRegisteredV1> builder) => builder.Properties(pb => pb .Split(m => m.FirstName, e => e.Name, PropertySeparator.Space, SplitPartIndex.First) .Split(m => m.LastName, e => e.Name, PropertySeparator.Space, SplitPartIndex.Second));
public override void Downcast(IEventMigrationBuilder<MigrationsDotnetClientAuthorRegisteredV1, MigrationsDotnetClientAuthorRegistered> builder) => builder.Properties(pb => pb .Combine(m => m.Name, PropertySeparator.Space, e => e.FirstName, e => e.LastName));}import io.cratis.chronicle.events.migrations.EventTypeMigrationimport io.cratis.chronicle.events.migrations.EventTypeMigrationBuilder
class MigrationsDotnetClientAuthorRegisteredMigration : EventTypeMigration<MigrationsDotnetClientAuthorRegistered, MigrationsDotnetClientAuthorRegisteredV1>( MigrationsDotnetClientAuthorRegistered::class, MigrationsDotnetClientAuthorRegisteredV1::class ) { override fun upcast(builder: EventTypeMigrationBuilder<MigrationsDotnetClientAuthorRegistered, MigrationsDotnetClientAuthorRegisteredV1>) { builder .split(MigrationsDotnetClientAuthorRegistered::firstName, MigrationsDotnetClientAuthorRegisteredV1::name, " ", 0) .split(MigrationsDotnetClientAuthorRegistered::lastName, MigrationsDotnetClientAuthorRegisteredV1::name, " ", 1) }
override fun downcast(builder: EventTypeMigrationBuilder<MigrationsDotnetClientAuthorRegisteredV1, MigrationsDotnetClientAuthorRegistered>) { builder.combine( MigrationsDotnetClientAuthorRegisteredV1::name, " ", MigrationsDotnetClientAuthorRegistered::firstName, MigrationsDotnetClientAuthorRegistered::lastName ) }}import io.cratis.chronicle.events.migrations.EventTypeMigration;import io.cratis.chronicle.events.migrations.EventTypeMigrationBuilder;
class MigrationsDotnetClientAuthorRegisteredMigration extends EventTypeMigration<MigrationsDotnetClientAuthorRegistered, MigrationsDotnetClientAuthorRegisteredV1> { MigrationsDotnetClientAuthorRegisteredMigration() { super(MigrationsDotnetClientAuthorRegistered.class, MigrationsDotnetClientAuthorRegisteredV1.class); }
@Override public void upcast(EventTypeMigrationBuilder<MigrationsDotnetClientAuthorRegistered, MigrationsDotnetClientAuthorRegisteredV1> builder) { builder .split("firstName", "name", " ", 0) .split("lastName", "name", " ", 1); }
@Override public void downcast(EventTypeMigrationBuilder<MigrationsDotnetClientAuthorRegisteredV1, MigrationsDotnetClientAuthorRegistered> builder) { builder.combine("name", " ", "firstName", "lastName"); }}defmodule MyApp.Events.MigrationsDotnetClientMigratorAuthorRegisteredV1 do use Chronicle.Events.EventType, id: "dotnet-client-migrator-author-registered", generation: 1
defstruct [:name]end
defmodule MyApp.Events.MigrationsDotnetClientMigratorAuthorRegistered do use Chronicle.Events.EventType, id: "dotnet-client-migrator-author-registered", generation: 2
defstruct [:first_name, :last_name]end
defmodule MyApp.Migrations.MigrationsDotnetClientMigratorAuthorRegisteredMigration do use Chronicle.Events.Migration, from: {MyApp.Events.MigrationsDotnetClientMigratorAuthorRegisteredV1, generation: 1}, to: {MyApp.Events.MigrationsDotnetClientMigratorAuthorRegistered, generation: 2}
alias Chronicle.Events.MigrationBuilder
@impl true def upcast(builder) do builder |> MigrationBuilder.split_property(:name, :first_name, " ", 0) |> MigrationBuilder.split_property(:name, :last_name, " ", 1) end
@impl true def downcast(builder) do MigrationBuilder.combine_properties(builder, [:first_name, :last_name], :name, " ") endendimport { eventTypeMigration, IEventMigrationBuilder, IEventTypeMigration } from '@cratis/chronicle';
@eventTypeMigration(MigrationsDotnetClientAuthorRegistered, MigrationsDotnetClientAuthorRegisteredV1)class MigrationsDotnetClientAuthorRegisteredMigration implements IEventTypeMigration<MigrationsDotnetClientAuthorRegistered, MigrationsDotnetClientAuthorRegisteredV1> { upcast(builder: IEventMigrationBuilder<MigrationsDotnetClientAuthorRegistered, MigrationsDotnetClientAuthorRegisteredV1>): void { builder.properties(propertyBuilder => propertyBuilder .split('firstName', 'name', ' ', 0) .split('lastName', 'name', ' ', 1)); }
downcast(builder: IEventMigrationBuilder<MigrationsDotnetClientAuthorRegisteredV1, MigrationsDotnetClientAuthorRegistered>): void { builder.properties(propertyBuilder => propertyBuilder .combine('name', ' ', 'firstName', 'lastName')); }}The From and To generation numbers are read automatically — from TPrevious’s [EventTypeGenerationFor<T>] (or its [EventType], if you’re using the old style) and from TUpgrade’s [EventType]. You do not declare them yourself. The base class constructor also validates, at that point, that both generations resolve to the same event type id (throwing MigrationGenerationsMustShareEventTypeId immediately if they don’t) and that To == From + 1 (throwing InvalidMigrationGenerationGap otherwise), preventing both an accidental identity mismatch and a generation gap.
Migrators are discovered automatically at startup — no explicit registration is needed. If two different migrator classes both claim to bridge the same generation pair for one event type, EventTypeMigrators.GetMigratorsFor throws MultipleMigratorsForSameEventTypeGeneration — exactly one migrator may own a given transition.
Migration operations
Section titled “Migration operations”All operations are called on the property builder inside builder.Properties(pb => ...). Each call declares one output property using a target property expression and one or more source property expressions drawn from the opposite generation’s record.
Extracts one segment of a string property by splitting on a separator and taking the part at a given index.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Migrations;
[EventType("dotnet-client-person-registered", generation: 2)]public record MigrationsDotnetClientSplitPersonRegistered(string FirstName, string LastName);
[EventTypeGenerationFor<MigrationsDotnetClientSplitPersonRegistered>(1)]public record MigrationsDotnetClientSplitPersonRegisteredV1(string FullName);
public class MigrationsDotnetClientSplitPersonRegisteredMigration : EventTypeMigration<MigrationsDotnetClientSplitPersonRegistered, MigrationsDotnetClientSplitPersonRegisteredV1>{ public override void Upcast(IEventMigrationBuilder<MigrationsDotnetClientSplitPersonRegistered, MigrationsDotnetClientSplitPersonRegisteredV1> builder) => builder.Properties(pb => pb .Split(t => t.FirstName, s => s.FullName, PropertySeparator.Space, SplitPartIndex.First) .Split(t => t.LastName, s => s.FullName, PropertySeparator.Space, SplitPartIndex.Second));
public override void Downcast(IEventMigrationBuilder<MigrationsDotnetClientSplitPersonRegisteredV1, MigrationsDotnetClientSplitPersonRegistered> builder) => builder.Properties(pb => pb .Combine(t => t.FullName, PropertySeparator.Space, s => s.FirstName, s => s.LastName));}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.events.migrations.EventTypeMigrationimport io.cratis.chronicle.events.migrations.EventTypeMigrationBuilder
@EventType(id = "dotnet-client-person-registered", generation = 2)data class MigrationsDotnetClientSplitPersonRegistered(val firstName: String, val lastName: String)
@EventType(id = "dotnet-client-person-registered", generation = 1)data class MigrationsDotnetClientSplitPersonRegisteredV1(val fullName: String)
class MigrationsDotnetClientSplitPersonRegisteredMigration : EventTypeMigration<MigrationsDotnetClientSplitPersonRegistered, MigrationsDotnetClientSplitPersonRegisteredV1>( MigrationsDotnetClientSplitPersonRegistered::class, MigrationsDotnetClientSplitPersonRegisteredV1::class ) { override fun upcast(builder: EventTypeMigrationBuilder<MigrationsDotnetClientSplitPersonRegistered, MigrationsDotnetClientSplitPersonRegisteredV1>) { builder .split(MigrationsDotnetClientSplitPersonRegistered::firstName, MigrationsDotnetClientSplitPersonRegisteredV1::fullName, " ", 0) .split(MigrationsDotnetClientSplitPersonRegistered::lastName, MigrationsDotnetClientSplitPersonRegisteredV1::fullName, " ", 1) }
override fun downcast(builder: EventTypeMigrationBuilder<MigrationsDotnetClientSplitPersonRegisteredV1, MigrationsDotnetClientSplitPersonRegistered>) { builder.combine( MigrationsDotnetClientSplitPersonRegisteredV1::fullName, " ", MigrationsDotnetClientSplitPersonRegistered::firstName, MigrationsDotnetClientSplitPersonRegistered::lastName ) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.events.migrations.EventTypeMigration;import io.cratis.chronicle.events.migrations.EventTypeMigrationBuilder;
@EventType(id = "dotnet-client-person-registered", generation = 2)record MigrationsDotnetClientSplitPersonRegistered(String firstName, String lastName) {}
@EventType(id = "dotnet-client-person-registered", generation = 1)record MigrationsDotnetClientSplitPersonRegisteredV1(String fullName) {}
class MigrationsDotnetClientSplitPersonRegisteredMigration extends EventTypeMigration<MigrationsDotnetClientSplitPersonRegistered, MigrationsDotnetClientSplitPersonRegisteredV1> { MigrationsDotnetClientSplitPersonRegisteredMigration() { super(MigrationsDotnetClientSplitPersonRegistered.class, MigrationsDotnetClientSplitPersonRegisteredV1.class); }
@Override public void upcast(EventTypeMigrationBuilder<MigrationsDotnetClientSplitPersonRegistered, MigrationsDotnetClientSplitPersonRegisteredV1> builder) { builder .split("firstName", "fullName", " ", 0) .split("lastName", "fullName", " ", 1); }
@Override public void downcast(EventTypeMigrationBuilder<MigrationsDotnetClientSplitPersonRegisteredV1, MigrationsDotnetClientSplitPersonRegistered> builder) { builder.combine("fullName", " ", "firstName", "lastName"); }}defmodule MyApp.Events.MigrationsDotnetClientSplitPersonRegisteredV1 do use Chronicle.Events.EventType, id: "dotnet-client-person-registered", generation: 1
defstruct [:full_name]end
defmodule MyApp.Events.MigrationsDotnetClientSplitPersonRegistered do use Chronicle.Events.EventType, id: "dotnet-client-person-registered", generation: 2
defstruct [:first_name, :last_name]end
defmodule MyApp.Migrations.MigrationsDotnetClientSplitPersonRegisteredMigration do use Chronicle.Events.Migration, from: {MyApp.Events.MigrationsDotnetClientSplitPersonRegisteredV1, generation: 1}, to: {MyApp.Events.MigrationsDotnetClientSplitPersonRegistered, generation: 2}
alias Chronicle.Events.MigrationBuilder
@impl true def upcast(builder) do builder |> MigrationBuilder.split_property(:full_name, :first_name, " ", 0) |> MigrationBuilder.split_property(:full_name, :last_name, " ", 1) end
@impl true def downcast(builder) do MigrationBuilder.combine_properties(builder, [:first_name, :last_name], :full_name, " ") endendimport { eventType, eventTypeMigration, IEventMigrationBuilder, IEventTypeMigration } from '@cratis/chronicle';
@eventType('dotnet-client-person-registered', 2)class MigrationsDotnetClientSplitPersonRegistered { constructor(readonly firstName: string, readonly lastName: string) {}}
@eventType('dotnet-client-person-registered', 1)class MigrationsDotnetClientSplitPersonRegisteredV1 { constructor(readonly fullName: string) {}}
@eventTypeMigration(MigrationsDotnetClientSplitPersonRegistered, MigrationsDotnetClientSplitPersonRegisteredV1)class MigrationsDotnetClientSplitPersonRegisteredMigration implements IEventTypeMigration<MigrationsDotnetClientSplitPersonRegistered, MigrationsDotnetClientSplitPersonRegisteredV1> { upcast(builder: IEventMigrationBuilder<MigrationsDotnetClientSplitPersonRegistered, MigrationsDotnetClientSplitPersonRegisteredV1>): void { builder.properties(propertyBuilder => propertyBuilder .split('firstName', 'fullName', ' ', 0) .split('lastName', 'fullName', ' ', 1)); }
downcast(builder: IEventMigrationBuilder<MigrationsDotnetClientSplitPersonRegisteredV1, MigrationsDotnetClientSplitPersonRegistered>): void { builder.properties(propertyBuilder => propertyBuilder .combine('fullName', ' ', 'firstName', 'lastName')); }}PropertySeparator.Space is a built-in constant. Any string is also implicitly convertible to a PropertySeparator, so ":" works directly for colon-delimited fields. SplitPartIndex.First (index 0) and SplitPartIndex.Second (index 1) cover the most common cases; pass any int for deeper splits.
Combine
Section titled “Combine”Concatenates multiple source properties into a single string target property, joining with a separator. The second argument is a PropertySeparator — use the built-in PropertySeparator.Space or pass any string (e.g. ":").
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Migrations;
[EventType("dotnet-client-shipping-address-recorded", generation: 2)]public record MigrationsDotnetClientCombineShippingAddressRecorded(string FullAddress);
[EventTypeGenerationFor<MigrationsDotnetClientCombineShippingAddressRecorded>(1)]public record MigrationsDotnetClientCombineShippingAddressRecordedV1(string Street, string City);
public class MigrationsDotnetClientCombineShippingAddressRecordedMigration : EventTypeMigration<MigrationsDotnetClientCombineShippingAddressRecorded, MigrationsDotnetClientCombineShippingAddressRecordedV1>{ public override void Upcast(IEventMigrationBuilder<MigrationsDotnetClientCombineShippingAddressRecorded, MigrationsDotnetClientCombineShippingAddressRecordedV1> builder) => builder.Properties(pb => pb .Combine(t => t.FullAddress, PropertySeparator.Space, s => s.Street, s => s.City));
public override void Downcast(IEventMigrationBuilder<MigrationsDotnetClientCombineShippingAddressRecordedV1, MigrationsDotnetClientCombineShippingAddressRecorded> builder) => builder.Properties(pb => pb .Split(t => t.Street, s => s.FullAddress, PropertySeparator.Space, SplitPartIndex.First) .Split(t => t.City, s => s.FullAddress, PropertySeparator.Space, SplitPartIndex.Second));}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.events.migrations.EventTypeMigrationimport io.cratis.chronicle.events.migrations.EventTypeMigrationBuilder
@EventType(id = "dotnet-client-shipping-address-recorded", generation = 2)data class MigrationsDotnetClientCombineShippingAddressRecorded(val fullAddress: String)
@EventType(id = "dotnet-client-shipping-address-recorded", generation = 1)data class MigrationsDotnetClientCombineShippingAddressRecordedV1(val street: String, val city: String)
class MigrationsDotnetClientCombineShippingAddressRecordedMigration : EventTypeMigration<MigrationsDotnetClientCombineShippingAddressRecorded, MigrationsDotnetClientCombineShippingAddressRecordedV1>( MigrationsDotnetClientCombineShippingAddressRecorded::class, MigrationsDotnetClientCombineShippingAddressRecordedV1::class ) { override fun upcast(builder: EventTypeMigrationBuilder<MigrationsDotnetClientCombineShippingAddressRecorded, MigrationsDotnetClientCombineShippingAddressRecordedV1>) { builder.combine( MigrationsDotnetClientCombineShippingAddressRecorded::fullAddress, " ", MigrationsDotnetClientCombineShippingAddressRecordedV1::street, MigrationsDotnetClientCombineShippingAddressRecordedV1::city ) }
override fun downcast(builder: EventTypeMigrationBuilder<MigrationsDotnetClientCombineShippingAddressRecordedV1, MigrationsDotnetClientCombineShippingAddressRecorded>) { builder .split(MigrationsDotnetClientCombineShippingAddressRecordedV1::street, MigrationsDotnetClientCombineShippingAddressRecorded::fullAddress, " ", 0) .split(MigrationsDotnetClientCombineShippingAddressRecordedV1::city, MigrationsDotnetClientCombineShippingAddressRecorded::fullAddress, " ", 1) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.events.migrations.EventTypeMigration;import io.cratis.chronicle.events.migrations.EventTypeMigrationBuilder;
@EventType(id = "dotnet-client-shipping-address-recorded", generation = 2)record MigrationsDotnetClientCombineShippingAddressRecorded(String fullAddress) {}
@EventType(id = "dotnet-client-shipping-address-recorded", generation = 1)record MigrationsDotnetClientCombineShippingAddressRecordedV1(String street, String city) {}
class MigrationsDotnetClientCombineShippingAddressRecordedMigration extends EventTypeMigration<MigrationsDotnetClientCombineShippingAddressRecorded, MigrationsDotnetClientCombineShippingAddressRecordedV1> { MigrationsDotnetClientCombineShippingAddressRecordedMigration() { super(MigrationsDotnetClientCombineShippingAddressRecorded.class, MigrationsDotnetClientCombineShippingAddressRecordedV1.class); }
@Override public void upcast(EventTypeMigrationBuilder<MigrationsDotnetClientCombineShippingAddressRecorded, MigrationsDotnetClientCombineShippingAddressRecordedV1> builder) { builder.combine("fullAddress", " ", "street", "city"); }
@Override public void downcast(EventTypeMigrationBuilder<MigrationsDotnetClientCombineShippingAddressRecordedV1, MigrationsDotnetClientCombineShippingAddressRecorded> builder) { builder .split("street", "fullAddress", " ", 0) .split("city", "fullAddress", " ", 1); }}defmodule MyApp.Events.MigrationsDotnetClientCombineShippingAddressRecordedV1 do use Chronicle.Events.EventType, id: "dotnet-client-shipping-address-recorded", generation: 1
defstruct [:street, :city]end
defmodule MyApp.Events.MigrationsDotnetClientCombineShippingAddressRecorded do use Chronicle.Events.EventType, id: "dotnet-client-shipping-address-recorded", generation: 2
defstruct [:full_address]end
defmodule MyApp.Migrations.MigrationsDotnetClientCombineShippingAddressRecordedMigration do use Chronicle.Events.Migration, from: {MyApp.Events.MigrationsDotnetClientCombineShippingAddressRecordedV1, generation: 1}, to: {MyApp.Events.MigrationsDotnetClientCombineShippingAddressRecorded, generation: 2}
alias Chronicle.Events.MigrationBuilder
@impl true def upcast(builder) do MigrationBuilder.combine_properties(builder, [:street, :city], :full_address, " ") end
@impl true def downcast(builder) do builder |> MigrationBuilder.split_property(:full_address, :street, " ", 0) |> MigrationBuilder.split_property(:full_address, :city, " ", 1) endendimport { eventType, eventTypeMigration, IEventMigrationBuilder, IEventTypeMigration } from '@cratis/chronicle';
@eventType('dotnet-client-shipping-address-recorded', 2)class MigrationsDotnetClientCombineShippingAddressRecorded { constructor(readonly fullAddress: string) {}}
@eventType('dotnet-client-shipping-address-recorded', 1)class MigrationsDotnetClientCombineShippingAddressRecordedV1 { constructor(readonly street: string, readonly city: string) {}}
@eventTypeMigration(MigrationsDotnetClientCombineShippingAddressRecorded, MigrationsDotnetClientCombineShippingAddressRecordedV1)class MigrationsDotnetClientCombineShippingAddressRecordedMigration implements IEventTypeMigration<MigrationsDotnetClientCombineShippingAddressRecorded, MigrationsDotnetClientCombineShippingAddressRecordedV1> { upcast(builder: IEventMigrationBuilder<MigrationsDotnetClientCombineShippingAddressRecorded, MigrationsDotnetClientCombineShippingAddressRecordedV1>): void { builder.properties(propertyBuilder => propertyBuilder .combine('fullAddress', ' ', 'street', 'city')); }
downcast(builder: IEventMigrationBuilder<MigrationsDotnetClientCombineShippingAddressRecordedV1, MigrationsDotnetClientCombineShippingAddressRecorded>): void { builder.properties(propertyBuilder => propertyBuilder .split('street', 'fullAddress', ' ', 0) .split('city', 'fullAddress', ' ', 1)); }}RenamedFrom
Section titled “RenamedFrom”Maps a property from its old name in the source generation to its new name in the target generation.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Migrations;
[EventType("dotnet-client-customer-registered", generation: 2)]public record MigrationsDotnetClientRenamedFromCustomerRegistered(string Email);
[EventTypeGenerationFor<MigrationsDotnetClientRenamedFromCustomerRegistered>(1)]public record MigrationsDotnetClientRenamedFromCustomerRegisteredV1(string EmailAddress);
public class MigrationsDotnetClientRenamedFromCustomerRegisteredMigration : EventTypeMigration<MigrationsDotnetClientRenamedFromCustomerRegistered, MigrationsDotnetClientRenamedFromCustomerRegisteredV1>{ public override void Upcast(IEventMigrationBuilder<MigrationsDotnetClientRenamedFromCustomerRegistered, MigrationsDotnetClientRenamedFromCustomerRegisteredV1> builder) => builder.Properties(pb => pb .RenamedFrom(t => t.Email, s => s.EmailAddress));
public override void Downcast(IEventMigrationBuilder<MigrationsDotnetClientRenamedFromCustomerRegisteredV1, MigrationsDotnetClientRenamedFromCustomerRegistered> builder) => builder.Properties(pb => pb .RenamedFrom(t => t.EmailAddress, s => s.Email));}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.events.migrations.EventTypeMigrationimport io.cratis.chronicle.events.migrations.EventTypeMigrationBuilder
@EventType(id = "dotnet-client-customer-registered", generation = 2)data class MigrationsDotnetClientRenamedFromCustomerRegistered(val email: String)
@EventType(id = "dotnet-client-customer-registered", generation = 1)data class MigrationsDotnetClientRenamedFromCustomerRegisteredV1(val emailAddress: String)
class MigrationsDotnetClientRenamedFromCustomerRegisteredMigration : EventTypeMigration<MigrationsDotnetClientRenamedFromCustomerRegistered, MigrationsDotnetClientRenamedFromCustomerRegisteredV1>( MigrationsDotnetClientRenamedFromCustomerRegistered::class, MigrationsDotnetClientRenamedFromCustomerRegisteredV1::class ) { override fun upcast(builder: EventTypeMigrationBuilder<MigrationsDotnetClientRenamedFromCustomerRegistered, MigrationsDotnetClientRenamedFromCustomerRegisteredV1>) { builder.renamedFrom(MigrationsDotnetClientRenamedFromCustomerRegistered::email, MigrationsDotnetClientRenamedFromCustomerRegisteredV1::emailAddress) }
override fun downcast(builder: EventTypeMigrationBuilder<MigrationsDotnetClientRenamedFromCustomerRegisteredV1, MigrationsDotnetClientRenamedFromCustomerRegistered>) { builder.renamedFrom(MigrationsDotnetClientRenamedFromCustomerRegisteredV1::emailAddress, MigrationsDotnetClientRenamedFromCustomerRegistered::email) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.events.migrations.EventTypeMigration;import io.cratis.chronicle.events.migrations.EventTypeMigrationBuilder;
@EventType(id = "dotnet-client-customer-registered", generation = 2)record MigrationsDotnetClientRenamedFromCustomerRegistered(String email) {}
@EventType(id = "dotnet-client-customer-registered", generation = 1)record MigrationsDotnetClientRenamedFromCustomerRegisteredV1(String emailAddress) {}
class MigrationsDotnetClientRenamedFromCustomerRegisteredMigration extends EventTypeMigration<MigrationsDotnetClientRenamedFromCustomerRegistered, MigrationsDotnetClientRenamedFromCustomerRegisteredV1> { MigrationsDotnetClientRenamedFromCustomerRegisteredMigration() { super(MigrationsDotnetClientRenamedFromCustomerRegistered.class, MigrationsDotnetClientRenamedFromCustomerRegisteredV1.class); }
@Override public void upcast(EventTypeMigrationBuilder<MigrationsDotnetClientRenamedFromCustomerRegistered, MigrationsDotnetClientRenamedFromCustomerRegisteredV1> builder) { builder.renamedFrom("email", "emailAddress"); }
@Override public void downcast(EventTypeMigrationBuilder<MigrationsDotnetClientRenamedFromCustomerRegisteredV1, MigrationsDotnetClientRenamedFromCustomerRegistered> builder) { builder.renamedFrom("emailAddress", "email"); }}defmodule MyApp.Events.MigrationsDotnetClientRenamedFromCustomerRegisteredV1 do use Chronicle.Events.EventType, id: "dotnet-client-customer-registered", generation: 1
defstruct [:email_address]end
defmodule MyApp.Events.MigrationsDotnetClientRenamedFromCustomerRegistered do use Chronicle.Events.EventType, id: "dotnet-client-customer-registered", generation: 2
defstruct [:email]end
defmodule MyApp.Migrations.MigrationsDotnetClientRenamedFromCustomerRegisteredMigration do use Chronicle.Events.Migration, from: {MyApp.Events.MigrationsDotnetClientRenamedFromCustomerRegisteredV1, generation: 1}, to: {MyApp.Events.MigrationsDotnetClientRenamedFromCustomerRegistered, generation: 2}
alias Chronicle.Events.MigrationBuilder
@impl true def upcast(builder), do: MigrationBuilder.renamed_from(builder, :email, :email_address)
@impl true def downcast(builder), do: MigrationBuilder.renamed_from(builder, :email_address, :email)endimport { eventType, eventTypeMigration, IEventMigrationBuilder, IEventTypeMigration } from '@cratis/chronicle';
@eventType('dotnet-client-customer-registered', 2)class MigrationsDotnetClientRenamedFromCustomerRegistered { constructor(readonly email: string) {}}
@eventType('dotnet-client-customer-registered', 1)class MigrationsDotnetClientRenamedFromCustomerRegisteredV1 { constructor(readonly emailAddress: string) {}}
@eventTypeMigration(MigrationsDotnetClientRenamedFromCustomerRegistered, MigrationsDotnetClientRenamedFromCustomerRegisteredV1)class MigrationsDotnetClientRenamedFromCustomerRegisteredMigration implements IEventTypeMigration<MigrationsDotnetClientRenamedFromCustomerRegistered, MigrationsDotnetClientRenamedFromCustomerRegisteredV1> { upcast(builder: IEventMigrationBuilder<MigrationsDotnetClientRenamedFromCustomerRegistered, MigrationsDotnetClientRenamedFromCustomerRegisteredV1>): void { builder.properties(propertyBuilder => propertyBuilder .renamedFrom('email', 'emailAddress')); }
downcast(builder: IEventMigrationBuilder<MigrationsDotnetClientRenamedFromCustomerRegisteredV1, MigrationsDotnetClientRenamedFromCustomerRegistered>): void { builder.properties(propertyBuilder => propertyBuilder .renamedFrom('emailAddress', 'email')); }}DefaultValue
Section titled “DefaultValue”Provides a literal default for a property that did not exist in the source generation. Chronicle applies this value to any event stored before the property was introduced.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Migrations;
[EventType("dotnet-client-task-created", generation: 2)]public record MigrationsDotnetClientDefaultValueTaskCreated(string Title, string Status, int RetryCount, bool Enabled);
[EventTypeGenerationFor<MigrationsDotnetClientDefaultValueTaskCreated>(1)]public record MigrationsDotnetClientDefaultValueTaskCreatedV1(string Title);
public class MigrationsDotnetClientDefaultValueTaskCreatedMigration : EventTypeMigration<MigrationsDotnetClientDefaultValueTaskCreated, MigrationsDotnetClientDefaultValueTaskCreatedV1>{ public override void Upcast(IEventMigrationBuilder<MigrationsDotnetClientDefaultValueTaskCreated, MigrationsDotnetClientDefaultValueTaskCreatedV1> builder) => builder.Properties(pb => pb .DefaultValue(t => t.Status, "active") .DefaultValue(t => t.RetryCount, 0) .DefaultValue(t => t.Enabled, true));
public override void Downcast(IEventMigrationBuilder<MigrationsDotnetClientDefaultValueTaskCreatedV1, MigrationsDotnetClientDefaultValueTaskCreated> builder) { // Status, RetryCount, and Enabled did not exist in generation 1 — nothing to map back }}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.events.migrations.EventTypeMigrationimport io.cratis.chronicle.events.migrations.EventTypeMigrationBuilder
@EventType(id = "dotnet-client-task-created", generation = 2)data class MigrationsDotnetClientDefaultValueTaskCreated( val title: String, val status: String, val retryCount: Int, val enabled: Boolean)
@EventType(id = "dotnet-client-task-created", generation = 1)data class MigrationsDotnetClientDefaultValueTaskCreatedV1(val title: String)
class MigrationsDotnetClientDefaultValueTaskCreatedMigration : EventTypeMigration<MigrationsDotnetClientDefaultValueTaskCreated, MigrationsDotnetClientDefaultValueTaskCreatedV1>( MigrationsDotnetClientDefaultValueTaskCreated::class, MigrationsDotnetClientDefaultValueTaskCreatedV1::class ) { override fun upcast(builder: EventTypeMigrationBuilder<MigrationsDotnetClientDefaultValueTaskCreated, MigrationsDotnetClientDefaultValueTaskCreatedV1>) { builder .defaultValue(MigrationsDotnetClientDefaultValueTaskCreated::status, "active") .defaultValue(MigrationsDotnetClientDefaultValueTaskCreated::retryCount, 0) .defaultValue(MigrationsDotnetClientDefaultValueTaskCreated::enabled, true) }
override fun downcast(builder: EventTypeMigrationBuilder<MigrationsDotnetClientDefaultValueTaskCreatedV1, MigrationsDotnetClientDefaultValueTaskCreated>) { // status, retryCount, and enabled did not exist in generation 1 - nothing to map back }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.events.migrations.EventTypeMigration;import io.cratis.chronicle.events.migrations.EventTypeMigrationBuilder;
@EventType(id = "dotnet-client-task-created", generation = 2)record MigrationsDotnetClientDefaultValueTaskCreated( String title, String status, int retryCount, boolean enabled) {}
@EventType(id = "dotnet-client-task-created", generation = 1)record MigrationsDotnetClientDefaultValueTaskCreatedV1(String title) {}
class MigrationsDotnetClientDefaultValueTaskCreatedMigration extends EventTypeMigration<MigrationsDotnetClientDefaultValueTaskCreated, MigrationsDotnetClientDefaultValueTaskCreatedV1> { MigrationsDotnetClientDefaultValueTaskCreatedMigration() { super(MigrationsDotnetClientDefaultValueTaskCreated.class, MigrationsDotnetClientDefaultValueTaskCreatedV1.class); }
@Override public void upcast(EventTypeMigrationBuilder<MigrationsDotnetClientDefaultValueTaskCreated, MigrationsDotnetClientDefaultValueTaskCreatedV1> builder) { builder .defaultValue("status", "active") .defaultValue("retryCount", 0) .defaultValue("enabled", true); }
@Override public void downcast(EventTypeMigrationBuilder<MigrationsDotnetClientDefaultValueTaskCreatedV1, MigrationsDotnetClientDefaultValueTaskCreated> builder) { // status, retryCount, and enabled did not exist in generation 1 - nothing to map back }}defmodule MyApp.Events.MigrationsDotnetClientDefaultValueTaskCreatedV1 do use Chronicle.Events.EventType, id: "dotnet-client-task-created", generation: 1
defstruct [:title]end
defmodule MyApp.Events.MigrationsDotnetClientDefaultValueTaskCreated do use Chronicle.Events.EventType, id: "dotnet-client-task-created", generation: 2
defstruct [:title, :status, :retry_count, :enabled]end
defmodule MyApp.Migrations.MigrationsDotnetClientDefaultValueTaskCreatedMigration do use Chronicle.Events.Migration, from: {MyApp.Events.MigrationsDotnetClientDefaultValueTaskCreatedV1, generation: 1}, to: {MyApp.Events.MigrationsDotnetClientDefaultValueTaskCreated, generation: 2}
alias Chronicle.Events.MigrationBuilder
@impl true def upcast(builder) do builder |> MigrationBuilder.default_value(:status, "active") |> MigrationBuilder.default_value(:retry_count, 0) |> MigrationBuilder.default_value(:enabled, true) end
@impl true def downcast(builder) do # status, retry_count, and enabled did not exist in generation 1 — nothing to map back builder endendimport { eventType, eventTypeMigration, IEventMigrationBuilder, IEventTypeMigration } from '@cratis/chronicle';
@eventType('dotnet-client-task-created', 2)class MigrationsDotnetClientDefaultValueTaskCreated { constructor( readonly title: string, readonly status: string, readonly retryCount: number, readonly enabled: boolean ) {}}
@eventType('dotnet-client-task-created', 1)class MigrationsDotnetClientDefaultValueTaskCreatedV1 { constructor(readonly title: string) {}}
@eventTypeMigration(MigrationsDotnetClientDefaultValueTaskCreated, MigrationsDotnetClientDefaultValueTaskCreatedV1)class MigrationsDotnetClientDefaultValueTaskCreatedMigration implements IEventTypeMigration<MigrationsDotnetClientDefaultValueTaskCreated, MigrationsDotnetClientDefaultValueTaskCreatedV1> { upcast(builder: IEventMigrationBuilder<MigrationsDotnetClientDefaultValueTaskCreated, MigrationsDotnetClientDefaultValueTaskCreatedV1>): void { builder.properties(propertyBuilder => propertyBuilder .defaultValue('status', 'active') .defaultValue('retryCount', 0) .defaultValue('enabled', true)); }
downcast(builder: IEventMigrationBuilder<MigrationsDotnetClientDefaultValueTaskCreatedV1, MigrationsDotnetClientDefaultValueTaskCreated>): void { // status, retryCount, and enabled did not exist in generation 1 — nothing to map back }}MapValues
Section titled “MapValues”The four operations above move values between properties. MapValues changes the values themselves — it says which value in one generation is which value in the other, which is what you need when an enum is renumbered or a code set is replaced wholesale.
Unlike the property-builder operations, a value map is declared by overriding MapValues on the migration:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Migrations;
public enum MigrationsDotnetClientPaymentStatusV1{ Pending = 0, Settled = 1}
public enum MigrationsDotnetClientPaymentStatus{ Awaiting = 10, Completed = 11}
[EventType("dotnet-client-payment-processed", generation: 2)]public record MigrationsDotnetClientPaymentProcessed(MigrationsDotnetClientPaymentStatus Status);
[EventTypeGenerationFor<MigrationsDotnetClientPaymentProcessed>(1)]public record MigrationsDotnetClientPaymentProcessedV1(MigrationsDotnetClientPaymentStatusV1 Status);
public class MigrationsDotnetClientPaymentProcessedMigration : EventTypeMigration<MigrationsDotnetClientPaymentProcessed, MigrationsDotnetClientPaymentProcessedV1>{ public override void Upcast(IEventMigrationBuilder<MigrationsDotnetClientPaymentProcessed, MigrationsDotnetClientPaymentProcessedV1> builder) { // Status is covered by the value map }
public override void Downcast(IEventMigrationBuilder<MigrationsDotnetClientPaymentProcessedV1, MigrationsDotnetClientPaymentProcessed> builder) { // Status is covered by the value map }
public override void MapValues(IEventValueMapBuilder<MigrationsDotnetClientPaymentProcessed, MigrationsDotnetClientPaymentProcessedV1> builder) => builder.For(current => current.Status, previous => previous.Status, map => map .Map(MigrationsDotnetClientPaymentStatusV1.Pending, MigrationsDotnetClientPaymentStatus.Awaiting) .Map(MigrationsDotnetClientPaymentStatusV1.Settled, MigrationsDotnetClientPaymentStatus.Completed));}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.The map is applied forward when upcasting and inverted when downcasting, so you state it once. Values it does not mention are carried across unchanged, and two values collapsing onto one take the first pair declared for that value on the way back.
MapValues runs before Upcast and Downcast, so a direction that declares its own transformation for the same property keeps it. That is also the way to express a translation that is not symmetric — several values collapsing onto one, where the way back has to make a choice the forward map cannot state. Declare that as a MapValues on the property builder, per direction:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Migrations;
public enum MigrationsDotnetClientDeclineReasonV1{ InsufficientFunds = 0, CardExpired = 1, CardReported = 2}
public enum MigrationsDotnetClientDeclineReason{ Funds = 0, Card = 1}
[EventType("dotnet-client-payment-declined", generation: 2)]public record MigrationsDotnetClientPaymentDeclined(MigrationsDotnetClientDeclineReason Reason);
[EventTypeGenerationFor<MigrationsDotnetClientPaymentDeclined>(1)]public record MigrationsDotnetClientPaymentDeclinedV1(MigrationsDotnetClientDeclineReasonV1 Reason);
public class MigrationsDotnetClientPaymentDeclinedMigration : EventTypeMigration<MigrationsDotnetClientPaymentDeclined, MigrationsDotnetClientPaymentDeclinedV1>{ public override void Upcast(IEventMigrationBuilder<MigrationsDotnetClientPaymentDeclined, MigrationsDotnetClientPaymentDeclinedV1> builder) => builder.Properties(pb => pb .MapValues(current => current.Reason, previous => previous.Reason, map => map .Map(MigrationsDotnetClientDeclineReasonV1.InsufficientFunds, MigrationsDotnetClientDeclineReason.Funds) .Map(MigrationsDotnetClientDeclineReasonV1.CardExpired, MigrationsDotnetClientDeclineReason.Card) .Map(MigrationsDotnetClientDeclineReasonV1.CardReported, MigrationsDotnetClientDeclineReason.Card)));
public override void Downcast(IEventMigrationBuilder<MigrationsDotnetClientPaymentDeclinedV1, MigrationsDotnetClientPaymentDeclined> builder) => builder.Properties(pb => pb .MapValues(previous => previous.Reason, current => current.Reason, map => map .Map(MigrationsDotnetClientDeclineReason.Funds, MigrationsDotnetClientDeclineReasonV1.InsufficientFunds) .Map(MigrationsDotnetClientDeclineReason.Card, MigrationsDotnetClientDeclineReasonV1.CardExpired)));}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Multi-generation migrations
Section titled “Multi-generation migrations”If your event type spans more than two generations, define one migrator per generation pair. Chronicle chains them automatically.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Migrations;
[EventType("dotnet-client-multi-gen-person-registered", generation: 3)]public record MigrationsDotnetClientMultiGenPersonRegistered(string Email, string FirstName, string LastName);
[EventTypeGenerationFor<MigrationsDotnetClientMultiGenPersonRegistered>(2)]public record MigrationsDotnetClientMultiGenPersonRegisteredV2(string Email, string Name);
[EventTypeGenerationFor<MigrationsDotnetClientMultiGenPersonRegistered>(1)]public record MigrationsDotnetClientMultiGenPersonRegisteredV1(string EmailAddress, string Name);
// Generation 1 → 2: rename EmailAddress to Emailpublic class MigrationsDotnetClientMultiGenPersonRegisteredV1ToV2 : EventTypeMigration<MigrationsDotnetClientMultiGenPersonRegisteredV2, MigrationsDotnetClientMultiGenPersonRegisteredV1>{ public override void Upcast(IEventMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegisteredV2, MigrationsDotnetClientMultiGenPersonRegisteredV1> builder) => builder.Properties(pb => pb .RenamedFrom(t => t.Email, s => s.EmailAddress));
public override void Downcast(IEventMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegisteredV1, MigrationsDotnetClientMultiGenPersonRegisteredV2> builder) => builder.Properties(pb => pb .RenamedFrom(t => t.EmailAddress, s => s.Email));}
// Generation 2 → 3: split Name into FirstName / LastNamepublic class MigrationsDotnetClientMultiGenPersonRegisteredV2ToV3 : EventTypeMigration<MigrationsDotnetClientMultiGenPersonRegistered, MigrationsDotnetClientMultiGenPersonRegisteredV2>{ public override void Upcast(IEventMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegistered, MigrationsDotnetClientMultiGenPersonRegisteredV2> builder) => builder.Properties(pb => pb .Split(t => t.FirstName, s => s.Name, PropertySeparator.Space, SplitPartIndex.First) .Split(t => t.LastName, s => s.Name, PropertySeparator.Space, SplitPartIndex.Second));
public override void Downcast(IEventMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegisteredV2, MigrationsDotnetClientMultiGenPersonRegistered> builder) => builder.Properties(pb => pb .Combine(t => t.Name, PropertySeparator.Space, s => s.FirstName, s => s.LastName));}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.events.migrations.EventTypeMigrationimport io.cratis.chronicle.events.migrations.EventTypeMigrationBuilder
@EventType(id = "dotnet-client-multi-gen-person-registered", generation = 3)data class MigrationsDotnetClientMultiGenPersonRegistered(val email: String, val firstName: String, val lastName: String)
@EventType(id = "dotnet-client-multi-gen-person-registered", generation = 2)data class MigrationsDotnetClientMultiGenPersonRegisteredV2(val email: String, val name: String)
@EventType(id = "dotnet-client-multi-gen-person-registered", generation = 1)data class MigrationsDotnetClientMultiGenPersonRegisteredV1(val emailAddress: String, val name: String)
// Generation 1 -> 2: rename emailAddress to emailclass MigrationsDotnetClientMultiGenPersonRegisteredV1ToV2 : EventTypeMigration<MigrationsDotnetClientMultiGenPersonRegisteredV2, MigrationsDotnetClientMultiGenPersonRegisteredV1>( MigrationsDotnetClientMultiGenPersonRegisteredV2::class, MigrationsDotnetClientMultiGenPersonRegisteredV1::class ) { override fun upcast(builder: EventTypeMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegisteredV2, MigrationsDotnetClientMultiGenPersonRegisteredV1>) { builder.renamedFrom(MigrationsDotnetClientMultiGenPersonRegisteredV2::email, MigrationsDotnetClientMultiGenPersonRegisteredV1::emailAddress) }
override fun downcast(builder: EventTypeMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegisteredV1, MigrationsDotnetClientMultiGenPersonRegisteredV2>) { builder.renamedFrom(MigrationsDotnetClientMultiGenPersonRegisteredV1::emailAddress, MigrationsDotnetClientMultiGenPersonRegisteredV2::email) }}
// Generation 2 -> 3: split name into firstName / lastNameclass MigrationsDotnetClientMultiGenPersonRegisteredV2ToV3 : EventTypeMigration<MigrationsDotnetClientMultiGenPersonRegistered, MigrationsDotnetClientMultiGenPersonRegisteredV2>( MigrationsDotnetClientMultiGenPersonRegistered::class, MigrationsDotnetClientMultiGenPersonRegisteredV2::class ) { override fun upcast(builder: EventTypeMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegistered, MigrationsDotnetClientMultiGenPersonRegisteredV2>) { builder .split(MigrationsDotnetClientMultiGenPersonRegistered::firstName, MigrationsDotnetClientMultiGenPersonRegisteredV2::name, " ", 0) .split(MigrationsDotnetClientMultiGenPersonRegistered::lastName, MigrationsDotnetClientMultiGenPersonRegisteredV2::name, " ", 1) }
override fun downcast(builder: EventTypeMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegisteredV2, MigrationsDotnetClientMultiGenPersonRegistered>) { builder.combine( MigrationsDotnetClientMultiGenPersonRegisteredV2::name, " ", MigrationsDotnetClientMultiGenPersonRegistered::firstName, MigrationsDotnetClientMultiGenPersonRegistered::lastName ) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.events.migrations.EventTypeMigration;import io.cratis.chronicle.events.migrations.EventTypeMigrationBuilder;
@EventType(id = "dotnet-client-multi-gen-person-registered", generation = 3)record MigrationsDotnetClientMultiGenPersonRegistered(String email, String firstName, String lastName) {}
@EventType(id = "dotnet-client-multi-gen-person-registered", generation = 2)record MigrationsDotnetClientMultiGenPersonRegisteredV2(String email, String name) {}
@EventType(id = "dotnet-client-multi-gen-person-registered", generation = 1)record MigrationsDotnetClientMultiGenPersonRegisteredV1(String emailAddress, String name) {}
// Generation 1 -> 2: rename emailAddress to emailclass MigrationsDotnetClientMultiGenPersonRegisteredV1ToV2 extends EventTypeMigration<MigrationsDotnetClientMultiGenPersonRegisteredV2, MigrationsDotnetClientMultiGenPersonRegisteredV1> { MigrationsDotnetClientMultiGenPersonRegisteredV1ToV2() { super(MigrationsDotnetClientMultiGenPersonRegisteredV2.class, MigrationsDotnetClientMultiGenPersonRegisteredV1.class); }
@Override public void upcast(EventTypeMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegisteredV2, MigrationsDotnetClientMultiGenPersonRegisteredV1> builder) { builder.renamedFrom("email", "emailAddress"); }
@Override public void downcast(EventTypeMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegisteredV1, MigrationsDotnetClientMultiGenPersonRegisteredV2> builder) { builder.renamedFrom("emailAddress", "email"); }}
// Generation 2 -> 3: split name into firstName / lastNameclass MigrationsDotnetClientMultiGenPersonRegisteredV2ToV3 extends EventTypeMigration<MigrationsDotnetClientMultiGenPersonRegistered, MigrationsDotnetClientMultiGenPersonRegisteredV2> { MigrationsDotnetClientMultiGenPersonRegisteredV2ToV3() { super(MigrationsDotnetClientMultiGenPersonRegistered.class, MigrationsDotnetClientMultiGenPersonRegisteredV2.class); }
@Override public void upcast(EventTypeMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegistered, MigrationsDotnetClientMultiGenPersonRegisteredV2> builder) { builder .split("firstName", "name", " ", 0) .split("lastName", "name", " ", 1); }
@Override public void downcast(EventTypeMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegisteredV2, MigrationsDotnetClientMultiGenPersonRegistered> builder) { builder.combine("name", " ", "firstName", "lastName"); }}defmodule MyApp.Events.MigrationsDotnetClientMultiGenPersonRegisteredV1 do use Chronicle.Events.EventType, id: "dotnet-client-multi-gen-person-registered", generation: 1
defstruct [:email_address, :name]end
defmodule MyApp.Events.MigrationsDotnetClientMultiGenPersonRegisteredV2 do use Chronicle.Events.EventType, id: "dotnet-client-multi-gen-person-registered", generation: 2
defstruct [:email, :name]end
defmodule MyApp.Events.MigrationsDotnetClientMultiGenPersonRegistered do use Chronicle.Events.EventType, id: "dotnet-client-multi-gen-person-registered", generation: 3
defstruct [:email, :first_name, :last_name]end
# Generation 1 -> 2: rename email_address to emaildefmodule MyApp.Migrations.MigrationsDotnetClientMultiGenPersonRegisteredV1ToV2 do use Chronicle.Events.Migration, from: {MyApp.Events.MigrationsDotnetClientMultiGenPersonRegisteredV1, generation: 1}, to: {MyApp.Events.MigrationsDotnetClientMultiGenPersonRegisteredV2, generation: 2}
alias Chronicle.Events.MigrationBuilder
@impl true def upcast(builder), do: MigrationBuilder.renamed_from(builder, :email, :email_address)
@impl true def downcast(builder), do: MigrationBuilder.renamed_from(builder, :email_address, :email)end
# Generation 2 -> 3: split name into first_name / last_namedefmodule MyApp.Migrations.MigrationsDotnetClientMultiGenPersonRegisteredV2ToV3 do use Chronicle.Events.Migration, from: {MyApp.Events.MigrationsDotnetClientMultiGenPersonRegisteredV2, generation: 2}, to: {MyApp.Events.MigrationsDotnetClientMultiGenPersonRegistered, generation: 3}
alias Chronicle.Events.MigrationBuilder
@impl true def upcast(builder) do builder |> MigrationBuilder.split_property(:name, :first_name, " ", 0) |> MigrationBuilder.split_property(:name, :last_name, " ", 1) end
@impl true def downcast(builder) do MigrationBuilder.combine_properties(builder, [:first_name, :last_name], :name, " ") endendimport { eventType, eventTypeMigration, IEventMigrationBuilder, IEventTypeMigration } from '@cratis/chronicle';
@eventType('dotnet-client-multi-gen-person-registered', 3)class MigrationsDotnetClientMultiGenPersonRegistered { constructor(readonly email: string, readonly firstName: string, readonly lastName: string) {}}
@eventType('dotnet-client-multi-gen-person-registered', 2)class MigrationsDotnetClientMultiGenPersonRegisteredV2 { constructor(readonly email: string, readonly name: string) {}}
@eventType('dotnet-client-multi-gen-person-registered', 1)class MigrationsDotnetClientMultiGenPersonRegisteredV1 { constructor(readonly emailAddress: string, readonly name: string) {}}
// Generation 1 → 2: rename emailAddress to email@eventTypeMigration(MigrationsDotnetClientMultiGenPersonRegisteredV2, MigrationsDotnetClientMultiGenPersonRegisteredV1)class MigrationsDotnetClientMultiGenPersonRegisteredV1ToV2 implements IEventTypeMigration<MigrationsDotnetClientMultiGenPersonRegisteredV2, MigrationsDotnetClientMultiGenPersonRegisteredV1> { upcast(builder: IEventMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegisteredV2, MigrationsDotnetClientMultiGenPersonRegisteredV1>): void { builder.properties(propertyBuilder => propertyBuilder .renamedFrom('email', 'emailAddress')); }
downcast(builder: IEventMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegisteredV1, MigrationsDotnetClientMultiGenPersonRegisteredV2>): void { builder.properties(propertyBuilder => propertyBuilder .renamedFrom('emailAddress', 'email')); }}
// Generation 2 → 3: split name into firstName / lastName@eventTypeMigration(MigrationsDotnetClientMultiGenPersonRegistered, MigrationsDotnetClientMultiGenPersonRegisteredV2)class MigrationsDotnetClientMultiGenPersonRegisteredV2ToV3 implements IEventTypeMigration<MigrationsDotnetClientMultiGenPersonRegistered, MigrationsDotnetClientMultiGenPersonRegisteredV2> { upcast(builder: IEventMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegistered, MigrationsDotnetClientMultiGenPersonRegisteredV2>): void { builder.properties(propertyBuilder => propertyBuilder .split('firstName', 'name', ' ', 0) .split('lastName', 'name', ' ', 1)); }
downcast(builder: IEventMigrationBuilder<MigrationsDotnetClientMultiGenPersonRegisteredV2, MigrationsDotnetClientMultiGenPersonRegistered>): void { builder.properties(propertyBuilder => propertyBuilder .combine('name', ' ', 'firstName', 'lastName')); }}When a generation 1 event arrives, the Kernel chains the upcasts: 1→2, then 2→3, and stores all three generations.
How registration works
Section titled “How registration works”When your application connects to Chronicle, the client:
- Discovers all
EventTypeMigration<TUpgrade, TPrevious>implementations viaIClientArtifactsProvider - Invokes
UpcastandDowncaston each migrator to capture the transformation declarations - Converts the declarations into JmesPath expressions
- Sends the complete
EventTypeDefinition— including all generations and their migration definitions — to the Kernel during event type registration
From that point on, the Kernel applies the migrations autonomously on every event append, without any further involvement from the client.
Validation: missing migrators
Section titled “Validation: missing migrators”If an event type is declared with a generation higher than 1 but has no migrators covering all generations up to the current one, Chronicle throws MissingEventTypeMigrators during startup. This prevents silent data loss from an incomplete migration chain.
Cratis.Chronicle.Events.Migrations.MissingEventTypeMigrators: Event type 'AuthorRegistered' is at generation 3 but no migrators are registered for it.Ensure every generation gap has a corresponding EventTypeMigration<TUpgrade, TPrevious> subclass before deploying an event type with a new generation.