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

PriyaPriya
3 min read

✨ 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

0
Subscribe to my newsletter

Read articles from Priya directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Priya
Priya