🧼 Clean Global Validation in ASP.NET Core with FluentValidation & Middleware

✨ Build scalable, reusable API validation with middleware and FluentValidation in .NET.
🧠 Why Global Validation?
Validation ensures your app processes only correct and expected data. While [ApiController] in ASP.NET Core automatically checks model binding and validation, larger applications require centralized and reusable logic.
In this post, we’ll see how to:
✅ Use FluentValidation to define expressive, testable rules
✅ Implement custom middleware for global error handling
✅ Return consistent API responses
🔧 Project Setup
Install the FluentValidation NuGet package:
dotnet add package FluentValidation.AspNetCore
Register FluentValidation in Program.cs:
builder.Services.AddControllers()
.AddFluentValidation(cfg => cfg.RegisterValidatorsFromAssemblyContaining<Program>());
📦 Sample DTO & Validator
DTO:
public class BusDto
{
public string BusName { get; set; }
public string Type { get; set; }
}
Validator:
using FluentValidation;
public class BusDtoValidator : AbstractValidator<BusDto>
{
public BusDtoValidator()
{
RuleFor(x => x.BusName)
.NotEmpty().WithMessage("Bus Name is required");
RuleFor(x => x.Type)
.NotEmpty().WithMessage("Bus Type is required");
}
}
⚙️ Global Validation Middleware
Create a custom middleware to catch and handle validation errors uniformly:
using System.Text.Json;
using FluentValidation;
public class ValidationMiddleware
{
private readonly RequestDelegate _next;
public ValidationMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context); // Proceed to next middleware or endpoint
}
catch (ValidationException ex)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.ContentType = "application/json";
var errors = ex.Errors.Select(e => new { e.PropertyName, e.ErrorMessage });
var result = JsonSerializer.Serialize(new
{
StatusCode = 400,
Message = "Validation failed",
Errors = errors
});
await context.Response.WriteAsync(result);
}
}
}
Register it in Program.cs:
app.UseMiddleware<ValidationMiddleware>();
✅ Example Controller
[ApiController]
[Route("api/[controller]")]
public class BusController : ControllerBase
{
[HttpPost]
public IActionResult Create(BusDto bus)
{
// If validation passes, continue processing
return Ok(new { message = "Bus created successfully", bus });
}
}
🧪 Sample Request & Response
Input:
{
"BusName": "",
"Type": ""
}
Output:
{
"statusCode": 400,
"message": "Validation failed",
"errors": [
{ "propertyName": "BusName", "errorMessage": "Bus Name is required" },
{ "propertyName": "Type", "errorMessage": "Bus Type is required" }
]
}
🧠 Benefits of This Approach
🔁 Reusable: Validators live separately from controllers
🧼 Clean: No manual ModelState.IsValid checks
📦 Scalable: Central error response logic
🧪 Testable: Validators are independently testable
📌 Final Thoughts
With FluentValidation + Middleware, you create a cleaner validation flow across your entire ASP.NET Core app.
✅ Centralized
✅ Consistent
✅ Easy to maintain
💡 What’s Next?
You can extend this pattern by:
Adding localization to error messages
Handling nested validation (complex objects)
Mapping error structure to your API spec (like RFC 7807 Problem Details)
🔗 Let’s Connect
Like this post? Drop a 💬 comment, or share your thoughts on improving API validation in .NET.
Follow for more C#/.NET + API design tips 👇
#aspnet-core #fluentvalidation #webapi #clean-architecture
Subscribe to my newsletter
Read articles from Priya directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
