CommanderCQRS 2.0.0
dotnet add package CommanderCQRS --version 2.0.0
NuGet\Install-Package CommanderCQRS -Version 2.0.0
<PackageReference Include="CommanderCQRS" Version="2.0.0" />
<PackageVersion Include="CommanderCQRS" Version="2.0.0" />
<PackageReference Include="CommanderCQRS" />
paket add CommanderCQRS --version 2.0.0
#r "nuget: CommanderCQRS, 2.0.0"
#:package CommanderCQRS@2.0.0
#addin nuget:?package=CommanderCQRS&version=2.0.0
#tool nuget:?package=CommanderCQRS&version=2.0.0
Commander.CQRS
A high-performance, modern CQRS / mediator library for .NET — designed to be a faster and cleaner alternative to MediatR.
dotnet add package CommanderCQRS
# Optional: FluentValidation integration
dotnet add package CommanderCQRS.FluentValidation
Why Commander.CQRS
| Feature | Commander.CQRS 2.x | MediatR 12.x |
|---|---|---|
| Multi-target net8 / net10 | Yes | Yes |
CancellationToken everywhere |
Yes | Yes |
| Pipeline behaviors (request + stream) | Yes | Request only |
| Streaming queries | Yes | Yes |
ISender / IPublisher split |
Yes | Yes |
| Pluggable notification publisher (sequential / parallel) | Yes | Yes |
ValueTask on the hot path |
Yes | Task only |
Strongly-typed ICommandResult / IQueryResult |
Yes | No |
| First-class FluentValidation pipeline behavior | Yes | Third party |
| Reflection-free dispatch (JIT-specialized) | Yes | Some reflection |
AOT / trim friendly core (IsAotCompatible=true) |
Yes | No |
| Source generator for handler registration (zero startup reflection) | Yes | No |
Public API surface tracked via PublicAPI.Shipped.txt |
Yes | No |
The core package has a single transitive dependency (Microsoft.Extensions.DependencyInjection.Abstractions). FluentValidation is opt-in via the Commander.FluentValidation adapter — keep your core lean.
Targets
net8.0(LTS)net10.0(LTS, latest)
Quick start
using Commander;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddCommander(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<Program>();
});
var sp = services.BuildServiceProvider();
var commander = sp.GetRequiredService<ICommander>();
var result = await commander.Execute<AddProductCommand, Product>(
new AddProductCommand { Name = "Notebook" });
Console.WriteLine(result.IsSuccess);
AOT / trim friendly registration (source generator)
AddCommander(cfg => cfg.RegisterServicesFromAssembly(...)) uses runtime reflection. For
trimmed or AOT-published apps, the bundled source generator emits an extension method that
registers every handler in your project at compile time — zero reflection, zero startup cost:
services.AddCommander(cfg => { /* options, behaviors, publisher */ });
services.AddCommanderHandlers(); // generated by Commander.SourceGenerator at compile time
The generator ships inside the CommanderCQRS NuGet package under analyzers/dotnet/cs,
so installing the package is the only step required.
Commands
public sealed class AddProductCommand : Command
{
public string Name { get; init; } = string.Empty;
}
public sealed class AddProductHandler : ICommandHandler<AddProductCommand, Product>
{
public ValueTask<ICommandResult<Product>> Execute(
AddProductCommand request,
CancellationToken cancellationToken = default)
=> CommandResult<Product>.SuccessAsync(new Product { Name = request.Name });
}
Execute<TRequest>(...) (no response) is also supported via ICommandHandler<TRequest>.
Queries
public sealed class ProductQuery : Query
{
public ProductQuery(Guid id) => Id = id;
public Guid Id { get; }
}
public sealed class ProductQueryHandler : IQueryHandler<ProductQuery, ProductOutput>
{
public ValueTask<IQueryResult<ProductOutput>> ExecuteQuery(
ProductQuery request,
CancellationToken cancellationToken = default)
=> QueryResult<ProductOutput>.SuccessAsync(new ProductOutput { Id = request.Id });
}
Streaming queries
public sealed class TailLogsHandler : IStreamQueryHandler<TailLogsQuery, LogLine>
{
public async IAsyncEnumerable<LogLine> Stream(
TailLogsQuery request,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var line in source.ReadAllAsync(cancellationToken))
yield return line;
}
}
await foreach (var line in commander.Stream<TailLogsQuery, LogLine>(query, ct))
Console.WriteLine(line);
Events / notifications
public sealed class ProductAddedEvent : Event
{
public ProductAddedEvent(string productName) => ProductName = productName;
public string ProductName { get; }
}
public sealed class EmailHandler : IEventHandler<ProductAddedEvent>
{
public ValueTask<IEventResult> Publish(ProductAddedEvent e, CancellationToken ct = default)
=> EventResult.SuccessAsync();
}
await commander.Publish(new ProductAddedEvent("Notebook"));
Pipeline behaviors
public sealed class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : Message
{
public async ValueTask<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
Console.WriteLine($"-> {typeof(TRequest).Name}");
try
{
return await next(cancellationToken);
}
finally
{
Console.WriteLine($"<- {typeof(TRequest).Name}");
}
}
}
services.AddCommander(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<Program>();
cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
});
Behaviors run in registration order, wrap both commands and queries, and receive the request object plus a RequestHandlerDelegate<TResponse> so they can short-circuit, retry, time, or transform the result.
Notification publisher strategies
By default events are dispatched sequentially with ForeachAwaitPublisher — deterministic, easy to reason about, no scheduler overhead. Switch to parallel fan-out:
using Commander.Internal;
services.AddCommander(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<Program>();
cfg.UseNotificationPublisher<TaskWhenAllPublisher>();
});
Or write your own by implementing INotificationPublisher.
Validation (FluentValidation)
using Commander.FluentValidation;
using FluentValidation;
public sealed class AddProductValidator : CommanderValidator<AddProductCommand>
{
public AddProductValidator() => RuleFor(x => x.Name).NotEmpty();
}
services.AddCommander(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<Program>();
cfg.AddFluentValidation(); // turns on the validation pipeline behavior
});
When validation fails, the pipeline returns a failed ICommandResult / ICommandResult<T> without invoking the handler.
If you don't want FluentValidation, implement ICommandValidator<T> directly — the validation pipeline behavior accepts any implementation.
ISender and IPublisher
If a class only sends commands/queries, depend on ISender. If it only publishes events, depend on IPublisher. ICommander is the umbrella, equivalent to MediatR's IMediator. All three resolve to the same instance.
Performance
A Commander.Benchmarks project ships side-by-side with the library and compares Commander vs MediatR with BenchmarkDotNet:
dotnet run -c Release --project Commander.Benchmarks
Designed for speed:
- Hot path is fully generic — the JIT specializes each call site, no per-call reflection
- Result types use cached singletons for
Success()to avoid allocations - Notification dispatch materializes handlers once per call, with a pre-bound
Funcper executor ValueTaskend-to-end avoids unnecessaryTaskallocations for synchronously-completing handlersTryAddEnumerableregistration deduplicates handlers without sacrificing event multi-cast
Migration from 1.x
| 1.x | 2.x |
|---|---|
services.AddCommander<TMarker>() |
Same call still works or use the new services.AddCommander(cfg => cfg.RegisterServicesFromAssemblyContaining<TMarker>()) |
Handlers without CancellationToken |
Add CancellationToken cancellationToken = default to handlers |
CommanderValidator<T> from Commander namespace |
Commander.FluentValidation.CommanderValidator<T>; install CommanderCQRS.FluentValidation and call cfg.AddFluentValidation() |
Task.WhenAll-based publish (hardcoded) |
Default is now ForeachAwaitPublisher. Restore old behavior with cfg.UseNotificationPublisher<TaskWhenAllPublisher>() |
Event.AggreggateId (typo) / SetAggreggateId |
Renamed to AggregateId / SetAggregateId |
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
-
net8.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.