Skip to content

Generated Code

This page covers the plain .NET interfaces, implementations, and DI registration generated by the Event Store module — no effects or Eff<RT, T> required.

Generated Store Interface

For each [EventSourcedAggregate], the generator produces a store interface:

public interface IOrderEventStore : IAutoCommittable
{
    // Fetch for writing with optimistic concurrency
    Task<IEventStream<Order>> FetchForWritingAsync(OrderId id, CancellationToken ct = default);

    // Rebuild aggregate from events
    Task<Order?> AggregateAsync(OrderId id, CancellationToken ct = default);

    // Rebuild aggregate up to a specific version
    Task<Order?> AggregateAsync(OrderId id, long version, CancellationToken ct = default);

    // Start a new stream with an explicit identity
    Task StartStreamAsync(OrderId id, IReadOnlyList<object> events, CancellationToken ct = default);

    // Start a new stream with an auto-generated identity
    Task<OrderId> StartStreamAsync(IReadOnlyList<object> events, CancellationToken ct = default);

    // Append events to an existing stream
    Task AppendAsync(OrderId id, IReadOnlyList<object> events, CancellationToken ct = default);

    // Append with optimistic concurrency check
    Task AppendOptimisticAsync(OrderId id, long expectedVersion,
        IReadOnlyList<object> events, CancellationToken ct = default);

    // Fetch all events from a stream
    Task<IReadOnlyList<EventEnvelope>> FetchStreamAsync(OrderId id, CancellationToken ct = default);
}

All store interfaces extend IAutoCommittable, enabling automatic commit after command dispatch when used with the Dispatch module.

IEventStream<T>

Returned by FetchForWritingAsync — holds the current aggregate state and buffers appended events:

public interface IEventStream<out TAggregate>
{
    TAggregate? Aggregate { get; }     // Current state, or null if not found
    long CurrentVersion { get; }        // For optimistic concurrency

    void AppendOne(object @event);      // Buffer a single event
    void AppendMany(IEnumerable<object> events); // Buffer multiple events
}

Events are not persisted until CommitAsync() is called. For fluent event appending in Eff pipelines, see Stream Extensions.

EventEnvelope

Events are stored wrapped in an envelope with metadata:

Field Type Description
Data object The event payload
EventId Guid Unique event occurrence identifier
Version long Stream version at which this event was appended
Timestamp DateTimeOffset When the event was persisted
EventTypeName string CLR type name of the event
CorrelationId string? From CorrelationContext
CausationId string? From CorrelationContext
UserId string? From CorrelationContext
TenantId string? From CorrelationContext
Metadata IReadOnlyDictionary<string, string>? Custom metadata

Metadata fields are automatically populated from CorrelationContext.Current when events are committed.

In-Memory Implementation

A ConcurrentDictionary-backed implementation is generated for every aggregate:

public sealed class InMemoryOrderEventStore : IOrderEventStore { ... }

Key behaviors:

  • Events are buffered in a _pending list until CommitAsync() is called
  • CommitAsync() wraps each event in an EventEnvelope with metadata from CorrelationContext.Current
  • AggregateAsync() rebuilds the aggregate via AggregateFold<T>.Fold() (Create/Apply convention)
  • Optimistic concurrency checks stream version on AppendOptimisticAsync()

Registered by default via TryAddSingleton — infrastructure implementations (e.g., Marten) override it.

DI Registration

The generator creates a registration extension:

public static class AppEventStoreRegistration
{
    public static IServiceCollection AddAppEventStore(this IServiceCollection services)
    {
        services.TryAddSingleton<IOrderEventStore, InMemoryOrderEventStore>();
        // ... per-aggregate registrations
        return services;
    }
}

TryAddSingleton means infrastructure implementations registered first take priority:

// In-memory (default)
services.AddAppEventStore();

// Marten overrides in-memory (registered as scoped, takes priority)
services.AddAppEventStoreMarten(connectionString);
services.AddAppEventStore(); // InMemoryOrderEventStore won't override

Direct Injection

Inject IOrderEventStore directly:

public class OrderService(IOrderEventStore orderStore)
{
    public async Task<Order?> GetAsync(OrderId id) =>
        await orderStore.AggregateAsync(id);

    public async Task CreateAsync(OrderId id, string customerName)
    {
        await orderStore.StartStreamAsync(id, [new OrderCreated(id, customerName)]);
        await orderStore.CommitAsync();
    }

    public async Task AddItemAsync(OrderId id, string sku, int quantity)
    {
        await orderStore.AppendAsync(id, [new OrderItemAdded(sku, quantity)]);
        await orderStore.CommitAsync();
    }
}

See Effects for the Eff<RT, T> composition layer, or Attributes for the full attribute reference.