HomeNikola Knezevic

In this article

Banner

CQRS Kommand in ASP.NET Core

16 Apr 2026
7 min

Special Thanks to Our Sponsors:

Hypereal AI Logo

Generate images, videos and avatars on demand with HypeReal AI.

It's a serverless platform that gives you 1 API with 50+ models.

Switch between Flux, Sora, Kling and others with one parameter.

If you want to experiment, grab the free tokens and test it yourself:

👉 Learn more

Sponsor Newsletter

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:

bash
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:

csharp
public sealed record CreateOrderCommand(
    string CustomerName,
    string Email,
    decimal TotalAmount) : ICommand<Guid>;
csharp
public record GetOrdersQuery : IQuery<List<OrderResponse>>;

If a command doesn’t return anything, use Unit.

Each command/query has its own handler:

csharp
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;
    }
}
csharp
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:

csharp
services.AddKommand(config =>
{
    config.RegisterHandlersFromAssembly(Assembly.GetExecutingAssembly());
});

Minimal API example

To dispatch commands/queries, use IMediator:

csharp
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);
});
csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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 Code

I hope you enjoyed it, subscribe and get a notification when a new blog is up!

Subscribe

Stay tuned for valuable insights every Thursday morning.