ArbitratR 1.3.0

dotnet add package ArbitratR --version 1.3.0
                    
NuGet\Install-Package ArbitratR -Version 1.3.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="ArbitratR" Version="1.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ArbitratR" Version="1.3.0" />
                    
Directory.Packages.props
<PackageReference Include="ArbitratR" />
                    
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 ArbitratR --version 1.3.0
                    
#r "nuget: ArbitratR, 1.3.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 ArbitratR@1.3.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=ArbitratR&version=1.3.0
                    
Install as a Cake Addin
#tool nuget:?package=ArbitratR&version=1.3.0
                    
Install as a Cake Tool

ArbitratR

A comprehensive CQRS (Command Query Responsibility Segregation) library for .NET applications that enforces the Result pattern for robust error handling, provides domain event support, and offers seamless dependency injection integration.

Features

  • 🎯 Clean CQRS Implementation - Separate command and query handling with clear interfaces
  • ✅ Enforced Result Pattern - All operations return Result<T> for consistent error handling
  • 📡 Domain Events - Built-in support for raising and dispatching domain events
  • 🔧 Flexible Configuration - Multiple registration strategies for handlers
  • 📦 Dependency Injection Ready - Built-in support for Microsoft.Extensions.DependencyInjection
  • 🚀 High Performance - Lightweight with minimal overhead
  • 📚 Rich Error Types - Comprehensive error hierarchy for different failure scenarios

Installation

dotnet add package ArbitratR

Quick Start

1. Register ArbitratR Services

using ArbitratR.CQRS;

// In Program.cs or Startup.cs
services.AddArbitratR(config =>
{
    // Register handlers from current assembly
    config.AddHandlers();

    // Or register from specific assemblies
    config.AddHandlers(typeof(MyHandler).Assembly, typeof(AnotherHandler).Assembly);

    // Register domain event dispatcher and handlers
    config.AddDomainEventHandlers();
});

2. Define Commands and Handlers

using ArbitratR.CQRS;
using ArbitratR.Results;

// Command without return value
public record CreateUserCommand(string Name, string Email) : ICommand;

public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand>
{
    public async Task<Result> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken)
    {
        // Validation
        if (string.IsNullOrEmpty(command.Email))
            return new ValidationError(new Dictionary<string, string[]?> 
            { 
                ["Email"] = ["Email is required"] 
            });

        // Business logic
        try
        {
            // Create user logic here
            return Result.Success();
        }
        catch (Exception ex)
        {
            return new Problem("User-CreationFailed", ex.Message);
        }
    }
}

// Command with return value
public record GetUserByIdCommand(int Id) : ICommand<User>;

public class GetUserByIdCommandHandler : ICommandHandler<GetUserByIdCommand, User>
{
    public async Task<Result<User>> HandleAsync(GetUserByIdCommand command, CancellationToken cancellationToken)
    {
        var user = await FindUserAsync(command.Id);
        
        return user != null 
            ? Result<User>.Success(user)
            : new NotFound("User-NotFound", $"User with ID {command.Id} was not found");
    }
}

3. Define Queries and Handlers

// Query
public record GetUsersQuery(int PageSize, int PageNumber) : IQuery<IEnumerable<User>>;

public class GetUsersQueryHandler : IQueryHandler<GetUsersQuery, IEnumerable<User>>
{
    public async Task<Result<IEnumerable<User>>> HandleAsync(GetUsersQuery query, CancellationToken cancellationToken)
    {
        if (query.PageSize <= 0)
            return new ValidationError(new Dictionary<string, string[]?> 
            { 
                ["PageSize"] = ["Page size must be greater than 0"] 
            });

        var users = await GetPagedUsersAsync(query.PageSize, query.PageNumber);
        return Result<IEnumerable<User>>.Success(users);
    }
}

4. Use in Controllers

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly ICommandHandler<CreateUserCommand> _createUserHandler;
    private readonly IQueryHandler<GetUsersQuery, IEnumerable<User>> _getUsersHandler;

    public UsersController(
        ICommandHandler<CreateUserCommand> createUserHandler,
        IQueryHandler<GetUsersQuery, IEnumerable<User>> getUsersHandler)
    {
        _createUserHandler = createUserHandler;
        _getUsersHandler = getUsersHandler;
    }

    [HttpPost]
    public async Task<IActionResult> CreateUser(CreateUserCommand command)
    {
        var result = await _createUserHandler.HandleAsync(command, CancellationToken.None);
        
        return result.Match(
            success: () => Ok(),
            failure: error => error switch
            {
                ValidationError validation => BadRequest(validation.Errors),
                _ => StatusCode(500, error.Description)
            });
    }

    [HttpGet]
    public async Task<IActionResult> GetUsers([FromQuery] int pageSize = 10, [FromQuery] int pageNumber = 1)
    {
        var query = new GetUsersQuery(pageSize, pageNumber);
        var result = await _getUsersHandler.HandleAsync(query, CancellationToken.None);
        
        return result.Match(
            success: users => Ok(users),
            failure: error => BadRequest(error.Description));
    }
}

Domain Events

ArbitratR provides built-in support for raising and dispatching domain events, following Domain-Driven Design (DDD) patterns.

1. Define a Domain Event

Domain events implement the IDomainEvent marker interface:

using ArbitratR.Events;

public record UserCreatedEvent(int UserId, string Email) : IDomainEvent;

public record OrderPlacedEvent(int OrderId, decimal Total) : IDomainEvent;

2. Create a Domain Entity

Inherit from DomainEntity to gain domain event support. Use Raise() to queue events from within your domain logic:

using ArbitratR.Events;

public class User : DomainEntity
{
    public int Id { get; private set; }
    public string Email { get; private set; }

    public static User Create(string email)
    {
        var user = new User { Email = email };
        user.Raise(new UserCreatedEvent(user.Id, user.Email));
        return user;
    }
}

3. Implement Event Handlers

Create handlers for each domain event by implementing IDomainEventHandler<T>:

using ArbitratR.Events;

public class SendWelcomeEmailHandler : IDomainEventHandler<UserCreatedEvent>
{
    private readonly IEmailService _emailService;

    public SendWelcomeEmailHandler(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public async Task Handle(UserCreatedEvent domainEvent, CancellationToken cancellationToken)
    {
        await _emailService.SendWelcomeEmailAsync(domainEvent.Email, cancellationToken);
    }
}

// Multiple handlers can subscribe to the same event
public class AuditUserCreationHandler : IDomainEventHandler<UserCreatedEvent>
{
    private readonly IAuditLogger _auditLogger;

    public AuditUserCreationHandler(IAuditLogger auditLogger)
    {
        _auditLogger = auditLogger;
    }

    public async Task Handle(UserCreatedEvent domainEvent, CancellationToken cancellationToken)
    {
        await _auditLogger.LogAsync($"User {domainEvent.UserId} created", cancellationToken);
    }
}

4. Register Domain Event Handlers

services.AddArbitratR(config =>
{
    config.AddHandlers();

    // Register dispatcher and handlers from calling assembly
    config.AddDomainEventHandlers();

    // Or from specific assemblies
    config.AddDomainEventHandlers(typeof(SendWelcomeEmailHandler).Assembly);
});

5. Dispatch Domain Events

Inject IDomainEventsDispatcher and dispatch events after persisting your entities:

public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, User>
{
    private readonly IUserRepository _userRepository;
    private readonly IDomainEventsDispatcher _dispatcher;

    public CreateUserCommandHandler(IUserRepository userRepository, IDomainEventsDispatcher dispatcher)
    {
        _userRepository = userRepository;
        _dispatcher = dispatcher;
    }

    public async Task<Result<User>> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken)
    {
        var user = User.Create(command.Email);

        await _userRepository.AddAsync(user, cancellationToken);

        // Dispatch events after persistence
        await _dispatcher.DispatchAsync(user.DomainEvents, cancellationToken);
        user.ClearDomainEvents();

        return Result<User>.Success(user);
    }
}

Configuration Options

services.AddArbitratR(config =>
{
    // Register all handlers from calling assembly
    config.AddHandlers();

    // Register from specific assemblies
    config.AddHandlers(typeof(UserHandler).Assembly, typeof(OrderHandler).Assembly);

    // Register only commands or queries
    config.AddCommandHandlers(typeof(Commands).Assembly);
    config.AddQueryHandlers(typeof(Queries).Assembly);

    // Register domain event dispatcher and handlers
    config.AddDomainEventHandlers(typeof(EventHandlers).Assembly);

    // Customise the error code separator (default is '-')
    config.SetErrorCodeSeparator('.');
});

Result Pattern

ArbitratR enforces the Result pattern for all operations, providing consistent error handling across your application.

Result Types

// Basic result (success/failure)
Result result = Result.Success();
Result failure = Result.Failure(new Problem("User-OperationFailed", "Description"));

// Result with value
Result<User> userResult = Result<User>.Success(user);
Result<User> notFound = new NotFound("User-NotFound", "User not found");

Built-in Error Types

// Validation errors
var validationError = new ValidationError(new Dictionary<string, string[]?> 
{ 
    ["Email"] = ["Email is required", "Email format is invalid"],
    ["Age"] = ["Age must be greater than 0"]
});

// Not found errors
var notFound = new NotFound("User-NotFound", "The requested user was not found");

// Conflict errors
var conflict = new Conflict("User-EmailInUse", "A user with this email already exists");

// Authorization errors
var unauthorized = new Unauthorised("User-InvalidToken", "The provided token is invalid");
var forbidden = new Forbidden("User-InsufficientPermissions", "You don't have permission to perform this action");

// General problems
var problem = new Problem("User-ExternalServiceError", "The external service is currently unavailable");

Custom Error Classes

Create domain-specific error classes for better organization and reusability:

public static class UserErrors
{
    public static readonly NotFound NotFound = new("User-NotFound", "The user could not be found.");
    public static readonly Problem IncorrectPassword = new("User-IncorrectPassword", "The password supplied was incorrect.");
    public static readonly Forbidden NotAuthorised = new("User-NotAuthorised", "You do not have permission to perform this action.");
    public static readonly Unauthorised NotAuthenticated = new("User-NotAuthenticated", "User could not be authenticated.");
    
    // Dynamic errors with parameters
    public static Problem EmailInUse(string email) => new("User-EmailInUse", $"A user already exists for the email '{email}'");
}

// Usage in handlers
public async Task<Result<User>> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken)
{
    if (await EmailExistsAsync(command.Email))
        return UserErrors.EmailInUse(command.Email);
    
    // Create user logic...
    return Result<User>.Success(user);
}

Pattern Matching with Results

var result = await handler.HandleAsync(command, cancellationToken);

// Simple pattern matching
return result.Match(
    success: () => Ok(),
    failure: error => BadRequest(error.Description)
);

// Advanced error handling
return result.Match(
    success: () => Ok(),
    failure: error => error switch
    {
        ValidationError validation => BadRequest(validation.Errors),
        NotFound notFound => NotFound(notFound.Description),
        Unauthorised _ => Unauthorized(),
        Forbidden _ => Forbid(),
        _ => StatusCode(500, "An unexpected error occurred")
    }
);

// With value results
var userResult = await getUserHandler.HandleAsync(command, cancellationToken);
return userResult.Match(
    success: user => Ok(user),
    failure: error => HandleError(error)
);

Implicit Conversions

public async Task<Result<User>> GetUserAsync(int id)
{
    var user = await FindUserAsync(id);
    if (user == null)
        return UserErrors.NotFound; // Implicit conversion
    
    return user; // Implicit conversion to Result<User>.Success(user)
}

Best Practices

1. Command/Query Separation

  • Commands modify state and may or may not return data
  • Queries only read data and never modify state
  • Keep handlers focused on a single responsibility

2. Error Handling

  • Use specific error types (ValidationError, NotFound, etc.)
  • Provide meaningful error codes and descriptions
  • Handle errors consistently using pattern matching

3. Validation

  • Validate input in handlers, not in commands/queries
  • Return ValidationError for input validation failures
  • Use meaningful field names in validation errors

4. Handler Organization

  • Group related handlers in the same assembly/namespace
  • Use descriptive names for commands, queries, and handlers
  • Keep handlers lightweight and delegate complex logic to domain services

5. Dependency Injection

  • Register handlers using the provided configuration methods
  • Prefer constructor injection for handler dependencies
  • Use scoped lifetime for handlers (default behavior)

6. Domain Events

  • Raise domain events within your domain logic, not in handlers or controllers
  • Dispatch events after persistence to ensure consistency
  • Clear domain events after dispatching to prevent duplicate processing
  • Keep event handlers focused on a single side effect
  • Multiple handlers can subscribe to the same event for different concerns

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  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 is compatible.  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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.3.0 529 3/9/2026
1.2.0 117 3/9/2026
1.1.0 783 2/9/2026
1.0.9 219 1/28/2026
1.0.8 126 1/27/2026
1.0.7 130 1/22/2026
1.0.6 139 1/22/2026
1.0.5 131 1/14/2026
1.0.4 630 11/20/2025
1.0.3 384 10/31/2025
1.0.2 209 10/31/2025
1.0.1 215 10/31/2025
1.0.0 271 10/30/2025