ArbitratR 1.3.0
dotnet add package ArbitratR --version 1.3.0
NuGet\Install-Package ArbitratR -Version 1.3.0
<PackageReference Include="ArbitratR" Version="1.3.0" />
<PackageVersion Include="ArbitratR" Version="1.3.0" />
<PackageReference Include="ArbitratR" />
paket add ArbitratR --version 1.3.0
#r "nuget: ArbitratR, 1.3.0"
#:package ArbitratR@1.3.0
#addin nuget:?package=ArbitratR&version=1.3.0
#tool nuget:?package=ArbitratR&version=1.3.0
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
ValidationErrorfor 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 | Versions 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. |
-
net10.0
-
net8.0
-
net9.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.