SH.Framework.Library.Cqrs
1.0.0
See the version list below for details.
dotnet add package SH.Framework.Library.Cqrs --version 1.0.0
NuGet\Install-Package SH.Framework.Library.Cqrs -Version 1.0.0
<PackageReference Include="SH.Framework.Library.Cqrs" Version="1.0.0" />
<PackageVersion Include="SH.Framework.Library.Cqrs" Version="1.0.0" />
<PackageReference Include="SH.Framework.Library.Cqrs" />
paket add SH.Framework.Library.Cqrs --version 1.0.0
#r "nuget: SH.Framework.Library.Cqrs, 1.0.0"
#:package SH.Framework.Library.Cqrs@1.0.0
#addin nuget:?package=SH.Framework.Library.Cqrs&version=1.0.0
#tool nuget:?package=SH.Framework.Library.Cqrs&version=1.0.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.
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
- 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
Installation
dotnet add package SH.Framework.Library.Cqrs
Quick Start
1. Register the Library
using SH.Framework.Library.Cqrs;
var builder = WebApplication.CreateBuilder(args);
// Register CQRS library with assemblies containing handlers
builder.Services.AddCqrsLibraryConfiguration(
Assembly.GetExecutingAssembly(),
typeof(MyHandler).Assembly
);
var app = builder.Build();
2. Create a Command/Query
// Command (no return value)
public record CreateUserCommand(string Name, string Email) : IRequest;
// Query (with return value)
public record GetUserQuery(int Id) : IRequest<UserDto>;
// Response DTO
public record UserDto(int Id, string Name, string Email);
3. Create Handlers
// Command Handler
public class CreateUserHandler : IRequestHandler<CreateUserCommand>
{
private readonly IUserRepository _repository;
public CreateUserHandler(IUserRepository repository)
{
_repository = repository;
}
public async Task<Unit> HandleAsync(CreateUserCommand request,
CancellationToken cancellationToken = default)
{
var user = new User(request.Name, request.Email);
await _repository.SaveAsync(user);
return Unit.Value;
}
}
// Query Handler
public class GetUserHandler : IRequestHandler<GetUserQuery, UserDto>
{
private readonly IUserRepository _repository;
public GetUserHandler(IUserRepository repository)
{
_repository = repository;
}
public async Task<UserDto> HandleAsync(GetUserQuery request,
CancellationToken cancellationToken = default)
{
var user = await _repository.GetByIdAsync(request.Id);
return new UserDto(user.Id, user.Name, user.Email);
}
}
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));
}
}
Notifications (Domain Events)
1. Create a Notification
public record UserCreatedNotification(int UserId, string Name, string Email) : INotification;
2. Create Notification Handlers
// Email notification handler
public class SendWelcomeEmailHandler : INotificationHandler<UserCreatedNotification>
{
private readonly IEmailService _emailService;
public SendWelcomeEmailHandler(IEmailService emailService)
{
_emailService = emailService;
}
public async Task HandleAsync(UserCreatedNotification notification,
CancellationToken cancellationToken = default)
{
await _emailService.SendWelcomeEmailAsync(notification.Email, notification.Name);
}
}
// Audit log handler
public class LogUserCreationHandler : INotificationHandler<UserCreatedNotification>
{
private readonly ILogger<LogUserCreationHandler> _logger;
public LogUserCreationHandler(ILogger<LogUserCreationHandler> logger)
{
_logger = logger;
}
public async Task HandleAsync(UserCreatedNotification notification,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("User created: {UserId} - {Name}",
notification.UserId, notification.Name);
}
}
3. Publish Notifications
public class CreateUserHandler : IRequestHandler<CreateUserCommand>
{
private readonly IUserRepository _repository;
private readonly IProjector _projector;
public CreateUserHandler(IUserRepository repository, IProjector projector)
{
_repository = repository;
_projector = projector;
}
public async Task<Unit> HandleAsync(CreateUserCommand request,
CancellationToken cancellationToken = default)
{
var user = new User(request.Name, request.Email);
await _repository.SaveAsync(user);
// Publish notification
var notification = new UserCreatedNotification(user.Id, user.Name, user.Email);
await _projector.PublishAsync(notification, cancellationToken);
return Unit.Value;
}
}
Pipeline Behaviors
Pipeline behaviors allow you to add cross-cutting concerns like validation, logging, caching, etc.
1. Create a Validation Behavior
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IValidator<TRequest>? _validator;
public ValidationBehavior(IValidator<TRequest>? validator = null)
{
_validator = validator;
}
public async Task<TResponse> HandleAsync(TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken = default)
{
if (_validator != null)
{
var validationResult = await _validator.ValidateAsync(request, cancellationToken);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
}
return await next(cancellationToken);
}
}
2. Create a Logging 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;
_logger.LogInformation("Handling {RequestName}", requestName);
var stopwatch = Stopwatch.StartNew();
var response = await next(cancellationToken);
stopwatch.Stop();
_logger.LogInformation("Handled {RequestName} in {ElapsedMs}ms",
requestName, stopwatch.ElapsedMilliseconds);
return response;
}
}
3. Register Behaviors
Note: You need to manually register pipeline behaviors in your DI container:
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
Notification Behaviors
Similar 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;
_logger.LogInformation("Publishing {NotificationName}", notificationName);
await next(cancellationToken);
_logger.LogInformation("Published {NotificationName}", notificationName);
}
}
Request and Notification IDs
The library provides interfaces for tracking requests and notifications:
public record CreateUserCommand(string Name, string Email) : IRequest, IHasRequestId
{
public Guid RequestId { get; init; } = Guid.NewGuid();
}
public record UserCreatedNotification(int UserId, string Name, string Email) : INotification, IHasNotificationId
{
public Guid NotificationId { get; init; } = Guid.NewGuid();
}
Error Handling
Notification handlers include built-in error handling - if one handler fails, others will still execute:
// In the Projector class, notification handlers are wrapped in try-catch
private async Task HandleNotificationCore<TNotification>(TNotification notification,
CancellationToken cancellationToken) where TNotification : INotification
{
// ... handlers execution with error handling
var tasks = handlers.Select(async handler =>
{
try
{
// Handler execution
}
catch (Exception ex)
{
Console.WriteLine($"Notification handler failed: {ex.Message}");
// Handler failure doesn't stop other handlers
}
});
}
Best Practices
- Keep Handlers Simple: Each handler should have a single responsibility
- Use Notifications for Side Effects: Commands should focus on the main operation, use notifications for side effects
- Validate Early: Use pipeline behaviors for validation before reaching handlers
- Log Appropriately: Use logging behaviors to track request execution
- Handle Errors Gracefully: Implement proper error handling in your handlers
- Use Cancellation Tokens: Always pass and respect cancellation tokens
Performance Considerations
- The library is designed for high performance with minimal allocations
- Behaviors are executed in reverse order of registration (LIFO)
- Notification handlers execute in parallel when possible
- Use appropriate DI lifetimes (typically Scoped for web applications)
Requirements
- .NET 9.0 or later
- Microsoft.Extensions.DependencyInjection 9.0.8 or later
Contributing
Contributions are welcome! Please feel free to submit issues and pull requests on GitHub.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Repository
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.