CQRS and Event Sourcing for .NET. Start small, keep the receipts.
Begin with a lean mediator. Add an event store when you need one. Scale out with an outbox, projections and sagas when you must. One MIT-licensed family of packages, one version, and tamper-evident streams and tenant-bound encryption already inside.
New to the terms? Thirty seconds, then pick a door.
Mediator
A controller that knows ten services is hard to test and harder to change. With a mediator it hands over one object — OpenAccount — and a dispatcher finds the single handler that answers it. One request, one handler, and no web server needed to test it.
CQRS
Command Query Responsibility Segregation. A command changes something and returns little; a query reads and changes nothing. Keeping them apart lets each side take the shape and the scaling its own job needs. It is a routing decision first, not a second database.
Event sourcing
Store the facts that happened — AccountOpened, MoneyDeposited — instead of the state they produced. Current state is a fold over those facts, so the history is the source of truth rather than an audit log kept beside it.
Pick your door
Three reasons people arrive here. Each one is a real entry point, not a teaser for the whole stack.
I need a mediator
Commands, queries and pipeline behaviors, in process, MIT. Nothing else comes along.
dotnet add package Stratara.Mediator
public sealed record OpenAccount(string Owner)
: ICommand<Guid>;
public sealed class OpenAccountHandler
: IQueryHandler<OpenAccount, Guid>
{
public Task<Guid> HandleAsync(
OpenAccount cmd, CancellationToken ct)
=> Task.FromResult(Guid.NewGuid());
}
builder.Services
.AddMediator()
.AddQueryHandlersFromAssemblyContaining<Program>();
You do not need: a database, a broker, or any telemetry setup.
First Stratara app →I want event sourcing without the plumbing
Aggregates and events on PostgreSQL, snapshots, outbox, projections, replay — shipped, not sketched.
dotnet add package Stratara.EventSourcing.WorkerDefaults
public sealed record InvoiceIssued(
Guid InvoiceId, Guid TenantId, decimal Total)
: IAggregateCreationEvent;
public sealed class Invoice : ITenantAggregate
{
public Guid Id { get; set; }
public Guid TenantId { get; set; }
public decimal Total { get; set; }
public void Apply(InvoiceIssued e) =>
(Id, TenantId, Total) =
(e.InvoiceId, e.TenantId, e.Total);
}
// inside a command handler
await events.CreateAsync<Invoice>(
id, new InvoiceIssued(id, tenantId, 120m), ct);
await events.SaveChangesAsync(ct);
You do not need: to write an event store, an outbox or a replay. They are the product.
Event-sourced walkthrough →I run a multi-tenant SaaS and get audited
Hash-chained streams, fields sealed to their tenant, and GDPR erasure by destroying a key.
dotnet add package Stratara.Security
public sealed record CustomerRegistered(
Guid CustomerId,
Guid TenantId,
[property: EncryptData] string Email)
: IAggregateCreationEvent;
// Right to erasure: shred the subject's key.
// Events, snapshots, replicas and backups
// all become noise.
await keyStore.EraseScopeAsync(scope, ct);
You do not need: a separate audit product, or a WHERE tenant_id you hope nobody forgets.
It grows with you
The same handler you wrote on day one runs unchanged on day three hundred. Only the hosting around it changes.
What is in the box
Integrated, not assembled. Every part below is versioned together and tested against the others.
Mediator and pipeline
Commands, queries, open-generic behaviors in registration order, authorization and tenant isolation at the entrance.
Event store on PostgreSQL
Streams, snapshots, optimistic concurrency, event upcasting, command audit — through EF Core you already run.
Outbox and messaging
At-least-once dispatch over RabbitMQ or Azure Service Bus, publisher confirms, a heavy-command lane.
Projections and sagas
Push-driven from the event bus, per-aggregate ordering, retry for facts that arrive before their beginning.
Tamper-evident streams
Every event hash-chained, with anchors you can pin outside your database. Edit a row and the chain names the sequence.
Tenant-bound encryption
AES-GCM with the tenant as associated data; a row leaked from one tenant cannot be read in another.
Identity and membership
Users in many tenants with per-membership roles, a code-first permission catalog, API keys that share the same plane.
Observability defaults
One activity source, one meter, stable log-event ids, OpenTelemetry and Serilog wired in a line.
Numbers, not adjectives
Measured with BenchmarkDotNet on a fanless MacBook Air M4. Read them as conservative ratios, not a tuned server's ceiling.
Where it sits
An honest map. Each project below is good at what it does; this is about scope and license, not ranking.
| Project | Scope | License | Approach |
|---|---|---|---|
| Stratara | Mediator, event store, outbox, projections, sagas, identity, encryption | MIT | One lockstep family; opt in per package; audit properties are defaults, not add-ons |
| MediatR | In-process mediator | RPL-1.5 or commercial from v13; free Community edition below 5 M USD revenue | The reference mediator; bring your own everything else |
| Marten + Wolverine | Document DB and event store on PostgreSQL; messaging and handlers | MIT, open core with commercial support | Two libraries that compose well; broad and mature |
| MassTransit | Distributed messaging, sagas | v9 commercial since 2026; v8 Apache 2.0, supported to end of 2026 | Transport-centric; no event store |
| KurrentDB (EventStoreDB) | Purpose-built event database | Vendor license | A server you operate, not a library you reference |
License facts verified September 2026 against each project's published terms. Check the source before you decide.
Five minutes to a running mediator. An afternoon to event sourcing.
Every guarantee on this page is written down as a specification and tested in CI. Read them, run the samples, then decide.
Derived. The behaviour described on this page is specified under
openspec/specs/in the repository. Those specifications are the source; this page explains and illustrates them.