Nexum CQRS runtime: command, query, and notification dispatchers with ValueTask-based pipelines.
$ dotnet add package NexumModern CQRS library for .NET -- compile-time safe, zero-reflection, observable.
Nexum is a next-generation CQRS (Command Query Responsibility Segregation) library for .NET 10, designed as a successor to MediatR with focus on performance, type safety, and observability.
| Feature | MediatR | Nexum |
|---|---|---|
| Command/Query separation | Shared IRequest<T> | Separate ICommand, IQuery, IStreamQuery |
| Handler resolution | Runtime reflection | Compile-time Source Generators |
| Return type | Task<T> | ValueTask<T> (zero-alloc for sync paths) |
| Pipeline behaviors | Global (IPipelineBehavior) | Separate ICommandBehavior / IQueryBehavior |
| Observability | External packages | Built-in OpenTelemetry ActivitySource |
| Streaming | Limited CreateStream | First-class IAsyncEnumerable<T> support |
ValueTask<T> as default return type eliminates Task allocations on synchronous paths.Activity for full observability out of the box.IAsyncEnumerable<T> as a first-class citizen via IStreamQuery<T>.Result<T, TError>| Requirement | Minimum Version | Notes |
|---|---|---|
| .NET SDK | 10.0 | Target framework: net10.0 |
| C# | 14 | Automatic with .NET 10 SDK |
NuGet CLI:
# Core packages
dotnet add package Nexum.Abstractions
dotnet add package Nexum
dotnet add package Nexum.Extensions.DependencyInjection
# Recommended: Source Generator for compile-time registration
dotnet add package Nexum.SourceGenerators
# Optional: OpenTelemetry, Result Pattern, ASP.NET Core
dotnet add package Nexum.OpenTelemetry
dotnet add package Nexum.Results
dotnet add package Nexum.Extensions.AspNetCore
PackageReference (.csproj):
<ItemGroup>
<PackageReference Include="Nexum.Abstractions" Version="1.0.0" />
<PackageReference Include="Nexum" Version="1.0.0" />
<PackageReference Include="Nexum.Extensions.DependencyInjection" Version="1.0.0" />
<PackageReference Include="Nexum.SourceGenerators" Version="1.0.0" />
</ItemGroup>
using Nexum.Abstractions;
// 1. Define a command
public record CreateOrderCommand(string CustomerId, List<string> Items) : ICommand<Guid>;
// 2. Implement a handler
[CommandHandler]
public class CreateOrderHandler : ICommandHandler<CreateOrderCommand, Guid>
{
public ValueTask<Guid> HandleAsync(CreateOrderCommand command, CancellationToken ct)
{
var orderId = Guid.NewGuid();
// ... business logic ...
return ValueTask.FromResult(orderId);
}
}
// 3. Register in DI and dispatch
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddNexum(); // Source Generator auto-discovery
var app = builder.Build();
app.MapPost("/orders", async (CreateOrderCommand cmd, ICommandDispatcher dispatcher, CancellationToken ct) =>
{
var orderId = await dispatcher.DispatchAsync(cmd, ct);
return Results.Created($"/orders/{orderId}", new { Id = orderId });
});
app.Run();
A command represents an intent to modify state. It implements ICommand<TResult> with the result type.
// Command returning a Guid (order ID)
public record CreateOrderCommand(
string CustomerId,
List<OrderItemDto> Items) : ICommand<Guid>;
// Void command (no return value)
public record DeleteOrderCommand(Guid OrderId) : IVoidCommand;
Conventions:
record (or record struct for fewer allocations).ICommand<TResult> for commands that return a value.IVoidCommand for commands with no result (alias for ICommand<Unit>).A handler contains your business logic. It implements ICommandHandler<TCommand, TResult>.
[CommandHandler] // Marker attribute for Source Generator discovery (optional)
public class CreateOrderHandler : ICommandHandler<CreateOrderCommand, Guid>
{
private readonly IOrderRepository _repo;
public CreateOrderHandler(IOrderRepository repo) => _repo = repo;
public async ValueTask<Guid> HandleAsync(
CreateOrderCommand command, CancellationToken ct)
{
var order = Order.Create(command.CustomerId, command.Items);
await _repo.SaveAsync(order, ct);
return order.Id;
}
}
Conventions:
[CommandHandler] attribute is optional -- it is needed when using the Source Generator for compile-time discovery.ValueTask<TResult> (not Task<TResult>).ValueTask.FromResult(value).A query represents an intent to read state. It implements IQuery<TResult>.
public record GetOrderQuery(Guid OrderId) : IQuery<OrderDto?>;
public record GetOrdersQuery(string CustomerId) : IQuery<IReadOnlyList<OrderDto>>;
[QueryHandler]
public class GetOrderQueryHandler : IQueryHandler<GetOrderQuery, OrderDto?>
{
private readonly IOrderRepository _repo;
public GetOrderQueryHandler(IOrderRepository repo) => _repo = repo;
public async ValueTask<OrderDto?> HandleAsync(
GetOrderQuery query, CancellationToken ct)
{
var order = await _repo.GetByIdAsync(query.OrderId, ct);
return order?.ToDto();
}
}
var builder = WebApplication.CreateBuilder(args);
// Mode A: With Source Generator (recommended)
builder.Services.AddNexum();
// Mode B: Without Source Generator (assembly scanning)
builder.Services.AddNexum(assemblies: typeof(CreateOrderHandler).Assembly);
// Optional configuration
builder.Services.AddNexum(configure: options =>
{
options.DefaultPublishStrategy = PublishStrategy.Sequential;
options.MaxDispatchDepth = 16;
});
The full AddNexum signature:
public static IServiceCollection AddNexum(
this IServiceCollection services,
Action<NexumOptions>? configure = null,
params Assembly[] assemblies)
Nexum.SourceGenerators package is installed, AddNexum() discovers handlers at compile time with zero reflection.app.MapPost("/orders", async (
CreateOrderCommand command,
ICommandDispatcher dispatcher,
CancellationToken ct) =>
{
var orderId = await dispatcher.DispatchAsync(command, ct);
return Results.Created($"/orders/{orderId}", new { Id = orderId });
});
app.MapGet("/orders/{id:guid}", async (
Guid id,
IQueryDispatcher dispatcher,
CancellationToken ct) =>
{
var order = await dispatcher.DispatchAsync(new GetOrderQuery(id), ct);
return order is not null ? Results.Ok(order) : Results.NotFound();
});
Commands are dispatched via ICommandDispatcher and queries via IQueryDispatcher. The compiler enforces separation -- you cannot dispatch a command through IQueryDispatcher or vice versa.
Nexum supports separate pipeline behaviors for commands and queries using the Russian doll model. Each behavior wraps the next, enabling cross-cutting concerns like validation, logging, and transactions.
ICommandBehavior<TCommand, TResult> -- wraps command execution.IQueryBehavior<TQuery, TResult> -- wraps query execution.[BehaviorOrder(int)] -- controls execution order (lower values execute first).Behaviors are type-safe and scoped: a command validation behavior will never accidentally run in the query pipeline.
Domain events are modeled as INotification and dispatched via INotificationPublisher.PublishAsync(). Nexum supports four publish strategies:
Task.WhenAll.BackgroundService. Each handler runs in its own IServiceScope. Exceptions are routed to INotificationExceptionHandler instead of propagating to the caller.The default strategy is configured via NexumOptions.DefaultPublishStrategy and can be overridden per-publish call.
For queries that return sequences of results, Nexum provides first-class IAsyncEnumerable<T> support via IStreamQuery<TResult>. Stream queries are dispatched with IQueryDispatcher.StreamAsync() and support dedicated IStreamQueryBehavior<TQuery, TResult> pipeline behaviors.
Nexum provides structured exception handling outside the pipeline via ICommandExceptionHandler<TCommand, TException> and IQueryExceptionHandler<TQuery, TException>. Exception handlers are side-effect only (logging, metrics, alerts) -- the dispatcher always re-throws after invoking them. Handlers are resolved most-specific-first based on both the command/query type and the exception type.
The Nexum.OpenTelemetry package adds automatic distributed tracing and metrics to every dispatch. Each command, query, and notification creates an Activity with structured tags, enabling full observability through any OpenTelemetry-compatible backend. No code changes required -- just add the package and configure your ActivitySource.
The Nexum.Results package provides a native Result<T, TError> type for explicit error handling without exceptions. Results use composition (not inheritance) and integrate naturally with Nexum commands and queries. A convenience alias Result<T> defaults the error type to NexumError.
The Nexum.Extensions.AspNetCore package provides middleware, endpoint routing helpers, and Problem Details integration for seamless use of Nexum in ASP.NET Core applications.
| Package | Description |
|---|---|
Nexum.Abstractions | Core interfaces (ICommand, IQuery, INotification, etc.). Zero dependencies. |
Nexum.SourceGenerators | Roslyn Source Generators for compile-time handler registration and validation. |
Nexum | Dispatchers (CommandDispatcher, QueryDispatcher, NotificationPublisher), pipeline middleware. |
Nexum.OpenTelemetry | ActivitySource, metrics, System.Diagnostics integration. |
Nexum.Results | Optional Result<T, TError>, NexumError, IResultAdapter. |
Nexum.Extensions.DependencyInjection | IServiceCollection.AddNexum() extensions. |
Nexum.Extensions.AspNetCore | Middleware, endpoint routing, Problem Details integration. |
Nexum with Source Generators is 2x faster than MediatR with zero allocations for simple commands. With pipeline behaviors, the advantage grows to 1.7x faster with 3.8x less memory. For notifications with 5 handlers, Nexum is 2.2x faster with 28x less memory.
Even without Source Generators, the Nexum Runtime dispatcher is 1.5x faster than MediatR with zero allocations.
BenchmarkDotNet v0.15.8, macOS Tahoe, Apple M3 Max, .NET SDK 10.0.103, .NET 10.0.3 (10.0.326.7603)
Measured February 2026
| Method | Mean | Allocated |
|---|---|---|
| Nexum Source Generator | 18.96 ns | 0 B |
| Nexum Runtime | 25.84 ns | 0 B |
| MediatR | 39.19 ns | 208 B |
Nexum SG vs MediatR: 2.1x faster, zero allocations. Nexum Runtime vs MediatR: 1.5x faster, zero allocations.
| Method | Mean | Allocated |
|---|---|---|
| Nexum Source Generator | 72.24 ns | 192 B |
| Nexum Runtime | 88.73 ns | 408 B |
| MediatR | 121.37 ns | 736 B |
Nexum SG vs MediatR: 1.7x faster, 3.8x less memory. Nexum Runtime vs MediatR: 1.4x faster, 1.8x less memory.
| Method | Mean | Allocated |
|---|---|---|
| Nexum Source Generator | 64.40 ns | 32 B |
| Nexum Runtime | 64.40 ns | 32 B |
| MediatR | 143.35 ns | 896 B |
Nexum vs MediatR: 2.2x faster, 28x less memory.
Nexum Source Generator uses a tiered architecture. Each tier builds on the previous one, progressively eliminating overhead:
DispatchAsync call sites at compile time, eliminating virtual dispatch.| Tier | Mean | Allocated | vs Runtime |
|---|---|---|---|
| Tier 3 -- Interceptor | 16.55 ns | 0 B | 1.52x faster |
| Tier 2 -- Compiled Pipeline | 19.04 ns | 0 B | 1.32x faster |
| Tier 1 -- Runtime | 25.19 ns | 0 B | baseline |
All three tiers achieve zero allocations. Tier 3 interceptors are the fastest path -- 34% faster than Runtime and 13% faster than Tier 2 compiled pipelines.
Nexum is designed as a drop-in evolution from MediatR. The migration can be done gradually -- both libraries can coexist in the same project during the transition. Key changes include replacing IRequest<T> with ICommand<T>/IQuery<T>, Task<T> with ValueTask<T>, Handle() with HandleAsync(), and Send() with DispatchAsync().
See MIGRATION.md for a complete step-by-step migration guide with before/after code examples.
Nexum includes comprehensive architecture documentation covering:
NexumOptions, behavior ordering, publish strategies, and DI setup.