A popular pattern when building modern .NET applications is CQRS, especially when paired with the Mediator pattern.
I like this approach because it helps structure code, reduce coupling and enforce a clear separation of concerns, keeping business logic clean and maintainable.
For years, the go-to solution for this was MediatR, at least while it was free.
That’s where Kommand comes in. It's a fully free, open-source and lightweight .NET library designed to make commands, queries, validation and cross-cutting concerns simple and explicit.
Kommand
Kommand wasn’t created as a side project or a “better MediatR clone”, it was built out of a real production need.
It was originally developed for internal use at Atherio as part of their system architecture. The goal was to create a clean, performant CQRS implementation with built-in observability and validation.
Eventually, they decided to open-source it to give back to the .NET ecosystem.
What is CQRS?
If you’re not familiar with CQRS (Command Query Responsibility Segregation), it’s all about separation.
It splits read and write operations:
- Commands - For create, update and delete operations
- Queries - Read-only operations
The philosophy is simple, commands should be task-based, not data-centric, while queries should never modify state and typically return DTOs.
For stronger isolation, you can even separate read and write databases. In such cases, keeping them in sync often involves patterns like event sourcing.
Getting Started
To get started with Kommand, you need to install the NuGet package. You can do this via the NuGet Package Manager or by running the following command in the Package Manager Console:
Install-Package Kommand
NOTE: Kommand is currently in pre-release. Make sure to enable Include prerelease in your NuGet manager.
Basic Flow
Kommand provides abstractions for commands and queries:
public sealed record CreateOrderCommand(
string CustomerName,
string Email,
decimal TotalAmount) : ICommand<Guid>;
public record GetOrdersQuery : IQuery<List<OrderResponse>>;
If a command doesn’t return anything, use Unit.
Each command/query has its own handler:
public class CreateOrderCommandHandler(ApplicationDbContext dbContext) : ICommandHandler<CreateOrderCommand, Guid>
{
public async Task<Guid> HandleAsync(CreateOrderCommand command, CancellationToken cancellationToken)
{
var order = new Order(command.CustomerName, command.Email, command.TotalAmount);
dbContext.Orders.Add(order);
await dbContext.SaveChangesAsync(cancellationToken);
return order.Id;
}
}
public class GetOrdersQueryHandler(ApplicationDbContext dbContext) : IQueryHandler<GetOrdersQuery, List<OrderResponse>>
{
public async Task<List<OrderResponse>> HandleAsync(GetOrdersQuery command, CancellationToken cancellationToken) =>
await dbContext.Orders
.Select(order => order.Adapt<OrderResponse>())
.ToListAsync(cancellationToken);
}
Registration
Registration is simple and concise:
services.AddKommand(config =>
{
config.RegisterHandlersFromAssembly(Assembly.GetExecutingAssembly());
});
Minimal API example
To dispatch commands/queries, use IMediator:
app.MapPost("orders", async (IMediator mediator, CreateOrderRequest request, CancellationToken cancellationToken) =>
{
var command = request.Adapt<CreateOrderCommand>();
var response = await mediator.SendAsync(command, cancellationToken);
return Results.Ok(response);
});
app.MapGet("orders", async (IMediator mediator, CancellationToken cancellationToken) =>
{
var request = new GetOrdersQuery();
var response = await mediator.QueryAsync(request, cancellationToken);
return Results.Ok(response);
});
Use SendAsync for commands and QueryAsync for queries, the separation into two methods makes the intent explicit.
Validation
Kommand has built-in support for validation, you can define validators that run before the handler:
public class CreateOrderCommandValidator : IValidator<CreateOrderCommand>
{
public async Task<ValidationResult> ValidateAsync(CreateOrderCommand request, CancellationToken cancellationToken)
{
var errors = new List<ValidationError>();
if (string.IsNullOrWhiteSpace(request.Email))
{
errors.Add(new ValidationError(nameof(request.Email), "Email is required"));
}
if (string.IsNullOrWhiteSpace(request.CustomerName))
{
errors.Add(new ValidationError(nameof(request.CustomerName), "CustomerName is required"));
}
if (request.TotalAmount <= 0)
{
errors.Add(new ValidationError(nameof(request.TotalAmount), "TotalAmount must be greater than 0"));
}
return errors.Count > 0
? ValidationResult.Failure(errors)
: ValidationResult.Success();
}
}
What stands out here is that validation supports dependency injection, meaning you can validate against a database or external services, not just request properties.
Enable validation like this:
services.AddKommand(config =>
{
config.RegisterHandlersFromAssembly(Assembly.GetExecutingAssembly());
config.WithValidation();
});
Type-Specific Interceptors
Interceptors are another powerful feature.
Kommand supports three types:
- IInterceptor<TRequest, TResponse>
- ICommandInterceptor<TCommand, TResponse>
- IQueryInterceptor<TQuery, TResponse>
This allows fine-grained control depending on the operation type.
Here's an example of a global logging interceptor:
public class LoggingInterceptor<TRequest, TResponse>
(ILogger<LoggingInterceptor<TRequest, TResponse>> logger)
: IInterceptor<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> HandleAsync(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
logger.LogInformation("Executing {RequestType}", typeof(TRequest).Name);
var response = await next();
logger.LogInformation("Completed {RequestType}", typeof(TRequest).Name);
return response;
}
}
Specific operation-type interceptors are created in a similar way.
They also need to be registered explicitly:
services.AddKommand(config =>
{
config.RegisterHandlersFromAssembly(Assembly.GetExecutingAssembly());
config.AddInterceptor(typeof(LoggingInterceptor<,>));
config.WithValidation();
});
Conclusion
Kommand offers a clean and explicit way to implement CQRS in .NET applications.
I think this library checks everything you should look for:
- Clean separation
- Lightweight mediator
- Built-in validation pipeline
- Cross-cutting handling
- Minimal setup and boilerplate
Additionally, it supports even more features such as pub/sub pattern and built-in observability.
If you like this package, let me know and we can create a follow-up blog post covering its other features.
If you want to check out examples I created, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!
