MediatR integration for ResultKit - Railway Oriented Programming pipeline behaviors for CQRS commands and queries.
ResultKit.MediatR provides seamless integration between MediatR and ResultKit, enabling automatic exception handling and result-based error handling in your CQRS command/query handlers.
- Automatic Exception Handling - Exceptions in handlers are automatically converted to
Result.Failure - Type-safe Pipeline Behavior - Works with both
ResultandResult<T>responses - Zero Configuration - Single extension method to enable
- Fluent Integration - Seamlessly integrates with existing MediatR setup
This is a private NuGet package. Configure your NuGet source first:
dotnet nuget add source https://your-private-feed-url --name PrivateFeedThen install both packages:
dotnet add package ResultKit
dotnet add package ResultKit.MediatRusing ResultKit.MediatR;
var builder = WebApplication.CreateBuilder(args);
// Register MediatR
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssemblyContaining<Program>());
// Add ResultKit pipeline behavior
builder.Services.AddResultKitMediatR();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);
}
}[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 })
}
);
}
}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}");
}
}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).
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"
]
}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
}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));
}| Method | Description |
|---|---|
AddResultKitMediatR(this IServiceCollection) |
Registers the ResultKit pipeline behavior |
Automatically registered pipeline behavior that:
- Catches unhandled exceptions in handlers
- Converts them to
Result.Failure(exception.Message) - Only applies to handlers returning
ResultorResult<T>
- .NET 8.0 or higher
- MediatR 12.0 or higher
- ResultKit 1.0 or higher
Copyright (c) 2026 Károly Akácz. All rights reserved.
This is proprietary software for internal use only.
Károly Akácz
- GitHub: @akikari
- Repository: ResultKit.MediatR
Note: This is a private NuGet package. Ensure you have proper access credentials configured.