Patterns
FluentValidation
Automatic input validation using FluentValidation
The FluentValidation integration automatically validates messages before they reach the handler, using your existing FluentValidation validators.
Installation
dotnet add package TurboMediator.FluentValidationConfiguration
builder.Services.AddTurboMediator(m =>
{
// Auto-discover validators in the current assembly
m.WithFluentValidation();
// Or specify the assembly
m.WithFluentValidation<Program>();
});Creating Validators
Define validators using standard FluentValidation:
using FluentValidation;
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
public CreateUserCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required")
.MaximumLength(100).WithMessage("Name must be 100 characters or less");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email format");
RuleFor(x => x.Age)
.InclusiveBetween(18, 120).WithMessage("Age must be between 18 and 120");
}
}
public class UpdateProductCommandValidator : AbstractValidator<UpdateProductCommand>
{
private readonly IProductRepository _repository;
public UpdateProductCommandValidator(IProductRepository repository)
{
_repository = repository;
RuleFor(x => x.Name)
.NotEmpty()
.MustAsync(async (name, ct) =>
{
var existing = await _repository.FindByNameAsync(name, ct);
return existing is null;
}).WithMessage("Product name already exists");
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("Price must be positive");
}
}ValidationException
When validation fails, a ValidationException is thrown with all failures:
public class ValidationException : Exception
{
public IReadOnlyList<ValidationFailure> Failures { get; }
}
public class ValidationFailure
{
public string PropertyName { get; }
public string ErrorMessage { get; }
public object? AttemptedValue { get; }
public string? ErrorCode { get; }
public ValidationSeverity Severity { get; }
}
public enum ValidationSeverity
{
Error,
Warning,
Info
}Handling Validation Errors
app.MapPost("/users", async (CreateUserCommand command, IMediator mediator) =>
{
try
{
var user = await mediator.Send(command);
return Results.Created($"/users/{user.Id}", user);
}
catch (ValidationException ex)
{
var errors = ex.Failures
.Select(f => new { f.PropertyName, f.ErrorMessage })
.ToList();
return Results.BadRequest(new { Errors = errors });
}
});Global Exception Handler
app.UseExceptionHandler(app =>
{
app.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
if (exception is ValidationException validationEx)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(new
{
Type = "ValidationError",
Errors = validationEx.Failures.Select(f => new
{
Field = f.PropertyName,
Message = f.ErrorMessage,
Severity = f.Severity.ToString()
})
});
}
});
});Multiple Validators
You can define multiple validators for the same message. All validators are executed and all failures are collected:
// Basic validation rules
public class CreateOrderBasicValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderBasicValidator()
{
RuleFor(x => x.Items).NotEmpty();
}
}
// Business rule validation (separate concerns)
public class CreateOrderBusinessValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderBusinessValidator(IInventoryService inventory)
{
RuleForEach(x => x.Items).MustAsync(async (item, ct) =>
{
var stock = await inventory.GetStockAsync(item.ProductId, ct);
return stock >= item.Quantity;
}).WithMessage("Insufficient stock");
}
}The FluentValidationBehavior resolves all IValidator<TMessage> from DI and runs them. Failures from all validators are aggregated into a single ValidationException.