CommandQuery.Framing 1.0.11

There is a newer version of this package available.
See the version list below for details.
dotnet add package CommandQuery.Framing --version 1.0.11
                    
NuGet\Install-Package CommandQuery.Framing -Version 1.0.11
                    
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="CommandQuery.Framing" Version="1.0.11" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CommandQuery.Framing" Version="1.0.11" />
                    
Directory.Packages.props
<PackageReference Include="CommandQuery.Framing" />
                    
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 CommandQuery.Framing --version 1.0.11
                    
#r "nuget: CommandQuery.Framing, 1.0.11"
                    
#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 CommandQuery.Framing@1.0.11
                    
#: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=CommandQuery.Framing&version=1.0.11
                    
Install as a Cake Addin
#tool nuget:?package=CommandQuery.Framing&version=1.0.11
                    
Install as a Cake Tool

CommandQuery.Framing

Current Version: 1.0.10
Target Framework: .NET 9.0

A lightweight, extensible CQRS (Command Query Responsibility Segregation) framework for .NET that simplifies command, query, and domain event handling with built-in pipeline support.

Features

โœจ Simple API - Single IBroker interface for executing commands and queries
๐Ÿ“ฆ Auto-Registration - Automatic handler discovery via assembly scanning
๐Ÿ”„ Domain Events - Built-in publisher/subscriber pattern with pipeline middleware
๐ŸŽฏ Type-Safe - Strongly-typed requests and responses
โšก Async First - Full async/await support with CancellationToken
๐Ÿ”Œ Pipeline Middleware - Add cross-cutting concerns (logging, validation, etc.) to domain events
๐Ÿ“ Well-Documented - Comprehensive XML documentation for IntelliSense
๐Ÿงช Tested - Includes test suite and working sample application

Installation

dotnet add package CommandQuery.Framing

Quick Start

Setup

Register CommandQuery services in your Startup.cs or Program.cs:

public void ConfigureServices(IServiceCollection services)
{
    // Automatically discovers and registers all handlers in the assembly
    services.AddCommandQuery(typeof(Startup).Assembly);
}

Using the Broker

Inject IBroker into your controllers or services to execute commands and queries:

[ApiController]
public class WidgetController : ControllerBase
{
    private readonly IBroker _broker;

    public WidgetController(IBroker broker)
    {
        _broker = broker;
    }

    [HttpPost("widget")]
    public async Task<IActionResult> CreateWidget(
        [FromBody] CreateWidgetMessage request, 
        CancellationToken cancellationToken)
    {
        var result = await _broker.HandleAsync<CreateWidgetMessage, CommandResponse<string>>(
            request, 
            cancellationToken);

        return result.Success 
            ? Ok(result.Data) 
            : BadRequest(result.Message);
    }

    [HttpGet("widget/{id}")]
    public async Task<IActionResult> GetWidget(
        string id, 
        CancellationToken cancellationToken)
    {
        var widget = await _broker.HandleAsync<GetWidget, Widget>(
            new GetWidget { Id = id }, 
            cancellationToken);

        return Ok(widget);
    }
}

Creating Handlers

Command Handler

Implement IAsyncHandler<TRequest, TResponse> for commands that modify state:

public class CreateWidgetHandler : IAsyncHandler<CreateWidgetMessage, CommandResponse<string>>
{
    private readonly IDomainEventPublisher _publisher;
    private readonly IWidgetRepository _repository;

    public CreateWidgetHandler(
        IDomainEventPublisher publisher,
        IWidgetRepository repository)
    {
        _publisher = publisher;
        _repository = repository;
    }
    
    public async Task<CommandResponse<string>> Execute(
        CreateWidgetMessage message, 
        CancellationToken cancellationToken = default)
    {
        // Validate input
        if (string.IsNullOrWhiteSpace(message?.Name))
            return Response.Failed<string>("Widget name is required");

        // Create widget
        var widgetId = Guid.NewGuid().ToString();
        await _repository.CreateAsync(widgetId, message.Name, cancellationToken);

        // Publish domain event
        await _publisher.Publish(
            new WidgetCreated { Id = widgetId, Name = message.Name }, 
            cancellationToken);

        return Response.Ok(widgetId);
    }
}
Query Handler

Implement IAsyncHandler<TRequest, TResponse> for queries that retrieve data:

public class GetWidgetQuery : IAsyncHandler<GetWidget, Widget>
{
    private readonly IWidgetRepository _repository;

    public GetWidgetQuery(IWidgetRepository repository)
    {
        _repository = repository;
    }

    public async Task<Widget> Execute(
        GetWidget message, 
        CancellationToken cancellationToken = default)
    {
        return await _repository.GetByIdAsync(message.Id, cancellationToken);
    }
}
Synchronous Handlers

For synchronous operations, implement IHandler<TRequest, TResponse>:

public class ValidateWidget : IHandler<Widget, bool>
{
    public bool Execute(Widget message)
    {
        return !string.IsNullOrEmpty(message.Name);
    }
}

Domain Events

Publish domain events to notify other parts of your application:

public class CreateWidgetHandler : IAsyncHandler<CreateWidgetMessage, CommandResponse<string>>
{
    private readonly IDomainEventPublisher _publisher;

    public CreateWidgetHandler(IDomainEventPublisher publisher)
    {
        _publisher = publisher;
    }

    public async Task<CommandResponse<string>> Execute(
        CreateWidgetMessage message,
        CancellationToken cancellationToken = default)
    {
        var widgetId = Guid.NewGuid().ToString();

        // Publish domain event
        await _publisher.Publish(
            new WidgetCreated { Id = widgetId, Name = message.Name },
            cancellationToken);

        return Response.Ok(widgetId);
    }
}

Domain Event Handlers

Implement IDomainEvent<TMessage> to handle domain events:

public class WidgetCreatedHandler : IDomainEvent<WidgetCreated>
{
    private readonly IEmailService _emailService;
    public event EventHandler<DomainEventArgs>? OnComplete;

    public WidgetCreatedHandler(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public async Task Execute(WidgetCreated message)
    {
        await _emailService.SendNotificationAsync($"Widget {message.Name} created");
        
        OnComplete?.Invoke(this, new DomainEventArgs 
        { 
            Success = true, 
            Message = "Notification sent" 
        });
    }
}

Pipeline Middleware (NEW in 1.0.10)

Add cross-cutting concerns to domain events using middleware pipelines powered by abes.GenericPipeline.

Configure Pipeline

public void ConfigureServices(IServiceCollection services)
{
    services.AddCommandQuery(typeof(Startup).Assembly);
    
    // Register middleware
    services
        .AddDomainEventMiddleware<LoggingMiddleware<WidgetCreated>>()
        .AddDomainEventMiddleware<ValidationMiddleware<WidgetCreated>>();
    
    // Configure pipeline for specific message type
    services.AddDomainEventPipeline<WidgetCreated>(builder =>
    {
        builder.Use<ValidationMiddleware<WidgetCreated>>();
        builder.Use<LoggingMiddleware<WidgetCreated>>();
    });
}

Create Middleware

public class LoggingMiddleware<TMessage> : IPipelineMiddleware<DomainEventContext<TMessage>>
{
    private readonly ILogger _logger;

    public LoggingMiddleware(ILogger<LoggingMiddleware<TMessage>> logger)
    {
        _logger = logger;
    }

    public async ValueTask InvokeAsync(
        DomainEventContext<TMessage> context, 
        PipelineDelegate<DomainEventContext<TMessage>> next)
    {
        _logger.LogInformation("Processing: {MessageType}", typeof(TMessage).Name);
        
        await next(context); // Execute next middleware or handler
        
        _logger.LogInformation("Completed: {MessageType}, Success: {Success}", 
            typeof(TMessage).Name, 
            context.Success);
    }
}

See PIPELINE_GUIDE.md for comprehensive pipeline documentation.

Response Helpers

Use the static Response class to create command responses:

// Success with data
return Response.Ok(widgetId);

// Success without data
return Response.Ok();

// Failure with single error
return Response.Failed<string>("Widget not found");

// Failure with multiple errors
return Response.Failed<string>(new List<string> { "Error 1", "Error 2" });

// Failure with exception
return Response.Failed<string>(exception);
return Response.Failed<string>("Custom message", exception);

Core Interfaces

IBroker

public interface IBroker
{
    Task<TResponse> HandleAsync<TRequest, TResponse>(
        TRequest message, 
        CancellationToken cancellationToken) where TRequest : IMessage;
    
    TResponse Handle<TRequest, TResponse>(
        TRequest message) where TRequest : IMessage;
}

IAsyncHandler

public interface IAsyncHandler<in TRequest, TResponse> where TRequest : IMessage
{
    Task<TResponse> Execute(TRequest message, CancellationToken cancellationToken);
}

IHandler

public interface IHandler<in TRequest, out TResponse> where TRequest : IMessage
{
    TResponse Execute(TRequest message);
}

IDomainEventPublisher

public interface IDomainEventPublisher
{
    event EventHandler MessageSent;
    event EventHandler<DomainEventArgs> MessageResult;
    
    Task Publish<TMessageType>(
        TMessageType message, 
        CancellationToken cancellationToken);
}

Best Practices

โœ… Commands - Modify state, return CommandResponse<T>
โœ… Queries - Read-only, return domain models
โœ… Validation - Validate in handlers or use pipeline middleware
โœ… Error Handling - Return Response.Failed() instead of throwing
โœ… CancellationToken - Always accept and pass cancellation tokens
โœ… Domain Events - Use for cross-cutting concerns and notifications
โœ… Pipelines - Add logging, validation, authorization as middleware

Sample Application

See the sample folder for a complete working example demonstrating:

  • Command and query handlers
  • Domain event publishing
  • Pipeline middleware
  • ASP.NET Core integration

Breaking Changes

Version 1.0.4+

  • Renamed ICommandBroker โ†’ IBroker
  • Unified command/query interfaces to IHandler/IAsyncHandler
  • Removed separate command/query base classes
  • Messages must implement IMessage marker interface

Version 1.0.10

  • Added pipeline middleware support via abes.GenericPipeline
  • Updated Microsoft.Extensions.DependencyInjection.Abstractions to 10.0.1
  • Enhanced error handling with detailed exception messages

License

MIT License - see LICENSE file for details.

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  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 was computed.  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.0.12 30 12/15/2025
1.0.11 39 12/14/2025
1.0.10 249 6/3/2025
1.0.9 243 4/10/2025
1.0.8 331 11/16/2023
1.0.7 811 9/12/2022
1.0.6 691 1/19/2022
1.0.5 659 8/5/2021
1.0.3 1,596 2/25/2021
1.0.2 1,402 7/8/2020
1.0.1 1,441 4/13/2020
1.0.0 2,900 11/21/2018