Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Eventuous/eventuous/llms.txt
Use this file to discover all available pages before exploring further.
Eventuous serializes domain events to bytes before writing them to the store and deserializes them back when reading. Two concerns are kept separate: the type map that translates between CLR types and stable string names, and the serializer that converts event objects to and from bytes. This separation means you can swap the JSON library, adopt native AOT, or evolve your event names without touching your domain model.
IEventSerializer
All serialization goes through the IEventSerializer interface:
public interface IEventSerializer
{
DeserializationResult DeserializeEvent(
ReadOnlySpan<byte> data,
string eventType,
string contentType);
SerializationResult SerializeEvent(object evt);
}
SerializationResult
Returned by SerializeEvent:
public record SerializationResult(string EventType, string ContentType, byte[] Payload);
| Property | Type | Description |
|---|
EventType | string | The registered type-map name (e.g. "V1.BookingMade") |
ContentType | string | MIME type — "application/json" for both built-in serializers |
Payload | byte[] | UTF-8 encoded JSON bytes |
DeserializationResult
A discriminated union returned by DeserializeEvent:
public abstract record DeserializationResult
{
public record SuccessfullyDeserialized(object Payload) : DeserializationResult;
public record FailedToDeserialize(DeserializationError Error) : DeserializationResult;
}
public enum DeserializationError
{
UnknownType, // No CLR type registered for the event type string
ContentTypeMismatch, // Stored content type does not match the serializer
PayloadEmpty // Deserialization returned null
}
Pattern-match on the result to handle unknown events gracefully without throwing:
var result = serializer.DeserializeEvent(data, eventType, contentType);
if (result is DeserializationResult.SuccessfullyDeserialized { Payload: var evt })
aggregate.Apply(evt);
else if (result is DeserializationResult.FailedToDeserialize { Error: var err })
logger.LogWarning("Could not deserialize {EventType}: {Error}", eventType, err);
Type mapping
The type map is the bridge between a stable event name string stored alongside the bytes and the CLR type used in your domain model. Renaming a C# record does not break historical events as long as the registered name stays the same.
ITypeMapper
public interface ITypeMapper
{
bool TryGetTypeName(Type type, [NotNullWhen(true)] out string? typeName);
bool TryGetType (string typeName, [NotNullWhen(true)] out Type? type);
}
TypeMap and TypeMapper
TypeMap.Instance is a process-wide singleton TypeMapper. You can inject ITypeMapper where needed, or call the static TypeMap helpers directly:
// Register a type manually
TypeMap.Instance.AddType<BookingMade>("V1.BookingMade");
// Look up the name for a given CLR type
if (TypeMap.Instance.TryGetTypeName(typeof(BookingMade), out var name))
Console.WriteLine(name); // "V1.BookingMade"
// Look up the CLR type for a stored name
if (TypeMap.Instance.TryGetType("V1.BookingMade", out var type))
Console.WriteLine(type.Name); // "BookingMade"
Registering the same type with a different name throws ArgumentException. Registering it with the same name is a no-op and is safe to call multiple times (e.g., from multiple startup paths).
[EventType] attribute and auto-registration
Decorate event records with [EventType] to declare their stable name in one place:
[EventType("V1.BookingMade")]
public record BookingMade(string BookingId, string RoomId, decimal Price);
Then scan and register all decorated types in one call:
// Scans all non-system assemblies in the current AppDomain
TypeMap.RegisterKnownEventTypes();
// Or provide specific assemblies
TypeMap.RegisterKnownEventTypes(typeof(BookingMade).Assembly);
RegisterKnownEventTypes uses reflection and is not trimming-safe. For native AOT applications, register types manually using TypeMap.Instance.AddType<T>(name) instead.
DefaultEventSerializer
DefaultEventSerializer uses System.Text.Json with reflection-based serialization. It is the default for conventional .NET applications.
public class DefaultEventSerializer(JsonSerializerOptions options, ITypeMapper? typeMapper = null)
: IEventSerializer
{
public static IEventSerializer Instance { get; private set; } // default with Web options
public static void SetDefaultSerializer(IEventSerializer serializer);
}
The built-in instance is created with JsonSerializerDefaults.Web (camelCase property names, case-insensitive deserialization). Replace it globally before the application starts:
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
options.Converters.Add(new JsonStringEnumConverter());
DefaultEventSerializer.SetDefaultSerializer(new DefaultEventSerializer(options));
You can also supply a custom ITypeMapper if you maintain a type registry separate from TypeMap.Instance:
var serializer = new DefaultEventSerializer(options, myCustomTypeMapper);
DefaultStaticEventSerializer (AOT/NativeAOT)
DefaultStaticEventSerializer uses System.Text.Json source generation, avoiding all runtime reflection. It is the correct choice for native AOT deployments.
public class DefaultStaticEventSerializer(JsonSerializerContext context, ITypeMapper? typeMapper = null)
: IEventSerializer
Setup with source generation
- Create a
JsonSerializerContext that includes all event types:
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(BookingMade))]
[JsonSerializable(typeof(RoomCharged))]
[JsonSerializable(typeof(BookingCancelled))]
public partial class BookingSerializerContext : JsonSerializerContext;
- Register event types manually (no assembly scanning):
TypeMap.Instance.AddType<BookingMade> ("V1.BookingMade");
TypeMap.Instance.AddType<RoomCharged> ("V1.RoomCharged");
TypeMap.Instance.AddType<BookingCancelled>("V1.BookingCancelled");
- Instantiate and set as the default:
var context = new BookingSerializerContext(new JsonSerializerOptions());
var serializer = new DefaultStaticEventSerializer(context);
DefaultEventSerializer.SetDefaultSerializer(serializer);
All three steps are typically done once during application startup, before the DI container is built.
Metadata attached to events (correlation IDs, causation IDs, etc.) is serialized separately through IMetadataSerializer:
public interface IMetadataSerializer
{
byte[] Serialize(Metadata evt);
Metadata? Deserialize(ReadOnlySpan<byte> bytes); // throws MetadataDeserializationException
}
The built-in DefaultMetadataSerializer uses System.Text.Json source generation internally. Replace the global instance when you need custom metadata handling:
DefaultMetadataSerializer.SetDefaultSerializer(new MyMetadataSerializer());
Complete setup example
Standard (reflection-based)
// Program.cs
TypeMap.RegisterKnownEventTypes(typeof(BookingMade).Assembly);
var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
DefaultEventSerializer.SetDefaultSerializer(new DefaultEventSerializer(jsonOptions));
builder.Services.AddEventStore<KurrentDBEventStore>();
AOT-safe (source generation)
// Declare context (all event types must be listed)
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(BookingMade))]
[JsonSerializable(typeof(RoomCharged))]
[JsonSerializable(typeof(BookingCancelled))]
public partial class AppSerializerContext : JsonSerializerContext;
// Program.cs
TypeMap.Instance.AddType<BookingMade> ("V1.BookingMade");
TypeMap.Instance.AddType<RoomCharged> ("V1.RoomCharged");
TypeMap.Instance.AddType<BookingCancelled>("V1.BookingCancelled");
var context = new AppSerializerContext(new JsonSerializerOptions());
var serializer = new DefaultStaticEventSerializer(context);
DefaultEventSerializer.SetDefaultSerializer(serializer);
builder.Services.AddEventStore<KurrentDBEventStore>();
When using the AOT serializer, ensure every event type referenced in your system — including those consumed from subscriptions — is declared in the JsonSerializerContext. Missing types will produce a FailedToDeserialize result at runtime.