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.

Every aggregate instance in Eventuous is stored in its own event stream. The stream name is the key that links an aggregate’s identity to its persisted history. Eventuous follows a predictable default convention and gives you a clean extension point to override it per aggregate type when your storage or projection topology requires a different scheme.

StreamName

StreamName is a validated record struct that wraps a plain string. It is used throughout the persistence, subscription, and projection APIs wherever a stream identity is needed.
public record struct StreamName
{
    public StreamName(string value);          // throws InvalidStreamName if null/whitespace

    // Convenience factories
    public static StreamName For<T>(string entityId);             // "{TypeName}-{entityId}"
    public static StreamName ForState<TState>(string entityId);   // strips "State" suffix

    // Accessors
    public string GetId();        // the part after the first '-'
    public string GetCategory();  // the part before the first '-'

    public static implicit operator string(StreamName streamName);
}
An empty or whitespace name throws InvalidStreamName at construction time, preventing silent failures at the persistence layer.

Default naming convention

By default, Eventuous combines the unqualified aggregate type name with the aggregate id value, separated by a hyphen:
{AggregateTypeName}-{aggregateId}
Examples:
Aggregate typeAggregate idStream name
Bookingabc123Booking-abc123
Paymentpay-9f8e7dPayment-pay-9f8e7d
CustomerAccountcust-42CustomerAccount-cust-42
The category segment (everything before the first -) is important for category subscriptions in stores like KurrentDB/EventStoreDB, where you can subscribe to all events for a given aggregate type.

StreamNameFactory

StreamNameFactory is the static class that builds stream names from type parameters and identity values:
public static class StreamNameFactory
{
    // Explicit aggregate + state + id types
    public static StreamName For<TAggregate, TState, TId>(TId id)
        where TAggregate : Aggregate<TState>
        where TState     : State<TState>, new()
        where TId        : Id;

    // Id-only overload — strips the "Id" suffix from the type name
    // e.g. BookingId → "Booking-{id.Value}"
    public static StreamName For<TId>(TId id) where TId : Id;
}
The id-only overload strips the conventional Id suffix from the type name so that BookingId produces Booking-{value} rather than BookingId-{value}:
var name = StreamNameFactory.For(new BookingId("abc123"));
// → "Booking-abc123"

StreamNameMap

StreamNameMap is an instance-scoped registry that lets you override stream name resolution per identity type. When a mapping is registered for a given TId, it is used instead of the default factory. If no custom mapping exists, the map falls back to StreamNameFactory.
public class StreamNameMap
{
    // Register a custom mapping for a specific identity type
    public void Register<TId>(Func<TId, StreamName> map) where TId : Id;

    // Resolve — uses custom mapping if registered, otherwise falls back to StreamNameFactory
    public StreamName GetStreamName<T, TState, TId>(TId aggregateId)
        where T      : Aggregate<TState>
        where TState : State<TState>, new()
        where TId    : Id;

    // Id-only resolve (no aggregate type required)
    public StreamName GetStreamName<TId>(TId id) where TId : Id;
}

Default stream name resolution

When no custom mapping is registered, GetStreamName delegates to StreamNameFactory.For<TAggregate, TState, TId>, which applies the {AggregateName}-{id} convention:
var map      = new StreamNameMap();
var streamName = map.GetStreamName<Booking, BookingState, BookingId>(new BookingId("abc123"));
// → "Booking-abc123"

Custom stream name registration

Register a custom function when you need a different naming scheme for a particular aggregate:
var map = new StreamNameMap();

// All Booking aggregates use a "bookings-" prefix instead of "Booking-"
map.Register<BookingId>(id => new StreamName($"bookings-{id.Value}"));

var streamName = map.GetStreamName<Booking, BookingState, BookingId>(new BookingId("abc123"));
// → "bookings-abc123"
You can register as many identity types as needed. Registrations are keyed by the identity type, so different aggregate types with different id types each get their own mapping.

Registering a StreamNameMap with DI

Pass a configured StreamNameMap instance to the command service or the aggregate store during DI setup:
var streamNameMap = new StreamNameMap();
streamNameMap.Register<BookingId>(id => new StreamName($"bookings-{id.Value}"));
streamNameMap.Register<PaymentId>(id => new StreamName($"payments-{id.Value}"));

builder.Services.AddSingleton(streamNameMap);

How stream names flow through the application

When a CommandService handles a command, it needs to know which stream to load and save the aggregate from/to. The lookup path is:
  1. The command service calls StreamNameMap.GetStreamName<TAggregate, TState, TId>(aggregateId).
  2. If a custom mapping for TId is registered, the custom function is called.
  3. Otherwise, StreamNameFactory.For<TAggregate, TState, TId>(aggregateId) produces the default {AggregateName}-{id} name.
  4. IEventReader.LoadAggregate / IEventWriter.StoreAggregate receive the resolved StreamName.
// Inside AggregatePersistenceExtensions — simplified
var streamName = streamNameMap?.GetStreamName<TAggregate, TState, TId>(aggregateId)
             ?? StreamNameFactory.For<TAggregate, TState, TId>(aggregateId);

var aggregate = await eventReader.LoadAggregate<TAggregate, TState>(streamName, ...);

Helper methods on StreamName

Once you have a StreamName you can extract its parts:
var name = new StreamName("Booking-abc123");

string id       = name.GetId();       // "abc123"
string category = name.GetCategory(); // "Booking"

// Implicit conversion to string
string s = name; // "Booking-abc123"
GetCategory() aligns with the KurrentDB/EventStoreDB $ce-{Category} projection that fans out events from all streams in the same category, enabling efficient per-aggregate-type subscriptions.

Build docs developers (and LLMs) love