CommandQuery.Framing
1.0.11
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
<PackageReference Include="CommandQuery.Framing" Version="1.0.11" />
<PackageVersion Include="CommandQuery.Framing" Version="1.0.11" />
<PackageReference Include="CommandQuery.Framing" />
paket add CommandQuery.Framing --version 1.0.11
#r "nuget: CommandQuery.Framing, 1.0.11"
#:package CommandQuery.Framing@1.0.11
#addin nuget:?package=CommandQuery.Framing&version=1.0.11
#tool nuget:?package=CommandQuery.Framing&version=1.0.11
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
IMessagemarker interface
Version 1.0.10
- Added pipeline middleware support via
abes.GenericPipeline - Updated
Microsoft.Extensions.DependencyInjection.Abstractionsto 10.0.1 - Enhanced error handling with detailed exception messages
License
MIT License - see LICENSE file for details.
Links
| Product | Versions 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. |
-
net9.0
- abes.GenericPipeline (>= 1.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Logging (>= 9.0.5)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.5)
- Microsoft.Extensions.Logging.Console (>= 9.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.