Skip to content

Latest commit

 

History

History
283 lines (212 loc) · 7.92 KB

File metadata and controls

283 lines (212 loc) · 7.92 KB

ResultKit.MediatR

MediatR integration for ResultKit - Railway Oriented Programming pipeline behaviors for CQRS commands and queries.

📋 Overview

ResultKit.MediatR provides seamless integration between MediatR and ResultKit, enabling automatic exception handling and result-based error handling in your CQRS command/query handlers.

✨ Features

  • Automatic Exception Handling - Exceptions in handlers are automatically converted to Result.Failure
  • Type-safe Pipeline Behavior - Works with both Result and Result<T> responses
  • Zero Configuration - Single extension method to enable
  • Fluent Integration - Seamlessly integrates with existing MediatR setup

🚀 Installation

This is a private NuGet package. Configure your NuGet source first:

dotnet nuget add source https://your-private-feed-url --name PrivateFeed

Then install both packages:

dotnet add package ResultKit
dotnet add package ResultKit.MediatR

📖 Usage

1. Register MediatR + ResultKit Pipeline

using ResultKit.MediatR;

var builder = WebApplication.CreateBuilder(args);

// Register MediatR
builder.Services.AddMediatR(cfg => 
    cfg.RegisterServicesFromAssemblyContaining<Program>());

// Add ResultKit pipeline behavior
builder.Services.AddResultKitMediatR();

2. Write Handlers that Return Result

using MediatR;
using ResultKit;

// Command
public record CreateUserCommand(string Email, string Password) : IRequest<Result<Guid>>;

// Handler
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Result<Guid>>
{
    private readonly IUserRepository _repository;

    public CreateUserCommandHandler(IUserRepository repository)
    {
        _repository = repository;
    }

    public async Task<Result<Guid>> Handle(CreateUserCommand request, CancellationToken cancellationToken)
    {
        // Validation with error codes
        if (string.IsNullOrWhiteSpace(request.Email))
            return Result<Guid>.Failure(ResultError.Create("VALIDATION_EMAIL_REQUIRED", "Email is required"));

        if (!request.Email.Contains("@"))
            return Result<Guid>.Failure(ResultError.Create("VALIDATION_EMAIL_FORMAT", "Invalid email format"));

        // Business logic (exceptions auto-handled by pipeline)
        var user = new User(request.Email, request.Password);
        await _repository.AddAsync(user, cancellationToken);

        return Result<Guid>.Success(user.Id);
    }
}

3. Use in Controllers

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IMediator _mediator;

    public UsersController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request)
    {
        var command = new CreateUserCommand(request.Email, request.Password);
        var result = await _mediator.Send(command);

        return result.Match<IActionResult>(
            onSuccess: userId => Ok(new { userId }),
            onFailure: error => ResultError.Parse(error) switch
            {
                ("VALIDATION_EMAIL_REQUIRED", var msg) => BadRequest(new { error = msg }),
                ("VALIDATION_EMAIL_FORMAT", var msg) => BadRequest(new { error = msg }),
                _ => BadRequest(new { error })
            }
        );
    }
}

🎯 How It Works

Without ResultKit.MediatR

public async Task<Result<User>> Handle(GetUserQuery request, CancellationToken ct)
{
    try
    {
        var user = await _repository.FindByIdAsync(request.Id, ct);
        return user != null 
            ? Result<User>.Success(user)
            : Result<User>.Failure("User not found");
    }
    catch (Exception ex)
    {
        return Result<User>.Failure($"Database error: {ex.Message}");
    }
}

With ResultKit.MediatR (Cleaner!)

public async Task<Result<User>> Handle(GetUserQuery request, CancellationToken ct)
{
    // Exceptions automatically caught and converted to Result.Failure
    var user = await _repository.FindByIdAsync(request.Id, ct);
    
    return user != null 
        ? Result<User>.Success(user)
        : Result<User>.Failure(ResultError.Create("USER_NOT_FOUND", "User not found"));
}

The pipeline behavior automatically catches exceptions and converts them to Result.Failure(ex.Message).

📚 Advanced Usage

Validation with ErrorsResult

Collect multiple validation errors before processing (better UX than fail-fast):

public async Task<Result<Guid>> Handle(CreateUserCommand request, CancellationToken ct)
{
    // Phase 1: Collect ALL validation errors
    var validation = ErrorsResult.Collect(
        ValidateEmail(request.Email),
        ValidatePassword(request.Password),
        ValidateAge(request.Age)
    );

    if (validation.IsFailure)
    {
        return validation.ToResult<Guid>(); // Returns all errors combined
    }

    // Phase 2: Business logic with fail-fast
    if (await _repository.EmailExistsAsync(request.Email, ct))
    {
        return Result<Guid>.Failure(ResultError.Create("USER_EMAIL_EXISTS", "Email already exists"));
    }

    var user = new User(request.Email, request.Password);
    await _repository.AddAsync(user, ct);
    
    return Result<Guid>.Success(user.Id);
}

private static Result ValidateEmail(string email)
{
    if (string.IsNullOrWhiteSpace(email))
        return Result.Failure(ResultError.Create("VALIDATION_EMAIL_REQUIRED", "Email is required"));
    
    if (!email.Contains("@"))
        return Result.Failure(ResultError.Create("VALIDATION_EMAIL_FORMAT", "Invalid email format"));
    
    return Result.Success();
}

Response on validation failure:

{
  "errors": [
    "VALIDATION_EMAIL_REQUIRED: Email is required",
    "VALIDATION_PASSWORD_TOO_SHORT: Password must be at least 8 characters"
  ]
}

Custom Error Handling

If you need custom exception handling, you can still catch specific exceptions:

public async Task<Result<User>> Handle(GetUserQuery request, CancellationToken ct)
{
    try
    {
        var user = await _repository.FindByIdAsync(request.Id, ct);
        return Result<User>.Success(user);
    }
    catch (UserNotFoundException ex)
    {
        return Result<User>.Failure($"User {request.Id} not found");
    }
    // Other exceptions still handled by pipeline
}

Combining with ResultKit Extensions

public async Task<Result<UserDto>> Handle(GetUserQuery request, CancellationToken ct)
{
    return await _repository.FindByIdAsync(request.Id, ct)
        .ToResult($"User {request.Id} not found")
        .MapAsync(user => new UserDto(user.Name, user.Email))
        .TapAsync(dto => _logger.LogInformation("Retrieved user: {Name}", dto.Name));
}

🏗️ API Reference

ServiceCollectionExtensions

Method Description
AddResultKitMediatR(this IServiceCollection) Registers the ResultKit pipeline behavior

ResultPipelineBehavior<TRequest, TResponse>

Automatically registered pipeline behavior that:

  • Catches unhandled exceptions in handlers
  • Converts them to Result.Failure(exception.Message)
  • Only applies to handlers returning Result or Result<T>

🔧 Requirements

  • .NET 8.0 or higher
  • MediatR 12.0 or higher
  • ResultKit 1.0 or higher

📝 License

Copyright (c) 2026 Károly Akácz. All rights reserved.

This is proprietary software for internal use only.

👤 Author

Károly Akácz

🔗 Related Packages

  • ResultKit - Core result handling library
  • MediatR - Simple mediator implementation in .NET

Note: This is a private NuGet package. Ensure you have proper access credentials configured.