Clywell.Core.Cqrs 1.1.2

dotnet add package Clywell.Core.Cqrs --version 1.1.2
                    
NuGet\Install-Package Clywell.Core.Cqrs -Version 1.1.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Clywell.Core.Cqrs" Version="1.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Clywell.Core.Cqrs" Version="1.1.2" />
                    
Directory.Packages.props
<PackageReference Include="Clywell.Core.Cqrs" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Clywell.Core.Cqrs --version 1.1.2
                    
#r "nuget: Clywell.Core.Cqrs, 1.1.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Clywell.Core.Cqrs@1.1.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Clywell.Core.Cqrs&version=1.1.2
                    
Install as a Cake Addin
#tool nuget:?package=Clywell.Core.Cqrs&version=1.1.2
                    
Install as a Cake Tool

Clywell.Core.Cqrs

Clywell.Core.Cqrs Clywell.Core.Cqrs.FluentValidation License

Packages

Package Description
Clywell.Core.Cqrs Core interfaces, dispatcher, pipeline infrastructure, and bundled source generator
Clywell.Core.Cqrs.FluentValidation FluentValidation pipeline behavior

Overview

A lightweight CQRS framework built without MediatR or any third-party mediator dependency. It provides strongly-typed command/query interfaces, a DI-resolved dispatcher, and a composable pipeline behavior system for cross-cutting concerns such as validation, logging, and caching.

The dispatcher resolves handlers and behaviors entirely through the DI container. The companion source generator replaces assembly-scanning reflection with compile-time registration, making the solution trimmer- and NativeAOT-compatible.


Installation

Install the core package — the source generator is bundled inside it:

dotnet add package Clywell.Core.Cqrs

Optionally add FluentValidation integration:

dotnet add package Clywell.Core.Cqrs.FluentValidation

Quick Start

1. Register services

builder.Services
    .AddCqrs()           // registers the IDispatcher
    .AddCqrsHandlers();  // source-generated — registers all handlers, zero reflection

2. Define a command

public record CreateOrderCommand(Guid CustomerId, string ProductId) : ICommand<OrderDto>;

3. Implement the handler

public class CreateOrderHandler : ICommandHandler<CreateOrderCommand, OrderDto>
{
    public async Task<OrderDto> HandleAsync(CreateOrderCommand command, CancellationToken ct = default)
    {
        // write business logic here
        return new OrderDto(Guid.NewGuid(), command.ProductId);
    }
}

4. Inject IDispatcher and dispatch

app.MapPost("/orders", async (IDispatcher dispatcher, CreateOrderRequest request, CancellationToken ct) =>
{
    var command = new CreateOrderCommand(request.CustomerId, request.ProductId);
    var order = await dispatcher.SendAsync(command, ct);
    return Results.Created($"/orders/{order.Id}", order);
});

Commands vs Queries

Concept Interface Dispatch method Intent
Command ICommand<TResult> dispatcher.SendAsync(command, ct) Write — changes state
Query IQuery<TResult> dispatcher.QueryAsync(query, ct) Read — must not change state

Query example

// Define
public record GetOrderByIdQuery(Guid OrderId) : IQuery<OrderDto>;

// Handle
public class GetOrderByIdHandler : IQueryHandler<GetOrderByIdQuery, OrderDto>
{
    public async Task<OrderDto> HandleAsync(GetOrderByIdQuery query, CancellationToken ct = default)
    {
        // read-only data access
        return await _repository.GetByIdAsync(query.OrderId, ct)
            ?? throw new KeyNotFoundException($"Order {query.OrderId} not found.");
    }
}

// Dispatch
var order = await dispatcher.QueryAsync(new GetOrderByIdQuery(orderId), ct);

Pipeline Behaviors

IPipelineBehavior<TRequest, TResult> is the middleware contract. Behaviors wrap handler execution and run in the order they are registered in DI — the first registered behavior is the outermost wrapper.

How the pipeline works

Dispatcher
  └── Behavior 1 (outermost — registered first)
        └── Behavior 2
              └── Behavior N (innermost — registered last)
                    └── Handler

When SendAsync or QueryAsync is called the dispatcher:

  1. Resolves the handler via the DI container.
  2. Resolves all registered IPipelineBehavior<TRequest, TResult> instances for that request type.
  3. Builds a delegate chain from innermost to outermost.
  4. Invokes the outermost delegate — execution flows through every behavior before reaching the handler and unwinds the same way on the return path.

Implementing a custom behavior

public class LoggingBehavior<TRequest, TResult> : IPipelineBehavior<TRequest, TResult>
    where TRequest : notnull
{
    private readonly ILogger<LoggingBehavior<TRequest, TResult>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResult>> logger)
        => _logger = logger;

    public async Task<TResult> HandleAsync(
        TRequest request,
        RequestHandlerDelegate<TResult> next,
        CancellationToken ct = default)
    {
        var requestName = typeof(TRequest).Name;
        _logger.LogInformation("Handling {Request}", requestName);

        var result = await next(ct);  // call the next behavior or handler

        _logger.LogInformation("Handled {Request}", requestName);
        return result;
    }
}

Registering a custom behavior

Register open-generic behaviors so they apply to every request type, or use a closed-generic registration to target a specific request:

// applies to every command and query
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));

// applies only to CreateOrderCommand
services.AddTransient<IPipelineBehavior<CreateOrderCommand, OrderDto>, AuditBehavior>();

FluentValidation Integration

Clywell.Core.Cqrs.FluentValidation provides ValidationBehavior<TRequest, TResult> — a pipeline behavior that runs all registered IValidator<TRequest> instances before the handler is invoked.

Registration

builder.Services
    .AddCqrs()
    .AddCqrsHandlers()
    .AddCqrsFluentValidation(typeof(CreateOrderValidator).Assembly);

AddCqrsFluentValidation registers ValidationBehavior<,> as an open-generic transient pipeline behavior and scans the provided assemblies for all IValidator<T> implementations.

Define validators

public class CreateOrderValidator : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderValidator()
    {
        RuleFor(x => x.ProductId).NotEmpty().WithMessage("Product ID is required.");
        RuleFor(x => x.CustomerId).NotEmpty().WithMessage("Customer ID is required.");
    }
}

Validation failures

When validation fails, ValidationBehavior throws FluentValidation.ValidationException containing all aggregated failures. Handle this at the application boundary — for example with a global exception handler middleware:

app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
        if (exception is ValidationException validationException)
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
            await context.Response.WriteAsJsonAsync(new
            {
                title = "Validation failed",
                errors = validationException.Errors
                    .GroupBy(e => e.PropertyName)
                    .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage))
            });
        }
    });
});

The behavior is a no-op when no IValidator<TRequest> is registered for the incoming request type — it simply calls next and continues.


Source Generator

Clywell.Core.Cqrs bundles a Roslyn source generator that scans your compilation for concrete ICommandHandler<,> and IQueryHandler<,> implementations at build time and emits an AddCqrsHandlers() extension method. This replaces reflection-based assembly scanning and is compatible with NativeAOT and .NET trimming. No separate package install is needed.

Generated output example (abbreviated):

// <auto-generated/>
public static partial class CqrsHandlerRegistrationExtensions
{
    public static IServiceCollection AddCqrsHandlers(this IServiceCollection services)
    {
        services.AddTransient<IHandlerInvoker<CreateOrderCommand, OrderDto>,
                              CommandHandlerInvoker<CreateOrderCommand, OrderDto>>();
        services.AddTransient<ICommandHandler<CreateOrderCommand, OrderDto>,
                              CreateOrderHandler>();
        // ... one entry per discovered handler
        return services;
    }
}

No assembly scanning. No reflection at startup.


Full Registration Example

builder.Services
    .AddCqrs()                                                         // IDispatcher
    .AddCqrsHandlers()                                                 // source-generated handlers
    .AddCqrsFluentValidation(typeof(CreateOrderValidator).Assembly)    // validation behavior + validators
    .AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); // custom behavior

Because behaviors run in registration order, the setup above produces:

Request → LoggingBehavior → ValidationBehavior → Handler → Response

API Reference

Clywell.Core.Cqrs

Type Kind Description
ICommand<TResult> Interface Marker for commands (write operations)
IQuery<TResult> Interface Marker for queries (read-only operations)
ICommandHandler<TCommand, TResult> Interface Implement to handle a specific command
IQueryHandler<TQuery, TResult> Interface Implement to handle a specific query
IDispatcher Interface Dispatches commands (SendAsync) and queries (QueryAsync) through the pipeline
IPipelineBehavior<TRequest, TResult> Interface Middleware that wraps handler execution
RequestHandlerDelegate<TResult> Delegate Represents the next step in the pipeline; invoke inside a behavior to continue
AddCqrs() Extension Registers IDispatcher in the DI container

Clywell.Core.Cqrs.Generators (bundled)

The source generator is shipped inside Clywell.Core.Cqrs — no separate install required.

Type Kind Description
CqrsHandlerRegistrationGenerator Source generator Emits AddCqrsHandlers() at compile time; registers all handlers — zero runtime reflection

Clywell.Core.Cqrs.FluentValidation

Type Kind Description
ValidationBehavior<TRequest, TResult> Pipeline behavior Runs all IValidator<TRequest> instances; throws ValidationException on failure
AddCqrsFluentValidation(assemblies) Extension Registers ValidationBehavior<,> and scans assemblies for validators

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Commit changes: git commit -m 'feat: add my feature'
  4. Push to branch: git push origin feature/my-feature
  5. Create a Pull Request

License

This project is licensed under the MIT License — see LICENSE for details.

Changelog

See CHANGELOG.md for version history.

Product Compatible and additional computed target framework versions.
.NET 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Clywell.Core.Cqrs:

Package Downloads
Clywell.Core.Cqrs.FluentValidation

FluentValidation pipeline behavior for Clywell.Core.Cqrs. Provides automatic validation of commands and queries via IValidator<T> implementations registered in the DI container. Throws FluentValidation.ValidationException when validation fails.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.2 239 6/2/2026
1.1.1 274 3/10/2026
1.1.0 137 3/2/2026
1.0.1 132 2/27/2026
1.0.0 138 2/26/2026