Introducing ResultR

A Modern Take on Request/Response Dispatching for .NET
Hello World! I’ve been working on a side project that I wanted to share with you all. After MediatR went to a paid model, I’m sure you heard all about that? Well, I like about 1000 other people decided I would just build my own. :)
So here’s ResultR - my opinionated take on a MediatR replacement. On my current project for my client we use MediatR but really only in the simplest way as a dispatcher for requests and responses. So I threw out all the extra features and functionality so that I could be focused on the core dispatcher pattern with a fixed, but optional, pipeline inside the handler class instead of hidden in nested pipeline classes that require a lot of setup.
The Four-Method Pipeline Pattern
One of the key design decisions in ResultR was to move the pipeline logic directly into the handler class. Instead of separate pipeline behaviors or middleware classes that require complex setup, each handler class has four methods that execute in sequence:
- ValidateAsync → BeforeHandleAsync → HandleAsync → AfterHandleAsync
Only HandleAsync is required - the others are optional virtual methods that you can implement as needed. This approach keeps related logic together and eliminates the need for separate classes or complex DI registrations.
public class CreateUserHandler : IRequestHandler<CreateUserRequest, User>
{
// Optional: Validate the request
public ValueTask<Result> ValidateAsync(CreateUserRequest request)
{
if (string.IsNullOrWhiteSpace(request.Email))
return new(Result.Failure("Email is required"));
return new(Result.Success());
}
// Optional: Before handle hook
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)
{
var user = new User(request.Email, request.Name, request.Age);
await _repository.AddAsync(user, cancellationToken);
return Result<User>.Success(user);
}
// Optional: After handle hook
public ValueTask AfterHandleAsync(CreateUserRequest request, Result<User> result)
{
if (result.IsSuccess)
_logger.LogInformation("User created successfully: {UserId}", result.Value.Id);
return default;
}
}
The Result Pattern Integration
I’m a big fan of the Result Pattern, so I rolled that into the base library too. Every request gets a standard result object back with any return data held in the result object. This 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;
}
This eliminates the need for try/catch blocks scattered throughout your codebase since exceptions are automatically caught and returned as failure results.
What ResultR Doesn’t Do
To keep the library focused and lightweight, ResultR intentionally avoids certain features that you might find in other similar libraries:
- ❌ 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. If you need these features, you might want to look at other alternatives. But if you just need a simple request/response dispatcher with the Result pattern, ResultR might be perfect for you.
IDE Extensions: Supercharging Development Workflow
One of the things I’m most excited about is the IDE extensions I’ve developed to make working with ResultR even better. I believe that developer tools should make our lives easier, not harder.
Visual Studio Toolkit
The ResultR Visual Studio Toolkit provides seamless integration with Visual Studio, offering:
- Instant Navigation: Place your cursor on any IRequest type and press Ctrl+R, Ctrl+H to instantly jump to the corresponding handler
- One-Click Scaffolding: Right-click on a folder and generate new request/handler pairs with proper namespaces and structure
- Smart Code Generation: Automatically detects your project’s conventions (file-scoped vs block-scoped namespaces)
- Keyboard Customization: Configure your own keybindings for the commands you use most
VS Code Toolkit
For those who prefer VS Code, the ResultR VS Code Toolkit offers the same powerful features:
- Cross-Platform Navigation: Use Ctrl+R, Ctrl+H (or Cmd+R, Cmd+H on Mac) to jump between requests and handlers
- Explorer Integration: Right-click on folders to create new request/handler pairs
- Workspace Intelligence: Automatically searches across your entire workspace to find handlers
- Configurable Search Patterns: Exclude certain directories from searches (bin, obj, node_modules, etc.)
These extensions solve one of the biggest pain points when using request/response patterns: the constant navigation between request definitions and their handlers. No more hunting through your solution or workspace - just press a shortcut and you’re there.
Getting Started with ResultR
Installation
dotnet add package ResultR
For inline validation with a fluent API:
dotnet add package ResultR.Validation
Basic Usage
- Define a Request:
public record CreateUserRequest(string Email, string Name, int Age) : IRequest<User>;
- Create a Handler:
public class CreateUserHandler : IRequestHandler<CreateUserRequest, User>
{
public async ValueTask<Result<User>> HandleAsync(CreateUserRequest request, CancellationToken cancellationToken)
{
var user = new User(request.Email, request.Name, request.Age);
await _repository.AddAsync(user, cancellationToken);
return Result<User>.Success(user);
}
}
- Register with DI:
// Simple: auto-scans entry assembly
services.AddResultR();
// Or explicit: scan specific assemblies
services.AddResultR(
typeof(Program).Assembly,
typeof(MyHandlers).Assembly);
- Dispatch Requests:
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);
}
}
Advanced Features
ResultR.Validation
The optional ResultR.Validation package adds fluent inline validation without separate validator classes:
using 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)
{
// This only runs if validation passes
var user = new User(request.Email, request.Name, request.Age);
await _repository.AddAsync(user, ct);
return Result<User>.Success(user);
}
}
Metadata Support
Add metadata to your results for additional context:
var result = Result<User>.Success(user)
.WithMetadata("CreatedAt", DateTime.UtcNow)
.WithMetadata("Source", "API");
Performance Considerations
I know performance is important to many developers, so I’ve included benchmarks comparing ResultR with other popular request dispatcher libraries:
| 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 |
When comparing equivalent functionality (full pipeline with behaviors), ResultR (73ns) significantly outperforms MediatR (170ns) - over 2.3x faster. In real applications where database queries take 1-10ms and HTTP calls take 50-500ms, these nanosecond differences are negligible, but it’s nice to know the library is efficient.
Why “Dispatcher” instead of “Mediator”?
I chose IDispatcher and Dispatcher because the name honestly describes the behavior: requests go in, get dispatched to a handler, and results come out.
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.
Requirements and Future
ResultR is built for modern .NET development:
- .NET 10.0 or later
- C# 14.0 or later
I’m actively developing ResultR and have plans for additional features based on community feedback. The IDE extensions will continue to evolve to make the development experience even better.
Try It Out
You can find ResultR on NuGet and the IDE extensions on their respective marketplaces:
- ResultR on NuGet
- ResultR.Validation on NuGet
- ResultR Visual Studio Toolkit on VS Marketplace
- ResultR VS Code Toolkit on VS Marketplace
The full source code is available on GitHub, and I welcome contributions, issues, and feedback from the community.
Conclusion
ResultR represents my vision for what a modern request/response dispatcher should be: simple, predictable, and developer-friendly. By focusing on the core dispatcher pattern and integrating the Result pattern directly into the library, I hope to make your .NET development experience more enjoyable and productive.
The IDE extensions are particularly exciting for me because they address real-world developer pain points around navigation and code generation. Being able to instantly jump between requests and handlers, or scaffold new pairs with proper structure, can save significant time during development.
Give ResultR a try in your next project and let me know what you think! I’m always looking for feedback and ideas for improvement.
Built with ❤️ for the C# / DotNet community.