UniCQRS is a lightweight library designed to provide essential building blocks for implementing the Command Query Responsibility Segregation (CQRS) pattern in .NET applications. The library offers interfaces and concrete implementations for Commands, Queries, Handlers, and Pipeline Behaviors, along with exception handling, validation, caching, and logging utilities.
-
Commands and Queries:
ICommand
andIQuery<TResult>
interfaces for defining commands and queries.
-
Handlers:
ICommandHandler<TCommand>
andIQueryHandler<TQuery, TResult>
interfaces for implementing business logic.
-
Pipeline Behaviors:
- Interceptors that execute pre- and post- request handling. For example, logging, validation, and caching behaviors.
-
Validation:
- Integration with FluentValidation for request validation.
-
Caching:
- Support for caching query responses.
-
Exception Handling and Logging:
- Interceptors for logging exceptions that occur during request handling.
You can install the library via NuGet package manager:
Install-Package UniCQRS
Below is a basic guide on how to use the library features:
public class CreateUser : ICommand
{
public string Username { get; set; }
public string Password { get; set; }
}
public class CreateUserHandler : ICommandHandler<CreateUser>
{
public async Task HandleAsync(CreateUser command)
{
// Your business logic here
}
}
public class GetUserById : IQuery<User>
{
public int Id { get; set; }
}
public class GetUserByIdHandler : IQueryHandler<GetUserById, User>
{
public async Task<User> HandleAsync(GetUserById query)
{
// Your business logic here
}
}
public class UserController : ControllerBase
{
private readonly IMediator _mediator;
public UserController(IMediator mediator)
{
_mediator = mediator;
}
public async Task<IActionResult> CreateUser(CreateUser command)
{
await _mediator.SendAsync(command);
return Ok();
}
public async Task<IActionResult> GetUserById(int id)
{
var query = new GetUserById { Id = id };
var user = await _mediator.SendAsync(query);
return Ok(user);
}
}
You can add your custom behaviors to the pipeline. Below are examples of how to implement some of the built-in behaviors like Timing, Exception Logging, Validation, and Caching.
Logs the time taken to handle a request.
public class TimingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly ILogger<TimingBehavior<TRequest, TResponse>> _logger;
public TimingBehavior(ILogger<TimingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, Func<Task<TResponse>> next)
{
var sw = Stopwatch.StartNew();
var response = await next();
sw.Stop();
_logger.LogInformation($"Handling {typeof(TRequest).Name} took {sw.ElapsedMilliseconds} ms.");
return response;
}
}
Logs any exceptions that occur during request handling.
public class ExceptionLoggingBehavior<
8000
TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly ILogger<ExceptionLoggingBehavior<TRequest, TResponse>> _logger;
public ExceptionLoggingBehavior(ILogger<ExceptionLoggingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, Func<Task<TResponse>> next)
{
try
{
return await next();
}
catch (Exception ex)
{
_logger.LogError(ex, $"An exception occurred while handling {typeof(TRequest).Name}.");
throw;
}
}
}
Validates the request using FluentValidation.
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(TRequest request, Func<Task<TResponse>> next)
{
// Validation logic here
}
}
Caches the query response.
public class CachingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly ObjectCache _cache;
private readonly TimeSpan _cacheDuration;
public CachingBehavior(ObjectCache cache, TimeSpan cacheDuration)
{
_cache = cache;
_cacheDuration = cacheDuration;
}
public async Task<TResponse> Handle(TRequest request, Func<Task<TResponse>> next)
{
// Caching logic here
}
}
To use these behaviors, you will need to register them with your dependency injection container.
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(TimingBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ExceptionLoggingBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>));
Now, these behaviors will be applied in the order they are registered, whenever you use the IMediator
to send a command or query.
For complete customization, you can implement your own behaviors that implement IPipelineBehavior<TRequest, TResponse>
and register them the same way.