Clywell.Core.Cqrs.FluentValidation 1.1.2

dotnet add package Clywell.Core.Cqrs.FluentValidation --version 1.1.2
                    
NuGet\Install-Package Clywell.Core.Cqrs.FluentValidation -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.FluentValidation" 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.FluentValidation" Version="1.1.2" />
                    
Directory.Packages.props
<PackageReference Include="Clywell.Core.Cqrs.FluentValidation" />
                    
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.FluentValidation --version 1.1.2
                    
#r "nuget: Clywell.Core.Cqrs.FluentValidation, 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.FluentValidation@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.FluentValidation&version=1.1.2
                    
Install as a Cake Addin
#tool nuget:?package=Clywell.Core.Cqrs.FluentValidation&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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.2 243 6/2/2026
1.1.1 270 3/10/2026
1.1.0 137 3/2/2026
1.0.1 134 2/27/2026
1.0.0 133 2/26/2026