SH.Framework.Library.Cqrs 1.2.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package SH.Framework.Library.Cqrs --version 1.2.0
                    
NuGet\Install-Package SH.Framework.Library.Cqrs -Version 1.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SH.Framework.Library.Cqrs" Version="1.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SH.Framework.Library.Cqrs" Version="1.2.0" />
                    
Directory.Packages.props
<PackageReference Include="SH.Framework.Library.Cqrs" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SH.Framework.Library.Cqrs --version 1.2.0
                    
#r "nuget: SH.Framework.Library.Cqrs, 1.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SH.Framework.Library.Cqrs@1.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SH.Framework.Library.Cqrs&version=1.2.0
                    
Install as a Cake Addin
#tool nuget:?package=SH.Framework.Library.Cqrs&version=1.2.0
                    
Install as a Cake Tool

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.1.0 Highlights

  • Enhanced Error Handling: Custom exceptions for better debugging and error management
  • Performance Optimizations: Reflection caching for improved throughput
  • Robust Validation: Null argument validation and improved exception messages
  • Better Logging: Enhanced ILogger integration with dependency injection
  • Improved Equality: Better Unit struct with proper equality semantics

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 with reflection caching
  • Clean Architecture: Promotes separation of concerns and maintainable code
  • Robust Error Handling: Custom exceptions for better error management
  • Comprehensive Logging: Built-in logging support with configurable error handling

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)
            {
                var errors = validationResult.Errors
                    .GroupBy(x => x.PropertyName)
                    .ToDictionary(g => g.Key, g => g.Select(x => x.ErrorMessage).ToArray());
                    
                throw new CqrsValidationException(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

Built-in Exceptions

The library includes custom exceptions for better error handling:

  • CqrsValidationException: Thrown when validation fails, contains detailed error information
  • HandlerNotFoundException: Thrown when no handler is found for a request type
  • MultipleHandlersFoundException: Thrown when multiple handlers are found for a single request

Exception Usage Examples

try
{
    var result = await _projector.SendAsync(new GetUserQuery(invalidId));
}
catch (HandlerNotFoundException ex)
{
    _logger.LogError("No handler found for request type: {RequestType}", ex.RequestType.Name);
    return NotFound();
}
catch (CqrsValidationException ex)
{
    _logger.LogWarning("Validation failed with {ErrorCount} errors", ex.Errors.Count);
    return BadRequest(ex.Errors);
}

Notification Error Handling

Notification handlers include built-in error handling with logging support. If one handler fails, others will still execute:

// The Projector automatically handles notification handler failures
// Errors are logged but don't prevent other handlers from executing
await _projector.PublishAsync(notification); // All handlers will attempt to run

Performance Optimizations

Version 1.1.0 includes significant performance improvements:

  • Reflection Caching: Handler and behavior method lookups are cached using ConcurrentDictionary
  • Type Caching: Generic type construction is cached to avoid repeated reflection operations
  • Optimized Handler Resolution: Faster service resolution with cached type information

Best Practices

  1. Keep Handlers Simple: Each handler should have a single responsibility
  2. Use Notifications for Side Effects: Commands should focus on the main operation, use notifications for side effects
  3. Validate Early: Use pipeline behaviors for validation before reaching handlers
  4. Log Appropriately: Use logging behaviors to track request execution
  5. Handle Errors Gracefully: Implement proper error handling in your handlers using the provided exception types
  6. Use Cancellation Tokens: Always pass and respect cancellation tokens
  7. Leverage Caching: The library's built-in caching optimizes performance automatically

Performance Considerations

  • The library is designed for high performance with minimal allocations and reflection caching
  • Behaviors are executed in reverse order of registration (LIFO)
  • Notification handlers execute in parallel when possible with built-in error isolation
  • Use appropriate DI lifetimes (typically Scoped for web applications)
  • Handler and behavior method resolution is cached for optimal performance

Logging Integration

The library integrates seamlessly with the Microsoft.Extensions.Logging framework:

// Register logging in your DI container
builder.Services.AddLogging();

// The Projector will automatically use ILogger<Projector> if available
// Notification handler failures are automatically logged with structured information

Requirements

  • .NET 9.0 or later
  • Microsoft.Extensions.DependencyInjection 9.0.8 or later
  • Microsoft.Extensions.Logging.Abstractions 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

https://github.com/muharremkackin/sh-framework-cqrs

Changelog

v1.1.0

  • Fixed behavior registration in AddCqrsLibraryConfiguration
  • Improved error handling with custom exceptions (CqrsValidationException, HandlerNotFoundException, MultipleHandlersFoundException)
  • Added reflection caching for better performance
  • Enhanced Unit struct with proper equality semantics
  • Better logging integration with ILogger dependency injection
  • Added null argument validation
  • Improved exception messages and error handling
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
1.4.1 134 8/21/2025
1.4.0 121 8/21/2025
1.2.0 124 8/20/2025
1.1.0 127 8/20/2025
1.0.0 132 8/18/2025

v1.2.0:
           - Enhanced reflection caching with ConcurrentDictionary for better performance
           - Improved error handling with comprehensive custom exceptions
           - Added null argument validation throughout the library
           - Better logging integration with structured logging support
           - Enhanced Unit struct with proper equality semantics and operators
           - Optimized behavior execution pipeline with reverse order processing
           - Added comprehensive XML documentation
           - Improved exception messages with detailed context information
           - Enhanced parallel notification handler execution with error isolation
           - Added support for source link and symbol packages
           - Performance improvements in handler resolution and caching
           - Better integration with Microsoft.Extensions.Logging.Abstractions
           - Comprehensive testing and validation improvements