Les 5 · Module 1 · API-basis
Validatie en ProblemDetails
Valideer invoer van Winkel.Api met DataAnnotations of FluentValidation in een endpoint filter, en geef fouten terug als gestandaardiseerde ProblemDetails (RFC 9457).
🎯 Na deze les kun je
- invoer valideren met DataAnnotations en met FluentValidation
- validatie in een herbruikbaar endpoint filter plaatsen
- fouten teruggeven als ProblemDetails en ValidationProblem
- onverwachte fouten omzetten naar een veilige 500 met ProblemDetails
Een client stuurt nu een product met naam "" en prijs -5, en de API slaat het op. Elke API moet invoer controleren, en
fouten zo teruggeven dat een programma ze kan lezen. Daarvoor bestaat een standaard: ProblemDetails (RFC 9457), een vast
JSON-formaat voor fouten dat ASP.NET Core ingebouwd heeft.
ProblemDetails
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Naam": ["Naam is verplicht."],
"Prijs": ["Prijs moet tussen 0,01 en 10.000 liggen."]
}
}
Met Content-Type: application/problem+json. Clients weten zo altijd waar ze de foutmeldingen vinden.
builder.Services.AddProblemDetails();
Optie 1: DataAnnotations
public record MaakProductRequest(
[property: Required, StringLength(60, MinimumLength = 2)] string Naam,
[property: Range(0.01, 10_000)] decimal Prijs,
[property: Range(0, 10_000)] decimal Inkoopprijs,
[property: Range(0, 100_000)] int Voorraad);
Controllers met [ApiController] valideren dit automatisch. Minimal API's in .NET 8 niet: je roept de validator zelf aan, het
liefst in een endpoint filter zodat elke handler het krijgt.
Optie 2: FluentValidation (deze cursus)
dotnet add package FluentValidation.DependencyInjectionExtensions
public class MaakProductValidator : AbstractValidator<MaakProductRequest>
{
public MaakProductValidator()
{
RuleFor(x => x.Naam).NotEmpty().Length(2, 60);
RuleFor(x => x.Prijs).InclusiveBetween(0.01m, 10_000m).PrecisionScale(7, 2, true);
RuleFor(x => x.Inkoopprijs).GreaterThanOrEqualTo(0).LessThan(x => x.Prijs)
.WithMessage("Inkoopprijs moet lager zijn dan de verkoopprijs.");
RuleFor(x => x.Voorraad).InclusiveBetween(0, 100_000);
}
}
Regels die velden vergelijken (inkoop < verkoop) zijn met FluentValidation duidelijker dan met attributen.
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
Het filter
public class ValidatieFilter<T> : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext ctx, EndpointFilterDelegate next)
{
var validator = ctx.HttpContext.RequestServices.GetRequiredService<IValidator<T>>();
var model = ctx.Arguments.OfType<T>().Single();
var resultaat = await validator.ValidateAsync(model);
if (!resultaat.IsValid)
return TypedResults.ValidationProblem(resultaat.ToDictionary());
return await next(ctx);
}
}
public static class ValidatieExtensies
{
public static RouteHandlerBuilder Valideer<T>(this RouteHandlerBuilder b) => b.AddEndpointFilter<ValidatieFilter<T>>();
}
groep.MapPost("/", Maak).Valideer<MaakProductRequest>();
Voorspel eerst: een client stuurt {"naam": "Muis", "prijs": "gratis"}. Komt het verzoek bij je validator?
Uitkomst
Nee. "gratis" kan niet naar decimal: het mislukt al bij het binden van de JSON, vóór filters en validators. ASP.NET Core
geeft dan een 400 (BadHttpRequestException). Zonder AddProblemDetails() is die body leeg; met AddProblemDetails() en de
exception-handler hieronder krijgt de client ook hier een ProblemDetails. Er zijn dus twee soorten fouten: kapotte JSON en
inhoudelijk ongeldig.
Fouten die geen validatie zijn
Een dubbele naam (409) of een product dat niet bestaat (404) geef je ook als ProblemDetails:
return TypedResults.Problem(
title: "Productnaam bestaat al",
detail: $"Er is al een product met de naam '{req.Naam}'.",
statusCode: StatusCodes.Status409Conflict);
AddProblemDetails() zorgt ook dat een kale TypedResults.NotFound() een ProblemDetails-body krijgt.
Onverwachte fouten
app.UseExceptionHandler(); // gebruikt AddProblemDetails
app.UseStatusCodePages();
Een exception in je code geeft nu een 500 met een ProblemDetails zonder stacktrace. In Development kun je
app.UseDeveloperExceptionPage() gebruiken. Wil je eigen exceptions vertalen (bijvoorbeeld VoorraadTeLaagException → 409),
schrijf dan een IExceptionHandler:
public class DomeinFoutHandler : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception ex, CancellationToken ct)
{
if (ex is not VoorraadTeLaagException v) return false;
ctx.Response.StatusCode = 409;
await ctx.Response.WriteAsJsonAsync(new ProblemDetails { Status = 409, Title = "Te weinig voorraad", Detail = v.Message }, ct);
return true;
}
}
// builder.Services.AddExceptionHandler<DomeinFoutHandler>();
Typische fouten
- Validator draait niet: niet geregistreerd (
AddValidatorsFromAssemblyContaining) of filter niet gekoppeld. InvalidOperationException: Sequence contains no elementsin het filter: het endpoint heeft geen parameter van typeT.- Stacktrace in productie:
UseDeveloperExceptionPagestaat buiten eenIsDevelopment()-check.
Oefenen
- Voeg
AddProblemDetails,UseExceptionHandlerenUseStatusCodePagestoe en bekijk de body van een 404. - Schrijf
MaakProductValidatorenWijzigProductValidatoren koppel ze met.Valideer<T>(). - Geef 409 als ProblemDetails bij een dubbele productnaam (hoofdletterongevoelig).
- Maak
VoorraadTeLaagExceptionen eenIExceptionHandlerdie hem vertaalt naar 409.
Modelantwoord oefening 1
GET /v1/producten/999 geeft {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.5","title":"Not Found","status":404}
met Content-Type: application/problem+json.
Modelantwoord oefening 2
POST met {"naam":"","prijs":0,"inkoopprijs":5,"voorraad":-1} geeft 400 met vier velden in errors. De handler Maak wordt
niet aangeroepen: zet er een breakpoint in om het te zien.
Modelantwoord oefening 3
if (opslag.Alle().Any(p => p.Naam.Equals(req.Naam.Trim(), StringComparison.OrdinalIgnoreCase)))
return TypedResults.Problem(title: "Productnaam bestaat al", statusCode: 409);
Het returntype wordt Results<CreatedAtRoute<ProductResponse>, ProblemHttpResult>. In les 10 dwingt een unieke index in de database dit af.
Modelantwoord oefening 4
Zie DomeinFoutHandler hierboven. Registreer hem vóór AddProblemDetails() en zorg dat app.UseExceptionHandler() in de pipeline staat.
Lokaal oefenen
Geen live lab. Maak in je .http-bestand een blok "foute verzoeken": lege velden, kapotte JSON, verkeerde typen, dubbele namen.
Samenvatting
- Fouten als ProblemDetails (
application/problem+json); validatiefouten alsValidationProblemmeterrors. - FluentValidation in een endpoint filter houdt handlers schoon.
- Binding-fouten komen vóór validatie; onverwachte fouten worden een veilige 500.
- Volgende les: OpenAPI en het testen van je API.
📝 Quiz: Validatie en ProblemDetails
Dit was de laatste gratis les
Je hebt les 1–5 van ASP.NET Core Web API en EF Core gratis gevolgd. Wil je verder? Met één aankoop (€ 49,00, eenmalig) krijg je de overige 15 lessen, de live labs en het certificaat. Geen abonnement.