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:

  1. ValidateAsyncBeforeHandleAsyncHandleAsyncAfterHandleAsync

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

  1. Define a Request:
public record CreateUserRequest(string Email, string Name, int Age) : IRequest<User>;
  1. 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);
    }
}
  1. Register with DI:
// Simple: auto-scans entry assembly
services.AddResultR();

// Or explicit: scan specific assemblies
services.AddResultR(
    typeof(Program).Assembly,
    typeof(MyHandlers).Assembly);
  1. 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:

MethodMeanAllocatedRatio
MediatorSG - Simple17.15 ns72 B0.25
MediatorSG - With Validation21.46 ns72 B0.31
DispatchR - With Validation38.51 ns96 B0.56
DispatchR - Simple39.33 ns96 B0.58
MediatorSG - Full Pipeline44.27 ns72 B0.65
DispatchR - Full Pipeline65.27 ns96 B0.96
MediatR - Simple68.23 ns296 B1.00
ResultR - Full Pipeline73.34 ns264 B1.08
ResultR - With Validation75.46 ns264 B1.11
ResultR - Simple80.65 ns264 B1.18
MediatR - With Validation139.48 ns608 B2.05
MediatR - Full Pipeline169.90 ns824 B2.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:

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.

Avatar
Alan P. Barber
Software Developer, Computer Scientist, Scrum Master, & Crohn’s Disease Fighter

I specialize in Software Development with a focus on Architecture and Design.

comments powered by Disqus
Next
Previous

Related