Skip to content

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.

License

Notifications You must be signed in to change notification settings

AlanBarber/ResultR

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

12 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🎯 ResultR

GitHub Release GitHub Actions Workflow Status GitHub Downloads (all assets, all releases) NuGet Version NuGet Downloads GitHub License

πŸ“– Overview

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

This focused scope keeps the library small, fast, and easy to understand.

✨ Key Features

  • πŸ”Œ Simple Interface Pattern: Uses IRequest/IRequest<TResponse> and IRequestHandler<TRequest>/IRequestHandler<TRequest, TResponse> - no distinction between commands and queries
  • πŸ“¦ Unified Result Type: All operations return Result or Result<T>, supporting success/failure states, exception capture, and optional metadata
  • πŸͺ Optional Inline Hooks: Handlers can override ValidateAsync(), BeforeHandleAsync(), and AfterHandleAsync() methods without requiring base classes or separate interfaces
  • ⚑ Minimal Configuration: Simple DI integration with minimal setup
  • πŸ”’ Strong Typing: Full type safety throughout the pipeline

Pipeline Execution

Each request flows through a simple, predictable pipeline:

  1. βœ… Validation - Calls ValidateAsync() if overridden, short-circuits on failure
  2. πŸš€ Before Handle - Invokes BeforeHandleAsync() for optional logging or setup
  3. βš™οΈ Handle - Executes the core HandleAsync() logic
  4. 🏁 After Handle - Invokes AfterHandleAsync() for logging or cleanup
  5. πŸ›‘οΈ Exception Handling - Any exceptions are caught and returned as Result.Failure with the exception attached

πŸ“š Read the full documentation on the Wiki β†’

πŸ’‘ Design Philosophy

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

πŸ“₯ Installation

dotnet add package ResultR

πŸš€ Quick Start

1. Define a Request

public record CreateUserRequest(string Email, string Name) : IRequest<User>;

2. Create a Handler

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);
        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;
    }
}

3. Register with DI

// Simple: auto-scans entry assembly
services.AddResultR();

// Or explicit: scan specific assemblies (for multi-project solutions)
services.AddResultR(
    typeof(Program).Assembly,
    typeof(MyHandlers).Assembly);

4. 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);
    }
}

πŸ“¦ Result Type

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();
    }
}

πŸ”§ Advanced Features

Metadata Support

var result = Result<User>.Success(user)
    .WithMetadata("CreatedAt", DateTime.UtcNow)
    .WithMetadata("Source", "API");

Optional Hooks

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);
    }
}

❓ FAQ

Why "Dispatcher" instead of "Mediator"?

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.

πŸ“Š Benchmarks

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 (2.1.7):

Method Mean Allocated Ratio
MediatorSG - With Validation 20.26 ns 72 B 0.27
MediatorSG - Simple 23.01 ns 72 B 0.31
DispatchR - With Validation 31.37 ns 96 B 0.42
DispatchR - Simple 34.93 ns 96 B 0.47
DispatchR - Full Pipeline 44.02 ns 96 B 0.59
MediatorSG - Full Pipeline 44.35 ns 72 B 0.59
ResultR - Full Pipeline 62.92 ns 264 B 0.84
MediatR - Simple 75.03 ns 296 B 1.00
ResultR - With Validation 77.10 ns 264 B 1.03
ResultR - Simple 95.42 ns 264 B 1.27
MediatR - With Validation 120.28 ns 608 B 1.60
MediatR - Full Pipeline 158.01 ns 824 B 2.11

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 (63ns) significantly outperforms MediatR (158ns) - over 2.5x 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

πŸ“‹ Requirements

  • .NET 10.0 or later
  • C# 14.0 or later

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

πŸ“„ License

ISC License - see LICENSE file for details

πŸ’¬ Support


Built with ❀️ for clean, maintainable C# applications.

About

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.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages