Skip to main content

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.

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 immutable 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

public record RoomBooked(
    string         GuestId,
    string         RoomId,
    LocalDate      CheckInDate,
    LocalDate      CheckOutDate,
    float          BookingPrice,
    float          PrepaidAmount,
    float          OutstandingAmount,
    string         Currency,
    DateTimeOffset BookingDate
);
Records give you structural equality, a generated 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 a static 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:
public static class BookingEvents {
    public static class V1 {
        // all current event records go here
    }
    public static class V2 {
        // future revised events go here
    }
}
With a 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 the Booking aggregate from the sample application:
using Eventuous;
using NodaTime;

namespace Bookings.Domain.Bookings;

public static class BookingEvents {
    public static class V1 {
        [EventType("V1.RoomBooked")]
        public record RoomBooked(
            string         GuestId,
            string         RoomId,
            LocalDate      CheckInDate,
            LocalDate      CheckOutDate,
            float          BookingPrice,
            float          PrepaidAmount,
            float          OutstandingAmount,
            string         Currency,
            DateTimeOffset BookingDate
        );

        [EventType("V1.PaymentRecorded")]
        public record PaymentRecorded(
            float          PaidAmount,
            float          Outstanding,
            string         Currency,
            string         PaymentId,
            string         PaidBy,
            DateTimeOffset PaidAt
        );

        [EventType("V1.FullyPaid")]
        public record BookingFullyPaid(DateTimeOffset FullyPaidAt);

        [EventType("V1.Overpaid")]
        public record BookingOverpaid(DateTimeOffset OverpaidAt);

        [EventType("V1.BookingCancelled")]
        public record BookingCancelled(string CancelledBy, DateTimeOffset CancelledAt);
    }
}

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:
[EventType("V1.RoomBooked")]
public record RoomBooked(...);
Decorate every event record with this attribute. The string value is what gets written to the event store metadata and must never change once events have been persisted.

Scanning at startup

TypeMap.RegisterKnownEventTypes();
This scans all non-system, non-Microsoft assemblies in the current AppDomain and registers every type decorated with [EventType]. Call it once during application startup. If you want to scan only specific assemblies:
TypeMap.RegisterKnownEventTypes(typeof(BookingEvents).Assembly);

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.AddType<RoomBooked>("V1.RoomBooked");
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:
public interface ITypeMapper {
    bool TryGetTypeName(Type type, out string? typeName);
    bool TryGetType(string typeName, out Type? type);
}
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.):
public class DefaultEventSerializer(
    JsonSerializerOptions options,
    ITypeMapper?          typeMapper = null
) : IEventSerializer
DefaultEventSerializer.Instance is the global default instance. You can replace it:
DefaultEventSerializer.SetDefaultSerializer(
    new DefaultEventSerializer(myOptions));
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:
public class DefaultStaticEventSerializer(
    JsonSerializerContext context,
    ITypeMapper?          typeMapper = null
) : IEventSerializer
This serializer calls JsonSerializer.Serialize / JsonSerializer.Deserialize with the source-generated JsonSerializerContext overloads, which never use reflection.

Wiring it up

First, declare a partial JsonSerializerContext class annotated with one [JsonSerializable] attribute per event type:
[JsonSerializable(typeof(BookingEvents.V1.RoomBooked))]
[JsonSerializable(typeof(BookingEvents.V1.BookingCancelled))]
[JsonSerializable(typeof(BookingEvents.V1.BookingFullyPaid))]
[JsonSerializable(typeof(BookingEvents.V1.BookingOverpaid))]
[JsonSerializable(typeof(BookingEvents.V1.PaymentRecorded))]
internal partial class SourceGenerationContext : JsonSerializerContext;
Then replace the default serializer during startup (before anything reads from or writes to the event store):
DefaultEventSerializer.SetDefaultSerializer(
    new DefaultStaticEventSerializer(new SourceGenerationContext()));
The complete Program.cs snippet from the Eventuous sample:
using System.Text.Json.Serialization;
using Bookings.Domain.Bookings;
using Eventuous;

DefaultEventSerializer.SetDefaultSerializer(
    new DefaultStaticEventSerializer(new SourceGenerationContext()));

var builder = WebApplication.CreateBuilder(args);
// ... rest of DI setup

[JsonSerializable(typeof(BookingEvents.V1.RoomBooked))]
[JsonSerializable(typeof(BookingEvents.V1.BookingCancelled))]
[JsonSerializable(typeof(BookingEvents.V1.BookingFullyPaid))]
[JsonSerializable(typeof(BookingEvents.V1.BookingOverpaid))]
[JsonSerializable(typeof(BookingEvents.V1.PaymentRecorded))]
internal partial class SourceGenerationContext : JsonSerializerContext;

Naming best practices

RuleRationale
Use past tense — RoomBooked, not BookRoomAn event records something that already happened, not a command to do something
Prefix with a version segment — V1.RoomBookedMakes schema evolution explicit and keeps old/new event names collision-free
Keep the [EventType] string stable foreverOnce an event is in the store, renaming the CLR type is safe — but the string name must not change
One concept per eventBroad “catch-all” events make state handlers complex and projections fragile
Never change the string value of an [EventType] attribute once events of that type have been written to a production store. The stored metadata contains the string name; if it no longer maps to a registered type, those events will fail to deserialize and will be silently skipped.

Build docs developers (and LLMs) love