ResultR is a lightweight request/response dispatcher for .NET applications. It routes requests to handlers and wraps all responses in a Result<T> type for consistent success/failure handling.
What it does:
- Decouples your application logic by routing requests to dedicated handler classes
- Provides a predictable pipeline: Validate β BeforeHandle β Handle β AfterHandle
- Catches exceptions automatically and returns them as failure results
- Eliminates the need for try/catch blocks scattered throughout your codebase
What it doesn't do:
- No notifications or pub/sub messaging
- No pipeline behaviors or middleware chains
- No stream handling
- No distributed messaging
This focused scope keeps the library small, fast, and easy to understand.
- π Simple Interface Pattern: Uses
IRequest/IRequest<TResponse>andIRequestHandler<TRequest>/IRequestHandler<TRequest, TResponse>- no distinction between commands and queries - π¦ Unified Result Type: All operations return
ResultorResult<T>, supporting success/failure states, exception capture, and optional metadata - πͺ Optional Inline Hooks: Handlers can override
ValidateAsync(),BeforeHandleAsync(), andAfterHandleAsync()methods without requiring base classes or separate interfaces - β‘ Minimal Configuration: Simple DI integration with minimal setup
- π Strong Typing: Full type safety throughout the pipeline
Each request flows through a simple, predictable pipeline:
- β
Validation - Calls
ValidateAsync()if overridden, short-circuits on failure - π Before Handle - Invokes
BeforeHandleAsync()for optional logging or setup - βοΈ Handle - Executes the core
HandleAsync()logic - π After Handle - Invokes
AfterHandleAsync()for logging or cleanup - π‘οΈ Exception Handling - Any exceptions are caught and returned as
Result.Failurewith the exception attached
π Read the full documentation on the Wiki β
ResultR prioritizes:
- Simplicity over flexibility: Opinionated design choices reduce boilerplate
- Clean architecture: No magic strings, reflection-heavy operations, or hidden behaviors
- Explicit over implicit: Clear pipeline execution with predictable behavior
- Modern C# practices: Leverages latest language features and patterns
- .NET 10.0 or later
- C# 14.0 or later
dotnet add package ResultRFor inline validation with a fluent API:
dotnet add package ResultR.Validationβ Learn more about ResultR.Validation
public record CreateUserRequest(string Email, string Name, int Age) : IRequest<User>;public class CreateUserHandler : IRequestHandler<CreateUserRequest, User>
{
private readonly IUserRepository _repository;
private readonly ILogger<CreateUserHandler> _logger;
public CreateUserHandler(IUserRepository repository, ILogger<CreateUserHandler> logger)
{
_repository = repository;
_logger = logger;
}
// Optional: Validate the request (override virtual method)
public ValueTask<Result> ValidateAsync(CreateUserRequest request)
{
if (string.IsNullOrWhiteSpace(request.Email))
return new(Result.Failure("Email is required"));
if (!request.Email.Contains("@"))
return new(Result.Failure("Invalid email format"));
return new(Result.Success());
}
// Optional: Before handle hook (override virtual method)
public ValueTask BeforeHandleAsync(CreateUserRequest request)
{
_logger.LogInformation("Creating user with email: {Email}", request.Email);
return default;
}
// Required: Core handler logic
public async ValueTask<Result<User>> HandleAsync(CreateUserRequest request, CancellationToken cancellationToken)
{
// Exceptions are automatically caught and converted to Result.Failure
var user = new User(request.Email, request.Name, request.Age);
await _repository.AddAsync(user, cancellationToken);
return Result<User>.Success(user);
}
// Optional: After handle hook (override virtual method)
public ValueTask AfterHandleAsync(CreateUserRequest request, Result<User> result)
{
if (result.IsSuccess)
_logger.LogInformation("User created successfully: {UserId}", result.Value.Id);
else
_logger.LogError("User creation failed: {Error}", result.Error);
return default;
}
}// Simple: auto-scans entry assembly
services.AddResultR();
// Or explicit: scan specific assemblies (for multi-project solutions)
services.AddResultR(
typeof(Program).Assembly,
typeof(MyHandlers).Assembly);public class UserController : ControllerBase
{
private readonly IDispatcher _dispatcher;
public UserController(IDispatcher dispatcher)
{
_dispatcher = dispatcher;
}
[HttpPost]
public async Task<IActionResult> CreateUser(CreateUserRequest request)
{
var result = await _dispatcher.Dispatch(request);
return result.IsSuccess
? Ok(result.Value)
: BadRequest(result.Error);
}
}The Result<T> type provides a clean way to handle success and failure states:
// Success
var success = Result<User>.Success(user);
// Failure with message
var failure = Result<User>.Failure("User not found");
// Failure with exception
var error = Result<User>.Failure("Database error", exception);
// Checking results
if (result.IsSuccess)
{
var value = result.Value;
}
else
{
var error = result.Error;
var exception = result.Exception;
}For void operations, use the non-generic Result with IRequest:
public record DeleteUserRequest(Guid UserId) : IRequest;
public class DeleteUserHandler : IRequestHandler<DeleteUserRequest>
{
public async ValueTask<Result> HandleAsync(DeleteUserRequest request, CancellationToken cancellationToken)
{
await _repository.DeleteAsync(request.UserId);
return Result.Success();
}
}Add fluent inline validation to your handlers without separate validator classes:
dotnet add package ResultR.Validationusing ResultR.Validation;
public class CreateUserHandler : IRequestHandler<CreateUserRequest, User>
{
public ValueTask<Result> ValidateAsync(CreateUserRequest request)
{
return Validator.For(request)
.RuleFor(x => x.Email)
.NotEmpty("Email is required")
.EmailAddress("Invalid email format")
.RuleFor(x => x.Name)
.NotEmpty("Name is required")
.MinLength(2, "Name must be at least 2 characters")
.RuleFor(x => x.Age)
.GreaterThan(0, "Age must be positive")
.ToResult();
}
public async ValueTask<Result<User>> HandleAsync(CreateUserRequest request, CancellationToken ct)
{
var user = new User(request.Email, request.Name, request.Age);
await _repository.AddAsync(user, ct);
return Result<User>.Success(user);
}
}β Full ResultR.Validation documentation
var result = Result<User>.Success(user)
.WithMetadata("CreatedAt", DateTime.UtcNow)
.WithMetadata("Source", "API");Override only the hooks you need - no base class required:
// Just validation + handle (no before/after hooks)
public class ValidatingHandler : IRequestHandler<CreateOrderRequest, Order>
{
public ValueTask<Result> ValidateAsync(CreateOrderRequest request)
{
if (request.Items.Count == 0)
return new(Result.Failure("Order must have at least one item"));
return new(Result.Success());
}
public async ValueTask<Result<Order>> HandleAsync(CreateOrderRequest request, CancellationToken cancellationToken)
{
// This only runs if validation passes
var order = await _repository.CreateAsync(request, cancellationToken);
return Result<Order>.Success(order);
}
}ResultR prioritizes simplicity and explicitness over flexibility, giving you a focused request/handler pattern without the complexity of pipelines, behaviors, or middleware chains that you may never need. Every operation returns a unified Result or Result type, making error handling consistent and predictable across your entire codebase. The optional inline hooks (ValidateAsync, BeforeHandleAsync, AfterHandleAsync) let you add cross-cutting concerns directly in your handlers without separate classes or DI registrations, keeping related logic together and reducing ceremony.
The classic GoF Mediator pattern describes an object that coordinates bidirectional communication between multiple colleague objects - think of a chat room where participants talk through the mediator to each other.
What ResultR actually does is simpler: route a request to exactly one handler and return a response. There's no inter-handler communication. This is closer to a command pattern or in-process message bus.
We chose IDispatcher and Dispatcher because the name honestly describes the behavior: requests go in, get dispatched to a handler, and results come out.
There are many great request dispatcher / "mediator" implementations out there. Here is a comparison between ResultR and some of the other popular ones:
Performance comparison between ResultR (latest), MediatR (12.5.0), DispatchR (2.1.1), and Mediator.SourceGenerator (3.0.1):
| Method | Mean | Allocated | Ratio |
|---|---|---|---|
| MediatorSG - Simple | 17.15 ns | 72 B | 0.25 |
| MediatorSG - With Validation | 21.46 ns | 72 B | 0.31 |
| DispatchR - With Validation | 38.51 ns | 96 B | 0.56 |
| DispatchR - Simple | 39.33 ns | 96 B | 0.58 |
| MediatorSG - Full Pipeline | 44.27 ns | 72 B | 0.65 |
| DispatchR - Full Pipeline | 65.27 ns | 96 B | 0.96 |
| MediatR - Simple | 68.23 ns | 296 B | 1.00 |
| ResultR - Full Pipeline | 73.34 ns | 264 B | 1.08 |
| ResultR - With Validation | 75.46 ns | 264 B | 1.11 |
| ResultR - Simple | 80.65 ns | 264 B | 1.18 |
| MediatR - With Validation | 139.48 ns | 608 B | 2.05 |
| MediatR - Full Pipeline | 169.90 ns | 824 B | 2.49 |
Note on benchmark methodology: All libraries are configured with equivalent pipeline behaviors (validation, pre/post processing) for fair comparison. MediatorSG and DispatchR use source generation for optimal performance. ResultR always executes its full pipeline (Validate β BeforeHandle β Handle β AfterHandle) even when hooks use default implementations, which explains why "Simple" is slower than "Full Pipeline" - they're doing the same work.
What does this mean? When comparing equivalent functionality (full pipeline with behaviors), ResultR (73ns) significantly outperforms MediatR (170ns) - over 2.3x faster. The source-generated libraries (MediatorSG, DispatchR) are fastest but require compile-time code generation. In real applications where database queries take 1-10ms and HTTP calls take 50-500ms, these nanosecond differences are negligible. ResultR also allocates less memory than MediatR (264B vs 296-824B), reducing GC pressure in high-throughput scenarios.
Run benchmarks locally:
cd src/ResultR.Benchmarks
dotnet run -c Release- Issues: GitHub Issues
- Documentation: GitHub Wiki
Contributions are welcome! Please feel free to submit a Pull Request.
ISC License - see the LICENSE file for details.
Built with β€οΈ for the C# / DotNet community.