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.

Projections are event handlers whose sole purpose is to maintain a read model — a query-optimised, denormalised view of your domain data stored in a separate database. Each domain event that reaches the subscription is translated into one or more database write operations. Because projections are ordinary IEventHandler implementations, they participate in the full subscription lifecycle: durable checkpoints, concurrent channel workers, partitioning, and OpenTelemetry tracing.

MongoProjector<T>

MongoProjector<T> is the base class for MongoDB-backed projections. The generic parameter T must extend ProjectedDocument, the base class for MongoDB read-model documents.
public abstract class MongoProjector<T>(
    IMongoDatabase              database,
    MongoProjectionOptions<T>?  options  = null,
    ITypeMapper?                typeMap  = null)
    : BaseEventHandler
    where T : ProjectedDocument
After calling the base constructor, register your event handlers inside your own constructor using the On family of methods.

The Collection property

protected IMongoCollection<T> Collection { get; }
Resolved automatically from the database using the collection name in MongoProjectionOptions<T>. If no options are provided, the collection name is derived from the document type name by convention (MongoCollectionName.For<T>()).

MongoProjectionOptions<T>

public record MongoProjectionOptions<T> where T : Document {
    public string CollectionName { get; set; } = MongoCollectionName.For<T>();
}
Pass a MongoProjectionOptions<T> instance to the constructor to override the default collection name.

Registering event handlers

MongoProjector<T> provides several On<TEvent> overloads to cover the most common MongoDB operation patterns.

Typed handler delegate

Supply a ProjectTypedEvent<T, TEvent> delegate for full control over what MongoDB operation to return:
protected void On<TEvent>(ProjectTypedEvent<T, TEvent> handler) where TEvent : class;
The delegate signature is:
public delegate ValueTask<MongoProjectOperation<T>> ProjectTypedEvent<T, TEvent>(
    MessageConsumeContext<TEvent> consumeContext)
    where T : ProjectedDocument where TEvent : class;

Fluent MongoOperationBuilder

Use MongoOperationBuilder<TEvent, T> for a concise, builder-style API that covers the most common operations:
protected void On<TEvent>(
    Func<MongoOperationBuilder<TEvent, T>, MongoOperationBuilder<TEvent, T>.IMongoProjectorBuilder> configure)
    where TEvent : class;
The builder exposes UpdateOne, UpdateMany, InsertOne, InsertMany, DeleteOne, DeleteMany, and Bulk sub-builders. Each sub-builder accepts an ID or filter selector and an update definition factory.

Update-by-ID shorthand

When you only need to update a single document by ID, use one of the two-argument overloads. Choose the first when the ID comes from a field on the event, or the second when it comes from the stream name:
// ID extracted from the event payload
protected void On<TEvent>(
    GetDocumentIdFromEvent<TEvent> getId,
    BuildUpdate<TEvent, T>         getUpdate)
    where TEvent : class;

// ID extracted from the stream name
protected void On<TEvent>(
    GetDocumentIdFromStream getId,
    BuildUpdate<TEvent, T>  getUpdate)
    where TEvent : class;

ProjectedDocument

All MongoDB read-model documents must extend ProjectedDocument, which provides:
PropertyDescription
IdDocument identifier (maps to MongoDB _id)
StreamPositionPosition of the last event within its originating stream
PositionGlobal log position of the last event that updated this document

Example: BookingStateProjection

The following projection from the Eventuous sample application maintains one BookingDocument per booking stream. It handles three event types using different builder styles:
public class BookingStateProjection : MongoProjector<BookingDocument> {
    public BookingStateProjection(IMongoDatabase database) : base(database) {
        // Shorthand: extract document ID from the stream name, then run a custom update
        On<V1.RoomBooked>(stream => stream.GetId(), HandleRoomBooked);

        // Fluent builder: DefaultId() uses the stream ID, then set a single field
        On<V1.PaymentRecorded>(
            b => b
                .UpdateOne
                .DefaultId()
                .Update((evt, update) => update.Set(x => x.Outstanding, evt.Outstanding))
        );

        On<V1.BookingFullyPaid>(
            b => b
                .UpdateOne
                .DefaultId()
                .Update((_, update) => update.Set(x => x.Paid, true))
        );
    }

    static UpdateDefinition<BookingDocument> HandleRoomBooked(
        IMessageConsumeContext<V1.RoomBooked>    ctx,
        UpdateDefinitionBuilder<BookingDocument> update) {
        var evt = ctx.Message;
        return update
            .SetOnInsert(x => x.Id,            ctx.Stream.GetId())
            .Set(x => x.GuestId,               evt.GuestId)
            .Set(x => x.RoomId,                evt.RoomId)
            .Set(x => x.CheckInDate,           evt.CheckInDate)
            .Set(x => x.CheckOutDate,          evt.CheckOutDate)
            .Set(x => x.BookingPrice,          evt.BookingPrice)
            .Set(x => x.Outstanding,           evt.OutstandingAmount);
    }
}

What each On call does

HandlerPatternDescription
RoomBookedGetDocumentIdFromStream + BuildUpdateUpserts the booking document with initial field values
PaymentRecordedUpdateOne.DefaultId()Updates Outstanding in the document whose _id matches the stream
BookingFullyPaidUpdateOne.DefaultId()Sets Paid = true without touching any other field

Example: MyBookingsProjection

This projection maintains a per-guest list of bookings in a single MyBookings document, demonstrating filtering by document content:
public class MyBookingsProjection : MongoProjector<MyBookings> {
    public MyBookingsProjection(IMongoDatabase database) : base(database) {
        On<V1.RoomBooked>(b => b
            .UpdateOne
            .Id(ctx => ctx.Message.GuestId)   // document keyed by guest, not booking
            .UpdateFromContext((ctx, update) =>
                update.AddToSet(
                    x => x.Bookings,
                    new(ctx.Stream.GetId(),
                        ctx.Message.CheckInDate,
                        ctx.Message.CheckOutDate,
                        ctx.Message.BookingPrice)
                )
            )
        );

        On<V1.BookingCancelled>(
            b => b.UpdateOne
                .Filter((ctx, doc) =>
                    doc.Bookings
                       .Select(b => b.BookingId)
                       .Contains(ctx.Stream.GetId())
                )
                .UpdateFromContext((ctx, update) =>
                    update.PullFilter(
                        x => x.Bookings,
                        x => x.BookingId == ctx.Stream.GetId()
                    )
                )
        );
    }
}

Registering projections

Projections are registered as event handlers on a subscription. Add the MongoCheckpointStore so progress is persisted between restarts:
services.AddSubscription<AllStreamSubscription, AllStreamSubscriptionOptions>(
    "BookingsProjections",
    builder => builder
        .UseCheckpointStore<MongoCheckpointStore>()
        .AddEventHandler<BookingStateProjection>()
        .AddEventHandler<MyBookingsProjection>()
        .WithPartitioningByStream(2)
);
WithPartitioningByStream(2) creates two parallel workers that each handle a disjoint subset of streams, keeping per-stream ordering while doubling throughput.

Other database projectors

Eventuous ships projector base classes for additional relational databases that follow the same On<TEvent> pattern:
ClassNuGet packageStorage
MongoProjector<T>Eventuous.Projections.MongoDBMongoDB
PostgresProjectorEventuous.Projections.PostgresPostgreSQL
SqlServerProjectorEventuous.Projections.SqlServerSQL Server
SqliteProjectorEventuous.Projections.SqliteSQLite
Each projector derives from BaseEventHandler and exposes the same On<TEvent> registration API, so the patterns shown here transfer directly to any supported database.

Build docs developers (and LLMs) love