CQRSharp is a fast, Native AOT-compatible CQRS framework for C# and .NET. It provides commands, queries, events, source-generated dispatch, pipeline behaviors, idempotency, resilience and transactional outbox support without runtime reflection.
- Wired at compile time. A source generator finds your handlers, so there is no reflection or assembly scanning, it runs under Native AOT, and a request without a handler shows up in your IDE, not in production.
- Faster than MediatR inside a scope, for requests, streams and notifications (benchmarks).
- Batteries included, pay for what you use. Validation, retries, timeouts, rate limiting, idempotency, a unit of work, a transactional outbox (Redis or EF Core) and a RabbitMQ transport for integration events are one builder call each, and cost nothing where you do not use them.
MIT licensed. Documentation · Changelog · Project page
dotnet add package CQRSharp// 1. Register. AddCqrsGenerated is emitted by the source generator: it wires every handler it discovered, plus the
// validation and exception-handling behaviors.
services.AddCqrsGenerated();
// 2. Define a request and its handler.
public sealed class CreateUser : CommandBase
{
public required string Name { get; init; }
}
public sealed class CreateUserHandler : ICommandHandler<CreateUser>
{
public Task<CommandResult> Handle(CreateUser command, CancellationToken cancellationToken)
=> Task.FromResult(CommandResult.FromSuccess());
}
// 3. Dispatch through ICqrsDispatcher. A failure is returned, not thrown: check IsSuccess.
public sealed class Users(ICqrsDispatcher dispatcher)
{
public async Task<bool> Create(string name)
{
CommandResult result = await dispatcher.Send(new CreateUser { Name = name });
return result.IsSuccess;
}
}The snippet needs no using lines: the CQRSharp meta-package adds global usings for its namespaces
(Getting started explains them
and the opt-out). Queries derive from QueryBase<TResult>, streams from StreamRequestBase<TItem> and are dispatched with
dispatcher.Stream, and notifications implement INotification and go through dispatcher.Publish. Scaffold a working
app with dotnet new cqrsharp, or read
Getting started.
Pass a builder to AddCqrsGenerated to add behaviors and stores. The order of the verbs does not matter: the builder
applies each behavior at its fixed place in the pipeline. In this form validation and exception handling are on (they do
nothing for a request without validators or exception hooks); every other behavior runs only when its verb is called.
Wiring mistakes do not fail silently: an IIdempotentRequest whose behavior was never enabled fails its dispatch, and the
startup validator, on by default in the Development environment and everywhere with ValidateOnStart(), reports every
such mistake at host start instead:
services.AddCqrsGenerated(b => b
.UseResilience(r => r.MaxRetries = 3)
.UseEntityFrameworkCoreUnitOfWork<AppDbContext>()
.UseIdempotency(i => i.UseRedis(redisConnectionString))
.UseOutbox(o => o.UseEntityFrameworkCore<AppDbContext>())
.ValidateOnStart());| Package | Adds |
|---|---|
CQRSharp |
The meta-package to install: the contracts, the runtime, the pipeline behaviors and builder, the source generator, and the global usings. |
CQRSharp.Abstractions |
The contracts alone (requests, notifications, results, store and unit-of-work interfaces) and the analyzers. A project that only declares requests can reference it; a project that declares handlers needs CQRSharp. |
CQRSharp.Core |
The runtime: the dispatcher, the pipeline executor, notification publishing, the background queue, the outbox processor, diagnostics and the in-memory stores. Pulled in by the meta-package. |
CQRSharp.Pipelines |
The built-in behaviors and the fluent builder. Pulled in by the meta-package. |
CQRSharp.Redis |
Redis outbox, inbox and idempotency stores. Docs |
CQRSharp.EntityFrameworkCore |
EF Core (relational) outbox, inbox and idempotency stores, and EfCoreUnitOfWork<TContext>. Docs |
CQRSharp.RabbitMQ |
A RabbitMQ transport: integration events published through the outbox with broker confirms, and consumed into it, deduplicated. Native-AOT compatible. Docs |
CQRSharp.AspNetCore |
CommandResult → IResult (the status follows the result's error kind: 400 / 401 / 403 / 404 / 409 / 503), pipeline exceptions → ProblemDetails (400 / 409 / 422 / 429 / 503 / 504), Idempotency-Key header handling. Docs |
CQRSharp.FluentValidation |
Runs your FluentValidation validators inside the validation behavior. Docs |
CQRSharp.Testing |
RecordingCqrsDispatcher, a stub-and-record dispatcher for unit tests; no test-framework dependency. Docs |
CQRSharp.Testing.Xunit.V3 |
The contract-test suites (for a custom outbox, inbox or idempotency store, or a notification transport), on xUnit v3. Docs |
CQRSharp.Templates |
dotnet new install CQRSharp.Templates, then dotnet new cqrsharp. |
Which packages work under Native AOT: Native AOT.
- Compile-time wiring. Dispatch, registration and outbox serialization are generated code: no
MakeGenericType, no assembly scanning, no reflection-based JSON. Multi-assembly solutions compose automatically: oneAddCqrsGeneratedwires the handlers of the calling assembly and of every assembly it references. - Analyzers with code fixes. Missing or duplicate handlers, a stream request sent with
Send, a pipeline exemption that has no effect, a custom request context without a factory, a directAddCqrs()call: reported as you type (CQRA*,CQRGEN*). See Diagnostics. - A defined request lifecycle. Pre- and post-handler interceptors, post-handlers that see the returned value or the
exception, and
Initiated→Completed/Failedlifecycle notifications: once a request is initiated, exactly one terminal notification follows. Commands, queries and streams follow the same contract. - Reliable messaging. A transactional outbox with at-least-once delivery, persisted retry and back-off,
dead-lettering and W3C trace propagation. Delivery is per handler: each handler of a notification gets its own
stored message, attempts and dead letter, so a failing handler never makes a healthy one run again. Deliveries are
ordered per partition key (
PartitionBy = nameof(OrderId)), also across several processor instances, which claim messages under leases so they can share one outbox. An inbox records each delivery, so a redelivered message is recognized and skipped; with an EF Core inbox and unit of work over oneDbContext, the handler's changes and the record commit together. Dead letters can be listed, requeued and purged, and a notification can be scheduled for later delivery (PublishAt/PublishAfter). See The outbox. - Integration events over RabbitMQ.
UseRabbitMq(...)insideUseOutboxsends the notifications you name to RabbitMQ through the outbox (atomic with your data, confirmed by the broker, in order per key, never dead-lettered by an outage) and takes the queues you name in through the inbox, acknowledging a message only once it is stored, so a consumer on the EF Core outbox, inbox and unit of work takes each one in exactly once. See RabbitMQ. - Idempotency that answers the retry. A duplicate of a completed request gets the original result back (a plain
CommandResultalways; any other result through a result serializer), a duplicate of one still running gets a distinguishable "in progress" (409 withRetry-Afterover HTTP), and a key reused with a different payload is rejected (422), not replayed. - Observability.
ActivitySourcespans for every dispatch, behavior and outbox delivery; request-duration, notification, outbox and queue metrics; outbox backlog gauges and an outbox health check; a diagnostics API that reports how each request is bound; source-generated log messages with stable event ids. Nothing is measured until something listens. - Testability. Every time read goes through
TimeProvider, so retries, timeouts, leases and expiry are deterministic underFakeTimeProvider.CQRSharp.Testing.Xunit.V3ships the contract suites every built-in store passes (derive one class to check a custom store), andCQRSharp.Testinga recording dispatcher for unit-testing code that dispatches.
MediatR is the ubiquitous runtime mediator; CQRSharp is a CQRS framework: generated dispatch plus the infrastructure that usually gets hand-rolled around a mediator.
| CQRSharp | MediatR | |
|---|---|---|
| License | MIT | Commercial since v13 (12.x and earlier remain Apache-2.0) |
| Handler discovery | Source generator | Runtime assembly scanning + reflection |
| Native AOT / trimming | Yes, built and run under AOT in CI | Not a design goal |
| Missing / duplicate handler | Build-time diagnostic | Runtime exception |
| Command / query distinction | First-class (ICommand, IQuery<T>, CommandResult) |
One IRequest<T> |
| Pipeline behaviors | Yes, priority-ordered; per-request exemptions | Yes, registration-ordered |
| Streaming requests | Yes, with stream behaviors | Yes |
| Notifications | Sequential / parallel strategies, notification behaviors | Pluggable publisher |
| Built-in validation, retry, timeout, rate limiting | Yes | No, bring your own behaviors |
| Idempotency (in-memory / Redis / EF Core stores), unit of work | Yes, with result replay | No |
| Transactional outbox | Yes, in-memory / Redis / EF Core stores; multi-instance; per-handler delivery; ordered per key | No |
| Message broker transport | Yes, RabbitMQ, through the outbox and the inbox | No |
| ASP.NET Core result / ProblemDetails mapping, FluentValidation adapter, test doubles | Yes (separate packages) | No |
| Startup configuration validation | Yes | No |
| Tracing / metrics | Built in (ActivitySource, Meter) |
No |
Choose MediatR if you want the de-facto standard and its ecosystem, and the licensing fits. Choose CQRSharp if you want AOT-safe dispatch and the outbox, idempotency, resilience and diagnostics from one tested, MIT-licensed place. Moving an existing MediatR 12.x codebase is covered step by step, one feature at a time, in Migrating from MediatR.
Measured with BenchmarkDotNet on trivial handlers, with a new request object per call and every library in its documented
default configuration, so the numbers are each framework's own cost. Two things are measured: a dispatch through a
mediator resolved once from a long-lived DI scope, and a whole request scope (create a scope, resolve the mediator,
dispatch once, dispose the scope), which is what a web request pays. ICqrsDispatcher is scoped, so the per-scope numbers
include resolving it in every new scope. MediatR is benchmarked at 12.5.0, its last Apache-2.0 release. The method, the
scenarios and the commands to reproduce the run are in
benchmarks/README.md.
The harness covers a request, a request with one behavior, a notification (published as its own type and as
INotification) and a three-item stream, each both ways. The results of the current harness, as the tables of one full
run with every library side by side, are published in
benchmarks/README.md; compare
the ratios there, since the absolute times move with the machine.
Three design points keep the per-dispatch cost low: lifecycle notifications and pipeline stages are pay-for-use
(nothing is resolved or published for a stage the container has no registration for, and what is registered is asked
once per provider and message type, not per dispatch); publishing is provider-wide, so a scope costs a publish nothing to
set up; and when nothing wraps a handler Send returns the handler's own task.
src/ the packages: CQRSharp (meta), .Abstractions, .Core, .Pipelines, .Generators, .Analyzers,
.Redis, .EntityFrameworkCore, .RabbitMQ, .AspNetCore, .FluentValidation, .Testing, .Testing.Xunit.V3
samples/ CQRSharp.Sample (end-to-end self-test, Native AOT canary), CQRSharp.Sample.AspNetCore (minimal API
over CQRSharp.AspNetCore, Native AOT canary for the HTTP edge), CQRSharp.Sample.ExternalModule
tests/ the test suite and a second-assembly fixture
benchmarks/ the BenchmarkDotNet comparison (not part of the solution)
templates/ the `dotnet new cqrsharp` template and the CQRSharp.Templates package project
docs/ the documentation
Build and test with dotnet build CQRSharp.sln -warnaserror and dotnet test --project tests/CQRSharp.Tests. The Redis,
PostgreSQL, SQL Server and RabbitMQ tests use the servers named by CQRSHARP_TEST_REDIS, CQRSHARP_TEST_POSTGRES,
CQRSHARP_TEST_SQLSERVER and CQRSHARP_TEST_RABBITMQ when set, and otherwise start containers with Testcontainers;
without Docker they skip.
CONTRIBUTING.md has the details.
All of CQRSharp's documentation was generated using large language models (LLMs): this README, the guides in docs, the project pages on bisocm.org, the XML documentation comments (the summaries IntelliSense shows) in the source code, and certain code comments. It may contain errors, omissions, or statements that do not match what the library actually does. Where the documentation and the library disagree, the library's behavior is what counts; please report the discrepancy so it can be fixed.
Issues and pull requests are welcome. CI builds every pull request with warnings as errors and runs the full suite; a
change to the generator should keep IncrementalGeneratorCachingTests green and come with a case in
GeneratedCodeCompilesTests. Releases are cut by bumping <Version> in Directory.Build.props on the Release branch.
MIT.