SH.Framework.Library.Cqrs
1.4.0
See the version list below for details.
dotnet add package SH.Framework.Library.Cqrs --version 1.4.0
NuGet\Install-Package SH.Framework.Library.Cqrs -Version 1.4.0
<PackageReference Include="SH.Framework.Library.Cqrs" Version="1.4.0" />
<PackageVersion Include="SH.Framework.Library.Cqrs" Version="1.4.0" />
<PackageReference Include="SH.Framework.Library.Cqrs" />
paket add SH.Framework.Library.Cqrs --version 1.4.0
#r "nuget: SH.Framework.Library.Cqrs, 1.4.0"
#:package SH.Framework.Library.Cqrs@1.4.0
#addin nuget:?package=SH.Framework.Library.Cqrs&version=1.4.0
#tool nuget:?package=SH.Framework.Library.Cqrs&version=1.4.0
SH.Framework.Library.Cqrs
A lightweight and high-performance library implementing the Command Query Responsibility Segregation (CQRS) pattern for .NET 9.0. Provides clean architecture and separated responsibilities in modern .NET applications with support for Request/Response, Notifications, and Pipeline Behaviors.
Version 1.3.0 Release Notes
New Features
- Enhanced Request/Response Tracking: Added
IHasRequestId
andIHasNotificationId
interfaces for improved traceability - Robust Error Handling: Comprehensive exception types with detailed context information
- Improved Pipeline Execution: Optimized behavior chain execution with better performance
- Enhanced Cancellation Support: Full cancellation token propagation throughout the pipeline
Improvements
- Better error messages with contextual information in exceptions
- Optimized reflection-based handler resolution
- Enhanced notification handler failure isolation
- Improved assembly scanning performance
Features
- Request/Response Pattern: Handle commands and queries with typed responses
- Notification System: Publish and handle domain events asynchronously
- Pipeline Behaviors: Add cross-cutting concerns like validation, logging, and caching
- Request/Notification Tracking: Built-in support for request and notification IDs
- Comprehensive Error Handling: Detailed exception types for debugging and monitoring
- Dependency Injection Integration: Seamless integration with Microsoft.Extensions.DependencyInjection
- High Performance: Optimized for minimal overhead and maximum throughput
- Clean Architecture: Promotes separation of concerns and maintainable code
- Auto-Discovery: Automatic registration of handlers via assembly scanning
- Cancellation Support: Full cooperative cancellation throughout the pipeline
Installation
dotnet add package SH.Framework.Library.Cqrs
Quick Start
1. Register Services
// Program.cs or Startup.cs
builder.Services.AddCqrsLibraryConfiguration(
Assembly.GetExecutingAssembly(),
typeof(SomeHandlerInAnotherAssembly).Assembly
);
2. Define Requests and Handlers
// Command (no response)
public record CreateUserCommand(string Name, string Email) : IRequest;
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand>
{
public async Task<Unit> HandleAsync(CreateUserCommand request, CancellationToken cancellationToken)
{
// Create user logic
return Unit.Value;
}
}
// Query (with response)
public record GetUserQuery(int Id) : IRequest<UserDto>;
public class GetUserQueryHandler : IRequestHandler<GetUserQuery, UserDto>
{
public async Task<UserDto> HandleAsync(GetUserQuery request, CancellationToken cancellationToken)
{
// Get user logic
return new UserDto(request.Id, "John Doe", "john@example.com");
}
}
public record UserDto(int Id, string Name, string Email);
3. Define Notifications and Handlers
// Notification
public record UserCreatedNotification(int UserId, string Name, string Email) : INotification;
public class UserCreatedNotificationHandler : INotificationHandler<UserCreatedNotification>
{
public async Task HandleAsync(UserCreatedNotification notification, CancellationToken cancellationToken)
{
// Handle user created event (e.g., send welcome email)
Console.WriteLine($"User {notification.Name} created with ID {notification.UserId}");
}
}
4. Use the Projector
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IProjector _projector;
public UsersController(IProjector projector)
{
_projector = projector;
}
[HttpPost]
public async Task<IActionResult> CreateUser(CreateUserCommand command)
{
await _projector.SendAsync(command);
return Ok();
}
[HttpGet("{id}")]
public async Task<UserDto> GetUser(int id)
{
return await _projector.SendAsync(new GetUserQuery(id));
}
}
Dependency Injection and Auto-Discovery
Call AddCqrsLibraryConfiguration(params Assembly[])
once during startup to:
- Register
IProjector
implementation - Scan provided assemblies and automatically register:
IRequestHandler<TRequest, TResponse>
andIRequestHandler<TRequest>
INotificationHandler<TNotification>
builder.Services.AddCqrsLibraryConfiguration(
Assembly.GetExecutingAssembly(),
typeof(SomeHandlerInAnotherAssembly).Assembly
);
Note: Pipeline behaviors must be registered explicitly (see Pipeline Behaviors section).
Projector API
The IProjector
interface provides three main methods:
SendAsync<TResponse>(IRequest<TResponse> request, CancellationToken ct = default)
: Sends a request expecting a typed responseSendAsync(IRequest request, CancellationToken ct = default)
: Sends a request that returnsUnit
PublishAsync<TNotification>(TNotification notification, CancellationToken ct = default)
: Publishes a notification to all matching handlers
Request and Notification Tracking
Request Tracking
Implement IHasRequestId
on your requests to enable automatic request tracking:
public record CreateUserCommand(string Name, string Email, Guid RequestId) : IRequest, IHasRequestId;
public record GetUserQuery(int Id, Guid RequestId) : IRequest<UserDto>, IHasRequestId;
Notification Tracking
Implement IHasNotificationId
on your notifications for event tracing:
public record UserCreatedNotification(int UserId, string Name, string Email, Guid NotificationId)
: INotification, IHasNotificationId;
Pipeline Behaviors
Pipeline behaviors allow you to add cross-cutting concerns like validation, logging, caching, and performance monitoring.
Creating a Behavior
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> HandleAsync(TRequest request, RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken = default)
{
var requestName = typeof(TRequest).Name;
var requestId = request is IHasRequestId tracked ? tracked.RequestId.ToString() : "N/A";
_logger.LogInformation("Handling request {RequestName} with ID {RequestId}", requestName, requestId);
var stopwatch = Stopwatch.StartNew();
try
{
var response = await next(cancellationToken);
stopwatch.Stop();
_logger.LogInformation("Request {RequestName} completed in {ElapsedMs}ms", requestName, stopwatch.ElapsedMilliseconds);
return response;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "Request {RequestName} failed after {ElapsedMs}ms", requestName, stopwatch.ElapsedMilliseconds);
throw;
}
}
}
Validation Behavior
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> HandleAsync(TRequest request, RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken = default)
{
if (!_validators.Any()) return await next(cancellationToken);
var validationResults = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(request, cancellationToken)));
var errors = validationResults
.Where(r => !r.IsValid)
.SelectMany(r => r.Errors)
.GroupBy(e => e.PropertyName)
.ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
if (errors.Any())
throw new CqrsValidationException(errors);
return await next(cancellationToken);
}
}
Registering Behaviors
Behaviors must be registered manually and are executed in reverse registration order (LIFO):
// Register behaviors - they execute in reverse order (LIFO)
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); // Executes last (outer)
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); // Executes first (inner)
Notification Behaviors
Notification behaviors work similarly to pipeline behaviors but for notifications:
public class NotificationLoggingBehavior<TNotification> : INotificationBehavior<TNotification>
where TNotification : INotification
{
private readonly ILogger<NotificationLoggingBehavior<TNotification>> _logger;
public NotificationLoggingBehavior(ILogger<NotificationLoggingBehavior<TNotification>> logger)
{
_logger = logger;
}
public async Task HandleAsync(TNotification notification, NotificationHandlerDelegate next,
CancellationToken cancellationToken = default)
{
var notificationName = typeof(TNotification).Name;
var notificationId = notification is IHasNotificationId tracked ? tracked.NotificationId.ToString() : "N/A";
_logger.LogInformation("Publishing notification {NotificationName} with ID {NotificationId}",
notificationName, notificationId);
await next(cancellationToken);
_logger.LogInformation("Notification {NotificationName} published successfully", notificationName);
}
}
Registering Notification Behaviors
builder.Services.AddScoped(typeof(INotificationBehavior<>), typeof(NotificationLoggingBehavior<>));
Notifications (Domain Events)
Notifications enable decoupled communication through domain events:
1. Define Notifications
public record UserCreatedNotification(int UserId, string Name, string Email) : INotification;
public record UserUpdatedNotification(int UserId, string Name, string Email) : INotification;
public record UserDeletedNotification(int UserId) : INotification;
2. Create Handlers
Multiple handlers can subscribe to the same notification:
// Email service handler
public class SendWelcomeEmailHandler : INotificationHandler<UserCreatedNotification>
{
public async Task HandleAsync(UserCreatedNotification notification, CancellationToken cancellationToken)
{
// Send welcome email logic
await SendWelcomeEmail(notification.Email, notification.Name);
}
}
// Analytics handler
public class UserAnalyticsHandler : INotificationHandler<UserCreatedNotification>
{
public async Task HandleAsync(UserCreatedNotification notification, CancellationToken cancellationToken)
{
// Track user creation in analytics
await TrackUserCreation(notification.UserId);
}
}
3. Publish Notifications
// Publish from your handlers or services
await _projector.PublishAsync(new UserCreatedNotification(user.Id, user.Name, user.Email), cancellationToken);
Cancellation Support
The library provides comprehensive cancellation support:
- All entry points check for cancellation early and propagate tokens
- Cancellation is supported throughout the pipeline (behaviors and handlers)
- If a token is already canceled,
OperationCanceledException
is thrown immediately
// Always pass cancellation tokens from your controllers
[HttpPost]
public async Task<IActionResult> CreateUser(CreateUserCommand command, CancellationToken cancellationToken)
{
await _projector.SendAsync(command, cancellationToken);
return Ok();
}
Error Handling
Exception Types
The library provides specific exception types for different error scenarios:
CqrsValidationException
Thrown when validation fails, containing structured validation errors:
try
{
await _projector.SendAsync(command);
}
catch (CqrsValidationException ex)
{
// ex.Errors contains Dictionary<string, string[]> of validation errors
foreach (var (property, errors) in ex.Errors)
{
Console.WriteLine($"{property}: {string.Join(", ", errors)}");
}
}
HandlerNotFoundException
Thrown when no handler is registered for a request type:
try
{
await _projector.SendAsync(new UnhandledCommand());
}
catch (HandlerNotFoundException ex)
{
Console.WriteLine($"No handler found for: {ex.RequestType.Name}");
}
MultipleHandlersFoundException
Thrown when multiple handlers are registered for the same request type:
try
{
await _projector.SendAsync(command);
}
catch (MultipleHandlersFoundException ex)
{
Console.WriteLine($"Found {ex.HandlerCount} handlers for: {ex.RequestType.Name}");
}
Notification Error Handling
Notification handlers execute independently. If one fails, others continue to run:
public class RobustNotificationHandler : INotificationHandler<UserCreatedNotification>
{
public async Task HandleAsync(UserCreatedNotification notification, CancellationToken cancellationToken)
{
try
{
// Handler logic that might fail
await SomeOperationThatMightFail();
}
catch (Exception ex)
{
// Exceptions are caught by the framework and logged
// Other handlers will continue to execute
throw; // Re-throw if you want it logged
}
}
}
Performance Considerations
- Minimal Allocations: Designed for high performance with minimal memory allocations
- Optimized Reflection: Efficient handler resolution and method invocation
- LIFO Execution: Behaviors execute in reverse registration order (Last In, First Out)
- Parallel Notifications: Notification handlers execute concurrently when possible
- Proper DI Lifetimes: Use appropriate lifetimes (typically Scoped for web applications)
Performance Tips
// Use appropriate service lifetimes
builder.Services.AddScoped<IProjector, Projector>(); // Already done by AddCqrsLibraryConfiguration
builder.Services.AddScoped<IMyService, MyService>();
// Consider using ValueTask for handlers that might complete synchronously
public class FastHandler : IRequestHandler<FastQuery, string>
{
public async Task<string> HandleAsync(FastQuery request, CancellationToken cancellationToken)
{
// For operations that might complete synchronously, consider ValueTask
return await ValueTask.FromResult("Fast response");
}
}
Best Practices
1. Use Records for Requests and Notifications
// Preferred: Immutable records
public record CreateUserCommand(string Name, string Email) : IRequest;
// Less preferred: Mutable classes
public class CreateUserCommand : IRequest
{
public string Name { get; set; }
public string Email { get; set; }
}
2. Keep Handlers Focused
// Good: Single responsibility
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand>
{
public async Task<Unit> HandleAsync(CreateUserCommand request, CancellationToken cancellationToken)
{
// Only handle user creation
return Unit.Value;
}
}
3. Use Behaviors for Cross-Cutting Concerns
// Use behaviors for logging, validation, caching, etc.
// Don't duplicate this logic in every handler
public class CachingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
// Implementation
}
4. Implement Proper Cancellation
public class LongRunningHandler : IRequestHandler<LongRunningCommand>
{
public async Task<Unit> HandleAsync(LongRunningCommand request, CancellationToken cancellationToken)
{
// Check cancellation at appropriate intervals
for (int i = 0; i < 1000; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await DoWork();
}
return Unit.Value;
}
}
Migration Guide
If you're upgrading from a previous version, here are the key changes:
From 1.0.x to 1.3.0
- No breaking changes
- Consider implementing
IHasRequestId
andIHasNotificationId
for better traceability - Review exception handling to take advantage of new detailed exception types
- Update any custom behaviors to utilize the enhanced cancellation support
Contributing
We welcome contributions! Please feel free to submit issues, feature requests, or pull requests on our GitHub repository.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
For questions, issues, or support, please visit our GitHub repository or create an issue.
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
- Microsoft.Extensions.DependencyInjection (>= 9.0.8)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on SH.Framework.Library.Cqrs:
Package | Downloads |
---|---|
SH.Framework.Library.Cqrs.Implementation
A comprehensive implementation layer for the SH.Framework.Library.Cqrs package, providing abstract base classes and Result pattern implementation for CQRS operations. This package extends the core CQRS framework with practical base classes for requests, handlers, behaviors, and a standardized Result pattern for better error handling and response management. |
GitHub repositories
This package is not used by any popular GitHub repositories.