SH.Framework.Library.Cqrs 1.4.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.4.0
                    
NuGet\Install-Package SH.Framework.Library.Cqrs -Version 1.4.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.4.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.4.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.4.0
                    
#r "nuget: SH.Framework.Library.Cqrs, 1.4.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.4.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.4.0
                    
Install as a Cake Addin
#tool nuget:?package=SH.Framework.Library.Cqrs&version=1.4.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.3.0 Release Notes

New Features

  • Enhanced Request/Response Tracking: Added IHasRequestId and IHasNotificationId 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> and IRequestHandler<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 response
  • SendAsync(IRequest request, CancellationToken ct = default): Sends a request that returns Unit
  • 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 and IHasNotificationId 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 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