ASP.NET Core Web APIs · Minimal APIs Done Right
Built-in Validation for Minimal APIs
Validate request bodies with data annotations and .NET 10's built-in minimal API validation, returning ProblemDetails errors.
What you'll build
A POST /api/orders endpoint whose OrderInput record uses data annotations ([Required], [Range], [EmailAddress]). With builder.Services.AddValidation(), invalid requests never reach the handler: clients get a 400 with a ValidationProblemDetails body listing each field's errors.
The idea
- Data annotations describe rules on records and classes.
- In .NET 10,
AddValidation()turns on automatic validation of minimal API parameters: bodies, and route or query values with attributes. A source generator finds the public types to validate at compile time. - Errors are returned as ProblemDetails (
application/problem+json), a standard error format (RFC 9457). - Use custom
IValidatableObject(or custom attributes) for rules involving several fields.
The code
using System.ComponentModel.DataAnnotations;
var builder = WebApplication.CreateBuilder();
builder.Services.AddValidation();
var app = builder.Build();
app.MapPost("/api/orders", (OrderInput input) => TypedResults.Created("/api/orders/1", input));
app.MapGet("/api/orders", ([Range(1, 100)] int pageSize = 20) => TypedResults.Ok(new { pageSize }));
app.Urls.Add("http://127.0.0.1:0");
await app.StartAsync();
using var http = new HttpClient { BaseAddress = new Uri(app.Urls.First()) };
await Show(await http.PostAsJsonAsync("/api/orders", new { email = "[email protected]", quantity = 2, sku = "MUG-1" }), "valid");
await Show(await http.PostAsJsonAsync("/api/orders", new { email = "not-an-email", quantity = 0 }), "invalid body");
await Show(await http.GetAsync("/api/orders?pageSize=500"), "invalid query");
await app.StopAsync();
static async Task Show(HttpResponseMessage r, string label)
{
var body = await r.Content.ReadAsStringAsync();
Console.WriteLine($"{label} -> {(int)r.StatusCode} {body}");
}
public record OrderInput( // public: the validation generator skips internal types
[Required, EmailAddress] string Email,
[Range(1, 10)] int Quantity,
[Required, StringLength(20, MinimumLength = 3)] string Sku);
Output
valid -> 201 {"email":"[email protected]","quantity":2,"sku":"MUG-1"}
invalid body -> 400 {"title":"One or more validation errors occurred.","errors":{"Email":["The Email field is not a valid e-mail address."],"Quantity":["The field Quantity must be between 1 and 10."],"Sku":["The Sku field is required."]}}
invalid query -> 400 {"title":"One or more validation errors occurred.","errors":{"pageSize":["The field pageSize must be between 1 and 100."]}}Run this on your computer — see “Get Started” for setup.
Common mistakes
- Validating manually in every handler with
ifstatements. - Declaring input types without
public: the source generator only sees public types, so validation silently doesn't run. - Returning custom error shapes per endpoint instead of ProblemDetails.
- Trusting validation alone for business rules that depend on data (stock, ownership): check those in the service.
Key points
- Describe rules with data annotations on input types.
AddValidation()validates minimal API parameters automatically in .NET 10.- Errors come back as standard ValidationProblemDetails.
Further reading: Validation in minimal APIs · RFC 9457: Problem Details
Practice questions
What does builder.Services.AddValidation() enable in .NET 10?
Answer
Automatic validation of minimal API parameters annotated with data annotations; invalid requests get a 400 ValidationProblemDetails response before the handler runs.
What is ProblemDetails?
Answer
A standard JSON format for HTTP API errors (RFC 9457) with type, title, status, detail and extensions such as per-field errors.
Why aren't data annotations enough for all validation?
Answer
They check the shape of input; rules depending on data or state (stock levels, ownership, uniqueness) must be checked in services or the database.
Open in the app to mark lessons complete, take notes and track your progress.