MediatR
The Spur.MediatR package integrates Spur with MediatR for CQRS architectures, providing base handler classes and automatic exception wrapping.
Installation
dotnet add package Spur.MediatR
ResultHandler base classes
Query handler (returns a value)
using Spur.MediatR;
public record GetUserQuery(int Id) : IRequest<Result<UserDto>>;
public class GetUserHandler : ResultHandler<GetUserQuery, UserDto>
{
private readonly IUserRepository _repo;
public GetUserHandler(IUserRepository repo) => _repo = repo;
public override async Task<Result<UserDto>> Handle(
GetUserQuery request, CancellationToken ct)
{
return await _repo.GetByIdAsync(request.Id)
.Map(user => new UserDto(user.Id, user.Name, user.Email));
}
}
Command handler (no return value)
public record DeleteUserCommand(int Id) : IRequest<Result<Unit>>;
public class DeleteUserHandler : ResultHandler<DeleteUserCommand>
{
private readonly IUserRepository _repo;
public DeleteUserHandler(IUserRepository repo) => _repo = repo;
public override async Task<Result<Unit>> Handle(
DeleteUserCommand request, CancellationToken ct)
{
return await _repo.DeleteAsync(request.Id);
}
}
ResultPipelineBehavior
Register the pipeline behavior to catch unhandled exceptions and wrap them as Result.Failure:
builder.Services.AddTransient(
typeof(IPipelineBehavior<,>),
typeof(ResultPipelineBehavior<,>));
What it does:
- If the handler throws
SpurException, it extracts theErrorand returnsResult.Failure. - If the handler throws any other exception, it wraps it as
Error.Unexpectedand returnsResult.Failure. - It only activates when the response type is
Result<T>.
Wiring it up with ASP.NET Core
using Spur.AspNetCore;
app.MapGet("/users/{id}", async (int id, IMediator mediator) =>
{
return await mediator.Send(new GetUserQuery(id))
.ToHttpResult();
});
app.MapDelete("/users/{id}", async (int id, IMediator mediator) =>
{
return await mediator.Send(new DeleteUserCommand(id))
.ToHttpResult(); // 204 No Content on success
});
With FluentValidation
Combine with Spur.FluentValidation for validated commands:
public class CreateUserHandler : ResultHandler<CreateUserCommand, UserDto>
{
private readonly IValidator<CreateUserCommand> _validator;
private readonly IUserRepository _repo;
public override async Task<Result<UserDto>> Handle(
CreateUserCommand request, CancellationToken ct)
{
return await Result.Start(request)
.ValidateAsync(_validator, ct)
.ThenAsync(r => _repo.CreateAsync(r))
.Map(user => user.ToDto());
}
}
See also
- ASP.NET Core — HTTP response mapping
- FluentValidation — input validation
- Pipeline operators — Result pipeline reference