This project provides a library for the command-query separation pattern.
Commands and queries will be separated on class-level and will be represented by the ICommand
and IQuery<TResult>
interfaces.
Define queries and commands in your application as classes/records implementing the IQuery<TResult>
or ICommand
interface. Both interfaces extend IRequest<TResult>
, but differentiating between queries and commands allows us to apply decorators for cross-cutting concerns to only queries or only commands (for example, you might want to surround all command executions with a transaction). Therefore you shouldn't directly implement IRequest<TResult>
:
public class GetThings : IQuery<IReadOnlyCollection<Thing>>;
public record SaveThing(Thing thing) : ICommand;
When you have defined your queries and commands, the next step is to define the according handlers by implementing the IRequestHandler<TRequest, TResult>
interface:
// Query Handler
public class GetThingsHandler : IRequestHandler<GetThings, IReadOnlyCollection<Thing>>
{
public async Task<IReadOnlyCollection<Thing>> HandleAsync(GetThings query, CancellationToken cancellationToken)
{
// return things
}
}
// Command Handler
public class SaveThingHandler : IRequestHandler<SaveThing, NoResult>
{
public async Task<NoResult> HandleAsync(SaveThing command, CancellationToken cancellationToken)
{
// save thing
return NoResult.CompletedTask;
}
}
A cross-cutting concern can be implemented with a decorator that also implements IRequestHandler<TRequest, TResult>
:
// This decorator should wrap all commands, but you can also implement
// more specific decorators by adding additional generic constraints.
public class CommandLoggingDecorator<TRequest, TResult> : IRequestHandler<TRequest, TResult>
where TRequest : ICommand<TResult>
{
private readonly IRequestHandler<TRequest, TResult> decoratee;
public CommandLoggingDecorator(IRequestHandler<TRequest, TResult> decoratee) =>
this.decoratee = decoratee ?? throw new ArgumentNullException(nameof(decoratee));
public async Task<TResult> HandleAsync(TRequest request, CancellationToken cancellationToken)
{
object type = typeof(TRequest).Name;
Debug.WriteLine("Executing command \"{0}\"", type);
var result = await this.decoratee.HandleAsync(request, cancellationToken);
Debug.WriteLine("Executed command \"{0}\"", type);
return result;
}
}
It is possible to define decorators for specific query or command types (IRequestHandler<SaveThing, NoResult>
) or generic ones that apply to all request types (IRequestHandler<TRequest, TResult>
) or a subset (IRequestHandler<TRequest, TResult> where TRequest : IMyMarkerInterface
). The example above applies to all commands, but not to queries.
The software CQS packages support two dependency injection frameworks:
- Dependency injection in .NET from the
Microsoft.Extensions.DependencyInjection
package (recommended). - Simple Injector.
When using the softaware.CQS.DependencyInjection
library some extension methods are provided to easily configure the CQS infrastructure with decorators for the IServiceCollection
.
var services = new ServiceCollection();
services
.AddSoftawareCqs(b => b.IncludeTypesFrom(Assembly.GetExecutingAssembly())) // this registers all request handlers
.AddDecorators(b => b
.AddRequestHandlerDecorator(typeof(CommandLoggingDecorator<,>)); // this registers the CommandLoggingDecorator for all command types
The following NuGet packages provide extension methods for the builder to easily add some predefined decorators:
softaware.Cqs.Decorators.Transaction.DependencyInjection
softaware.Cqs.Decorators.Validation.DependencyInjection
softaware.Cqs.Decorators.FluentValidation.DependencyInjection
softaware.Cqs.Decorators.UsageAware.DependencyInjection
var services = new ServiceCollection();
services
.AddSoftawareCqs(b => b.IncludeTypesFrom(Assembly.GetExecutingAssembly()))
.AddDecorators(b => b
.AddRequestHandlerDecorator(typeof(CommandLoggingDecorator<,>))
.AddTransactionCommandHandlerDecorator()
.AddUsageAwareDecorators()
.AddDataAnnotationsValidationDecorators()
.AddFluentValidationDecorators(
builder => builder.IncludeTypesFrom(Assembly.GetExecutingAssembly())));
The decorators wrap the handler in the order they are added here (so they are called in opposite order). In this case, the FluentValidationRequestHandlerDecorator
is the first decorator to be called. The CommandLoggingDecorator
is the last one and calls the actual handler.
When using the softaware.CQS.SimpleInjector
library, use AddRequestHandlerDecorator
method to register decorators in the Simple Injector container:
this.container = new Container();
this.container
.AddSoftawareCqs(b => b.IncludeTypesFrom(Assembly.GetExecutingAssembly()))
.AddDecorators(b => b
.AddRequestHandlerDecorator(typeof(TransactionAwareCommandHandlerDecorator<>)));
To use validation decorators from (1) softaware.CQS.Decorators.Validation
or (2) softaware.CQS.Decorators.FluentValidation
with SimpleInjector configure it like following:
// (1) Register validator
container.RegisterInstance<IValidator>(new DataAnnotationsValidator());
// (2) Register all fluent validators which are available in this project.
container.Collection.Register(typeof(FluentValidation.IValidator<>), Assembly.GetExecutingAssembly());
container
.AddSoftawareCqs(b => b.IncludeTypesFrom(Assembly.GetExecutingAssembly()))
.AddDecorators(b => b
// (1) Add Validation Decorator
.AddRequestHandlerDecorator(typeof(ValidationRequestHandlerDecorator<,>))
// (2) Add FluentValidation Decorator
.AddRequestHandlerDecorator(typeof(FluentValidationRequestHandlerDecorator<,>));
Commands and queries can be executed via injecting the according IRequestHandler<TRequest, TResult>
into the caller. You will receive the actual handler wrapped by any decorators that apply.
Executing requests this way requires some boilerplate code (specifying all the generic type arguments), so the library also provides a simpler way via the IRequestProcessor
interface. You can pass any request to it and it takes care of resolving the correct handler(s):
// using IServiceProvider
IRequestProcessor requestProcessor = serviceProvider.GetRequiredService<IRequestProcessor>();
// or using SimpleInjector
IRequestProcessor requestProcessor = this.container.GetInstance<IRequestProcessor>();
// Execute a command without a return type.
await requestProcessor.HandleAsync(new SaveThing(thing), cancellationToken);
// Execute a query with a return type.
var queryResult = await requestProcessor.HandleAsync(new GetThings(), cancellationToken);
The project consists of several separate packages, which allows flexible usage of various features.
Package | NuGet | Description |
---|---|---|
softaware.CQS |
Core library for command-query separation pattern. | |
softaware.CQS.Analyzers |
Roslyn analyzers that ensure correct usage of the library. (Shipped with core library.) | |
softaware.CQS.SimpleInjector |
Adds support for dynamic resolving of commands handlers and query handlers via SimpleInjector. | |
softaware.CQS.DependencyInjection |
Adds support for dynamic resolving of commands handlers and query handlers via Microsoft.Extensions.DependencyInjection . |
|
softaware.CQS.Decorators.Transaction |
A decorator for command-query architecture, which supports transactions. | |
softaware.CQS.Decorators.Transaction.DependencyInjection |
Builder extensions for adding decorators to Microsoft's DI. | |
softaware.CQS.Decorators.Validation |
A decorator for command-query architecture, which supports validation of data annotations. | |
softaware.CQS.Decorators.Validation.DependencyInjection |
Builder extensions for adding decorators to Microsoft's DI. | |
softaware.CQS.Decorators.FluentValidation |
A decorator for command-query architecture, which supports validation using FluentValidation. | |
softaware.CQS.Decorators.FluentValidation.DependencyInjection |
Builder extensions for adding decorators to Microsoft's DI. | |
softaware.CQS.Decorators.UsageAware |
A decorator for command-query architecture, which adds support for UsageAware. | |
softaware.CQS.Decorators.UsageAware.DependencyInjection |
Builder extensions for adding decorators to Microsoft's DI. | |
softaware.CQS.Decorators.ApplicationInsights |
A decorator for command-query architecture, which adds support for Azure Application Insights. | |
softaware.CQS.Decorators.ApplicationInsights.DependencyInjection |
Builder extensions for adding decorators to Microsoft's DI. |
Version 4.0 contains some breaking changes you need to be aware of before updating:
- There is now an interface
ICommand<TResult>
for commands that return values. While this should be used sparingly, there are some cases where it can be useful. ICommand
(which should be the default for implementing commands) derives from this newICommand<NoResult>
interface.- There is now a common base interface for
IQuery<TResult>
andICommand<TResult>
(and thusICommand
):IRequest<TResult
. This has the following advantages:- There is no need to distinguish between
IQueryHandler<TResult>
andICommandHandler
anymore. Simply useIRequestHandler<TResult>
. - For cross-cutting concerns, you can write decorators that target
IRequestHandler<TResult>
. Before you had to write one forIQueryHandler<TResult>
and one forICommandHandler
. - Instead of
IQueryProcessor
andICommandProcessor
, you can simply useIRequestProcessor
.ExecuteAsync
has been renamed toHandleAsync
.
- There is no need to distinguish between
- The cancellation token is now a required parameter for the
HandleAsync
method.
Show detailed upgrade instructions
-
Update all
softaware.CQS
packages to version 4.0.0 or higher -
Replace
IQueryHandler<TQuery, TResult>
withIRequestHandler<TQuery, TResult>
:- Replace in files (Use regular expression)
IQueryHandler<(.*?), (.*?)> IRequestHandler<$1, $2>
- Replace in files (Use regular expression)
-
Replace
ICommandHandler<TCommand>
withIRequestHandler<TCommand, NoResult>
- Replace
ICommandHandler<(.*?)> IRequestHandler<$1, NoResult>
- Replace
-
Replace query handler
HandleAsync
interface implementation: AddCancellationToken
- Replace
Task<(.+?)> HandleAsync\(([^,]+?) ([^,]+?)\) Task<$1> HandleAsync($2 $3, System.Threading.CancellationToken cancellationToken)
- Replace
-
Replace command handler
HandleAsync
: addNoResult
andCancellationToken
- Replace
Task HandleAsync\(([^,]+?) ([^,]+?)\) Task<NoResult> HandleAsync($1 $2, System.Threading.CancellationToken cancellationToken)
- Replace
-
Remove
HandleAsync
overloads delegating toCancellationToken
version- Replace with empty string (You might need to adjust the expressions based on your formatting):
Task<(.+?)> HandleAsync\(([^,]+?) ([^,]+?)\) =>
- Replace with empty string
public this.HandleAsync(query, default);
- Replace with empty string (You might need to adjust the expressions based on your formatting):
-
Replace
IQueryProcessor
andICommandProcessor
withIRequestProcessor
- Replace
IQueryProcessor
withIRequestProcessor
- Replace
ICommandProcessor
withIRequestProcessor
- Replace
queryProcessor
withrequestProcessor
- Replace
commandProcessor
withrequestProcessor
- Replace
requestProcessor.ExecuteAsync\(([^,]+?)\); requestProcessor.HandleAsync($1, cancellationToken);
- Remove duplicates where
IQueryProcessor
andICommandProcessor
were injected
- Replace
-
Add
return NoResult.Value
to command handlers -
Optional: Add
CancellationToken
to Controller actions-
Replace with file pattern: *Controller.cs
-
With parameters:
public async (.+)\((.+)\) public async $1($2, System.Threading.CancellationToken cancellationToken)
- Without parameters:
public async (.+)\(\) public async $1(System.Threading.CancellationToken cancellationToken)
-
-
Add missing
CancellationToken
parameters -
Decorators: Refactor command handler decorators to 2 generic type parameters
-
Replace
AddQueryHandlerDecorator
andAddCommandHandlerDecorator
withAddRequestHandlerDecorator
-
Remove
PublicQueryHandlerDecorator
andPublicCommandHandlerDecorator
if you referenced them explicitely. -
Optional: Combine duplicate decorators implementing
IQueryHandler<TResult>
andICommandHandler
into a single class implementingIRequestHandler
-
Optional: Use
ICommand<TResult>
instead ofICommand
if you used some workarounds for returning values from commands (like setting a property on the command)