Skip to content
yesbertPublic

About

CQRS and Event Sourcing for .NET — with tamper-evident streams and tenant-aware encryption built in, and an Orleans execution model to scale out. Integrated mediator, outbox, sagas, projections, and identity across 27 lockstep-versioned NuGet packages. MIT-licensed.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

258 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Stratara

Stratara

CQRS and Event Sourcing for .NET 10. Start with a mediator, scale out on Orleans, keep the receipts.

CI NuGet License: MIT Docs .NET 10


New to the terms? Mediator — your controller hands over one object (OpenAccount) and a dispatcher finds the single handler that answers it. CQRS — Command Query Responsibility Segregation: a command changes something and returns little, a query reads and changes nothing, and keeping them apart lets each side take the shape its own job needs. Event sourcing — store the facts that happened (AccountOpened, MoneyDeposited) instead of the state they produced, and fold them to get the current value. Longer: the glossary.

Stratara is one MIT-licensed family of 28 NuGet packages, versioned together: mediator, event store on PostgreSQL, outbox over RabbitMQ or Azure Service Bus, projections, sagas, identity — and, as defaults rather than add-ons, hash-chained tamper-evident event streams and tenant-bound field encryption with GDPR-grade crypto-shredding. Scale out on the Orleans execution model: one writer per aggregate across the cluster, and no accepted command lost to a crash. Take one package or take all of them; they never disagree about each other's version.

Pick your door

🚪 I need a mediator

Commands, queries and pipeline behaviors, in process. One package; no database, no broker, no telemetry setup.

dotnet add package Stratara.Mediator
public sealed record OpenAccount(string Owner, decimal Initial) : ICommand<Guid>;

public sealed class OpenAccountHandler : IQueryHandler<OpenAccount, Guid>
{
    public Task<Guid> HandleAsync(OpenAccount cmd, CancellationToken ct)
        => Task.FromResult(Guid.NewGuid());
}

// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services
    .AddMediator()
    .AddQueryHandlersFromAssemblyContaining<Program>();
var app = builder.Build();

using var scope = app.Services.CreateScope();          // IMediator is scoped
var id = await scope.ServiceProvider.GetRequiredService<IMediator>()
    .HandleAsync(new OpenAccount("Alice", 100m));

→ First Stratara app · samples/Stratara.Sample.CqrsBasics

🚪 I want event sourcing without the plumbing

Aggregates and events on PostgreSQL, snapshots, optimistic concurrency, an outbox, push-driven projections and replay. You write the aggregate; the store, the outbox and the workers ship.

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);   // one transaction, snapshot by policy, one bundle to the outbox

→ Write a command handler · Write a projection · Event-sourced walkthrough

🚪 I run a multi-tenant SaaS and get audited

Every event stream is hash-chained, with periodic anchors you can commit outside your database. [EncryptData] fields are sealed with the tenant as associated data, so a row leaked from one tenant cannot be read in another, even with the master key. Erasure is a key you destroy, not history you rewrite.

public sealed record CustomerRegistered(
    Guid CustomerId,
    Guid TenantId,
    [property: EncryptData] string Email) : IAggregateCreationEvent;

// GDPR Art. 17: shred the subject's key — events, snapshots, replicas and backups become noise
await keyStore.EraseScopeAsync(scope, ct);

→ Tamper-evident streams · Tenant-aware encryption · hero samples TamperProof and Encryption

🚪 I scale out and cannot lose a command

The Orleans execution model runs your handlers, projections, sagas and timers as virtual actors on a cluster. A committed fact is never lost to a crash. One aggregate has one writer across the whole deployment. Work that must happen once per cluster needs no lock. Your handlers do not change; the registration does.

dotnet add package Stratara.Orleans
dotnet add package Stratara.Orleans.EntityFrameworkCore
builder.UseOrleans(silo => silo
    .UseAdoNetClustering(o => { o.Invariant = "Npgsql"; o.ConnectionString = orleansDb; })
    .UseAdoNetReminderService(o => { o.Invariant = "Npgsql"; o.ConnectionString = orleansDb; })
    .AddStrataraOrleans((s, name) => s.AddRedisGrainDirectory(name, o => o.ConfigurationOptions = redis)));

builder.Services
    .AddStrataraAggregateGrains()                 // one writer per aggregate, cluster-wide
    .AddStrataraOrleansCommandDispatcher()        // an accepted command is recorded before the call returns
    .AddStrataraIntentStore<AppWriteDbContext>()  // ... and resumed after a crash
    .AddStrataraSingletonWork<OutboxDrainWork>(OutboxDrainWork.WorkName); // once per cluster, without a lock

Test the same registrations in one process, without a cluster or Docker: Stratara.Testing.Orleans runs a silo in memory — see the sample OrleansExecutionModel.

→ The Orleans execution model · Choose an execution model · Migrate from the bus workers

It grows with you

The handler from door one runs unchanged behind every later stage. Only the hosting around it changes. At stage three you choose what runs it: the Orleans execution model, which is recommended, or the bus workers, which stay supported.

flowchart LR
    subgraph S1["1 · Mediator only"]
        A0["API host<br/>IMediator + handlers"]
    end
    subgraph S2["2 · + Event store"]
        A1["API host<br/>IMediator + IEventSource"] --> DB1[("PostgreSQL<br/>streams · snapshots · outbox")]
    end
    subgraph S3["3 · + Orleans cluster (recommended)"]
        SI["Silos 1..N<br/>aggregate · projection · saga grains"] --> DB3[("PostgreSQL<br/>streams · recorded commands · checkpoints")]
        DB3 -->|commit order| SI
    end
    subgraph S3B["3 · + Workers and bus (supported)"]
        A2["API hosts 1..N"] --> BUS{{"RabbitMQ / Azure Service Bus"}}
        BUS -->|competing consumers| CW["Command workers"]
        CW --> DB2[("PostgreSQL<br/>4096 stream buckets")]
        DB2 -->|pushed event bundles| PW["Projection · saga workers"]
        PW --> RM[("Read models")]
    end
    S1 -.-> S2
    S2 -.-> S3
    S2 -.-> S3B
Loading

Why Stratara

  • Integrated, not assembled. Mediator, outbox, event store, sagas, projections and identity are one family at one version. No composition tax, no version-skew puzzles.
  • Audit-grade by default. Hash-chained streams with periodic anchors ready for external commitment; a direct edit in the database no longer recomputes, and a verification pass names the sequence where it broke.
  • Tenant isolation you can prove. Cryptographic binding of encrypted fields to their tenant, plus a mediator-entrance guard that rejects a request naming another tenant before your handler runs.
  • Erasure without rewriting history. Per-subject keys; EraseScopeAsync makes every copy undecryptable, including the backups you cannot reach.
  • Fast and horizontal. Reflection-free hot paths, push-driven projections, deterministic stream buckets so workers scale out as competing consumers.

Numbers, not adjectives

Measured with BenchmarkDotNet on a fanless MacBook Air M4 (.NET 10, Arm64). Conservative ratios, not a tuned server's ceiling. Re-run: dotnet run -c Release --project tests/Stratara.Benchmarks -- --filter '*'.

What Result
Replay 1,000,000 events in memory 11.6 ms, 64 B allocated
Replay 10,000 events 0.11 ms, 64 B
Property write, compiled delegate vs reflection 0.47 ns vs 6.04 ns, ~13× faster, allocation-free
Tamper-evident chain hashing sub-microsecond per event

Methodology and caveats: Performance & scaling.

The Orleans execution model against the bus workers, same machine, one process each, PostgreSQL, Redis and RabbitMQ in containers; each command's handler appends to its aggregate's stream. Medians of three runs of 2,000 commands. The bus workers' own numbers moved by up to 23 % between runs on the same day, so read the ratios as approximate.

What Orleans execution model Bus workers
Commands to 2,000 aggregates, one each, recorded durably before the call returns 1,443 /s 487 /s
Commands to 20 aggregates, 100 each 1,320 /s 506 /s
2,000 commands to one aggregate — a single writer is the limit on both 316 /s 321 /s
Processor time per 1,000 commands, one host 2.44 CPU-s (+7 %) 2.27 CPU-s
Memory at idle / under 200 commands/s 190 / 217 MB 167 / 185 MB

Documentation

stratara.tech — concepts, getting started, guides, sample walkthroughs, the package map and the generated API reference. Every guarantee is written down as a specification under openspec/specs/ and tested in CI; the docs are derived from those specs.

Using an AI assistant? Point it at llms.txt (orientation) and llms-full.txt (every registration, option and exception, generated from the assemblies), or connect any MCP-capable client to gitmcp.io/yesbert/Stratara.

Samples

Self-contained concept samples, each running in under a second: a five-step learning path on one bank-account domain (CqrsBasics → EventSourced → OutboxWorker → MoneyTransferSaga → AspNetCoreApi), two hero samples (TamperProof, Encryption), plus Validation, Identity and IdentityDirectory. See samples/ and the walkthroughs.

dotnet run --project samples/Stratara.Sample.TamperProof

Build from source

Requires the .NET 10 SDK (global.json pins it).

dotnet build Stratara.Publish.slnf -c Release
./scripts/local-gauntlet.sh          # what CI runs

Versioning and license

Lockstep across the family — one <VersionPrefix> in Directory.Build.props. A v* tag publishes to nuget.org and nothing else does; prereleases are tagged v4.0.0-preview.1 and reach only those who ask. Per-release notes: CHANGELOG.md.

MIT — see LICENSE. Free for any use, including commercial; no competition clause, no time delay.

Contributing

Issues, questions and pull requests are welcome: open an issue, read CONTRIBUTING.md, run ./scripts/local-gauntlet.sh before a PR. Security issues go through SECURITY.md, not a public issue. Community standards: CODE_OF_CONDUCT.md; getting help: SUPPORT.md.

The repository was mirrored from a private one until 2026-08-30, one squashed commit per release; development happens here now.

About

CQRS and Event Sourcing for .NET — with tamper-evident streams and tenant-aware encryption built in, and an Orleans execution model to scale out. Integrated mediator, outbox, sagas, projections, and identity across 27 lockstep-versioned NuGet packages. MIT-licensed.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages