Domain events are the foundation of every event-sourced system. In Eventuous, an event is any plain C# object — no base class, no marker interface, no code generation required. The recommended shape is an immutableDocumentation 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.
record with positional or init-only properties. Because records are value types by nature and support non-destructive mutation with with, they are a natural fit for data that should never change after it has been written to the store.
Event design conventions
Use records
ToString(), and immutability with minimal boilerplate.
Nest events under a versioned static class
As your system evolves, you will occasionally need to change an event’s schema. The Eventuous convention is to nest all events for a given aggregate under astatic class V1 (and later V2, etc.) inside an outer static container class. This keeps CLR type names stable and lets you introduce V2 events without removing V1 handlers from your state:
using static Bookings.Domain.Bookings.BookingEvents; import you can refer to V1.RoomBooked throughout your aggregate and state classes without qualification noise.
Full example — BookingEvents
The complete event catalogue for theBooking aggregate from the sample application:
TypeMap and [EventType]
When Eventuous serialises an event to the store, it needs a stable string name for the event type — using the CLR fully qualified name would break if you ever rename or move a class. The TypeMap solves this by maintaining a two-way dictionary between CLR types and their string event-type names.
[EventType] attribute
The simplest registration path is the [EventType("...")] attribute:
Scanning at startup
AppDomain and registers every type decorated with [EventType]. Call it once during application startup. If you want to scan only specific assemblies:
Manual registration
If you cannot add attributes to an event type (for example, a shared contract defined in another library), register it explicitly:TypeMap.Instance is the global TypeMapper that all Eventuous components use by default.
ITypeMapper
ITypeMapper is the interface behind TypeMap.Instance. You can substitute a custom implementation on any component that accepts one:
ITypeMapperExt extends this with GetRegisteredTypes() which returns all known (TypeName, Type) pairs — useful for diagnostics and tooling.
Serialization
DefaultEventSerializer
The standard JSON serializer, powered by System.Text.Json. It uses the global TypeMapper by default and Web-defaults JSON options (camelCase property names, etc.):
DefaultEventSerializer.Instance is the global default instance. You can replace it:
DefaultEventSerializer uses reflection-based serialization, which triggers [RequiresUnreferencedCode] and [RequiresDynamicCode] warnings in trimmed or AOT-compiled applications.
DefaultStaticEventSerializer (AOT-safe)
For Native AOT or aggressive trimming scenarios, use DefaultStaticEventSerializer together with a JsonSerializerContext generated by System.Text.Json source generation:
JsonSerializer.Serialize / JsonSerializer.Deserialize with the source-generated JsonSerializerContext overloads, which never use reflection.
Wiring it up
First, declare a partialJsonSerializerContext class annotated with one [JsonSerializable] attribute per event type:
Program.cs snippet from the Eventuous sample:
Naming best practices
| Rule | Rationale |
|---|---|
Use past tense — RoomBooked, not BookRoom | An event records something that already happened, not a command to do something |
Prefix with a version segment — V1.RoomBooked | Makes schema evolution explicit and keeps old/new event names collision-free |
Keep the [EventType] string stable forever | Once an event is in the store, renaming the CLR type is safe — but the string name must not change |
| One concept per event | Broad “catch-all” events make state handlers complex and projections fragile |