Les 4 · Module 1 · API-basis
DTO's en model binding
Scheid wat de API naar buiten laat zien van je domeinmodel: request- en response-DTO's, mapping, model binding uit route, query, body en headers, en overposting voorkomen.
đ¯ Na deze les kun je
- uitleggen waarom je DTO's gebruikt in plaats van domeinobjecten
- request- en response-DTO's ontwerpen voor Winkel.Api
- parameters binden uit route, query, body, header en services
- overposting herkennen en voorkomen
Tot nu toe ontvangt en verstuurt de API Product zelf. Dat lijkt handig, maar zodra het domein groeit (een interne inkoopprijs,
een voorraadteller, relaties) lekt alles naar buiten en kan een client velden zetten die hij niet hoort te zetten. DTO's
(data transfer objects) zijn aparte klassen die precies beschrijven wat er over de lijn gaat.
Het domein groeit
// Domein/Product.cs
public class Product
{
public int Id { get; set; }
public required string Naam { get; set; }
public decimal Prijs { get; set; }
public decimal Inkoopprijs { get; set; } // geheim voor klanten
public int Voorraad { get; set; }
public bool Actief { get; set; } = true;
public DateTime AangemaaktOp { get; set; }
}
DTO's
// Contracten/ProductContracten.cs
public record ProductResponse(int Id, string Naam, decimal Prijs, bool OpVoorraad);
public record MaakProductRequest(string Naam, decimal Prijs, decimal Inkoopprijs, int Voorraad);
public record WijzigProductRequest(string Naam, decimal Prijs, bool Actief);
public static class ProductMapping
{
public static ProductResponse NaarResponse(this Product p) => new(p.Id, p.Naam, p.Prijs, p.Voorraad > 0);
public static Product NaarDomein(this MaakProductRequest r) => new()
{
Naam = r.Naam.Trim(), Prijs = r.Prijs, Inkoopprijs = r.Inkoopprijs, Voorraad = r.Voorraad,
AangemaaktOp = DateTime.UtcNow,
};
}
static CreatedAtRoute<ProductResponse> Maak(MaakProductRequest req, ProductOpslag opslag)
{
var product = opslag.Voeg(req.NaarDomein()); // opslag kiest het id
return TypedResults.CreatedAtRoute(product.NaarResponse(), "ProductOphalen", new { id = product.Id });
}
- De client stuurt geen
Iden geenAangemaaktOpmeer: die bepaalt de server. ProductResponsetoontOpVoorraad(ja/nee) in plaats van het exacte aantal en nooit de inkoopprijs.- Mapping met de hand in extensiemethodes is duidelijk en snel. Bibliotheken zoals Mapster of AutoMapper kunnen het ook, maar verbergen fouten (een vergeten veld) achter conventies.
Voorspel eerst: zonder DTO stuurt een client {"naam":"Muis","prijs":1,"inkoopprijs":0,"actief":true,"id":1} naar
PUT /v1/producten/1. Wat kan er misgaan?
Uitkomst
Alles wat in Product staat, kan de client overschrijven: de inkoopprijs, de voorraad, zelfs het id (en daarmee misschien een ander
product). Dat heet overposting (of mass assignment). Met WijzigProductRequest kan hij alleen naam, prijs en actief
wijzigen; andere velden in de JSON worden genegeerd.
Model binding: waar komt een parameter vandaan?
| Bron | Standaard voor | Expliciet |
|---|---|---|
| route | parameter met dezelfde naam als {id} |
[FromRoute] |
| query | eenvoudige types die niet in de route staan | [FromQuery] |
| body | ÊÊn complex type (JSON) | [FromBody] |
| header | â | [FromHeader(Name = "X-Kanaal")] |
| services | geregistreerde types | [FromServices] (meestal automatisch) |
| formulier | â | [FromForm] (uploads) |
Meerdere queryparameters bundelen:
public record ProductFilter(string? Zoek, decimal? MaxPrijs, int Pagina = 1);
groep.MapGet("/", ([AsParameters] ProductFilter filter, ProductOpslag opslag) => ...);
[AsParameters] leest elke eigenschap als losse parameter: ?zoek=muis&maxPrijs=30&pagina=2.
JSON-instellingen
builder.Services.ConfigureHttpJsonOptions(o =>
{
o.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); // enums als tekst
o.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});
Enums als tekst ("status": "Verzonden") zijn leesbaarder en breken minder snel dan getallen.
Typische fouten
- Body is
nullof velden zijn leeg: de JSON-namen kloppen niet (product_naamvsnaam) ofContent-Typeontbreekt. InvalidOperationException: Body was inferred but the method does not allow inferred body parameters: een complex type op een GET. Gebruik[AsParameters]of[FromQuery].- Inkoopprijs zichtbaar in de API: je gaf ergens nog het domeinobject terug. Zoek op
TypedResults.Ok(p)met eenProduct.
Oefenen
- Breid
Productuit zoals hierboven en voer de drie DTO's en de mapping in voor alle endpoints. - Maak
PUT /v1/producten/{id}metWijzigProductRequesten laat zien dat een meegestuurdinkoopprijsgenegeerd wordt. - Voeg
ProductFiltertoe met[AsParameters]en filter op zoekterm en maximumprijs. - Lees een optionele header
X-Kanaal(webofapp) en log hem.
Modelantwoord oefening 1
Elk endpoint geeft nu ProductResponse of een lijst daarvan. Zoek ter controle: grep -rn "Ok<Product>" Endpoints/ geeft niets.
Modelantwoord oefening 2
static Results<Ok<ProductResponse>, NotFound> Wijzig(int id, WijzigProductRequest req, ProductOpslag opslag)
{
if (opslag.Zoek(id) is not { } p) return TypedResults.NotFound();
p.Naam = req.Naam.Trim(); p.Prijs = req.Prijs; p.Actief = req.Actief;
return TypedResults.Ok(p.NaarResponse());
}
System.Text.Json negeert onbekende velden standaard, dus inkoopprijs in de body heeft geen effect.
Modelantwoord oefening 3
var q = opslag.Alle().AsEnumerable();
if (!string.IsNullOrWhiteSpace(filter.Zoek)) q = q.Where(p => p.Naam.Contains(filter.Zoek, StringComparison.OrdinalIgnoreCase));
if (filter.MaxPrijs is { } max) q = q.Where(p => p.Prijs <= max);
return TypedResults.Ok(q.Select(p => p.NaarResponse()).ToList());
Modelantwoord oefening 4
groep.MapGet("/", ([AsParameters] ProductFilter f, [FromHeader(Name = "X-Kanaal")] string? kanaal, ILogger<Program> log, ProductOpslag opslag) =>
{
log.LogInformation("Productlijst via {Kanaal}", kanaal ?? "onbekend");
...
});
Lokaal oefenen
Geen live lab. Stuur met je .http-bestand bewust extra en foute velden mee en kijk wat de API ermee doet.
Samenvatting
- DTO's bepalen wat er over de lijn gaat; het domein blijft binnen.
- Mapping in extensiemethodes is expliciet; overposting is met request-DTO's opgelost.
- Model binding haalt parameters uit route, query, body, header of services;
[AsParameters]bundelt. - Volgende les: validatie en ProblemDetails.