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.

Aggregate identities in Eventuous are strong types, not raw strings. Wrapping every identity in its own record type turns a category of runtime bugs — accidentally passing a RoomId where a BookingId is expected — into a compile-time error. The Id abstract record provides the validation, conversion, and formatting behaviour shared by all identities in the system.

The Id abstract record

public abstract record Id {
    protected Id(string value) {
        if (string.IsNullOrWhiteSpace(value))
            throw new Exceptions.InvalidIdException(this);

        Value = value;
    }

    public string Value { get; }

    public sealed override string ToString() => Value;

    public static implicit operator string(Id? id)
        => id?.ToString() ?? throw new Exceptions.InvalidIdException(typeof(Id));

    public void Deconstruct(out string value) => value = Value;
}

Value

The underlying string value. Id validates that this is not null, empty, or whitespace in the constructor. Any violation throws InvalidIdException immediately, so an Id instance can never represent an empty stream name.

ToString()

Returns Value directly. Because event store stream names are derived from Id.ToString(), the string representation of your identity is the stream key — keep it stable across deployments.

Implicit conversion to string

public static implicit operator string(Id? id)
You can pass any Id subtype wherever a string is expected without an explicit cast. If id is null, the conversion throws InvalidIdException rather than silently producing a null string.

Deconstruct

public void Deconstruct(out string value) => value = Value;
Supports C# positional deconstruction, so you can write:
var (rawId) = bookingId;
This is handy when integrating with APIs that work directly with strings.

Creating a typed identity

Declare a one-line record that calls the Id base constructor with the value:
public record BookingId(string Value) : Id(Value);
That single line gives you:
  • Null/whitespace validation on construction.
  • Value equality via record semantics (==, !=, .Equals()).
  • A stable ToString() and implicit string cast.
  • Compiler-enforced type safety — a BookingId cannot be accidentally used as a RoomId.
The complete BookingId from the sample application looks exactly like this:
using Eventuous;

namespace Bookings.Domain.Bookings;

public record BookingId(string Value) : Id(Value);

Why typed identities matter

Consider a booking service that works with several aggregate types: Booking, Room, and Guest. Each has its own identity:
public record BookingId(string Value) : Id(Value);
public record RoomId(string Value)    : Id(Value);
public record GuestId(string Value)   : Id(Value);
Without typed IDs you might write:
// Which ID is which? The compiler won't warn you.
void Cancel(string bookingId, string guestId) { ... }
With typed IDs the signature is unambiguous and compiler-checked:
void Cancel(BookingId bookingId, GuestId guestId) { ... }
Swapping the arguments is now a compile-time error, not a runtime mystery.

InvalidIdException

Thrown by the Id constructor when value is null, empty, or whitespace. Two overloads exist:
// Thrown with the concrete Id subtype in the message
public InvalidIdException(Id id)

// Thrown with just the type (used from the implicit string conversion)
public InvalidIdException(Type idType)
The exception message names the specific Id subtype, making it straightforward to identify which aggregate identity was invalid.

AggregateId is obsolete

Earlier versions of Eventuous shipped an AggregateId base class. It still compiles but carries an [Obsolete] attribute:
[Obsolete("Use Id instead")]
public abstract record AggregateId : Id { ... }
Any code that derives from AggregateId should be migrated to derive from Id directly. The behaviour is identical.
The CommandService<TAggregate, TState, TId> generic parameter TId has a where TId : Id constraint. All three type parameters must align — your aggregate, its state record, and its identity must all reference the same TId type.

Build docs developers (and LLMs) love