Les 3 · Module 1 · API-basis
Minimal API's of controllers
Vergelijk minimal API's en controllers voor Winkel.Api: route-groepen en TypedResults, of ApiController met attribuutroutes, en een structuur die meegroeit.
๐ฏ Na deze les kun je
- dezelfde endpoints met minimal API's en met een controller schrijven
- route-groepen en TypedResults gebruiken
- endpoints in aparte bestanden met extensiemethodes organiseren
- beargumenteren wanneer je welke stijl kiest
ASP.NET Core heeft twee manieren om een API te schrijven: minimal API's (app.MapGet(...), zoals in les 1) en
controllers (klassen met [HttpGet]-attributen). Ze draaien op dezelfde basis en kunnen naast elkaar bestaan. In deze les
schrijf je hetzelfde endpoint op beide manieren, en kies je een structuur voor de rest van de cursus.
Minimal API's, netjes georganiseerd
Program.cs met tachtig MapGet-regels is onleesbaar. Zet endpoints per resource in een eigen bestand:
// Endpoints/ProductEndpoints.cs
public static class ProductEndpoints
{
public static RouteGroupBuilder MapProducten(this IEndpointRouteBuilder app)
{
var groep = app.MapGroup("/v1/producten").WithTags("Producten");
groep.MapGet("/", Lijst);
groep.MapGet("/{id:int}", EenProduct).WithName("ProductOphalen");
groep.MapPost("/", Maak);
return groep;
}
static Ok<List<Product>> Lijst(ProductOpslag opslag) => TypedResults.Ok(opslag.Alle());
static Results<Ok<Product>, NotFound> EenProduct(int id, ProductOpslag opslag) =>
opslag.Zoek(id) is { } p ? TypedResults.Ok(p) : TypedResults.NotFound();
static CreatedAtRoute<Product> Maak(Product nieuw, ProductOpslag opslag)
{
opslag.Voeg(nieuw);
return TypedResults.CreatedAtRoute(nieuw, "ProductOphalen", new { id = nieuw.Id });
}
}
// Program.cs
builder.Services.AddSingleton<ProductOpslag>();
var app = builder.Build();
app.MapProducten();
app.Run();
TypedResultsin plaats vanResults: het returntype (Results<Ok<Product>, NotFound>) zegt precies welke antwoorden mogelijk zijn. OpenAPI (les 6) en unit-tests lezen dat.MapGroupdeelt het voorvoegsel, tags en later ook autorisatie en filters.ProductOpslagis een eenvoudige klasse die de lijst beheert; de container (les 7) geeft hem aan elke handler.
Hetzelfde als controller
[ApiController]
[Route("v1/producten")]
public class ProductenController(ProductOpslag opslag) : ControllerBase
{
[HttpGet]
public ActionResult<List<Product>> Lijst() => opslag.Alle();
[HttpGet("{id:int}", Name = "ProductOphalenCtrl")]
public ActionResult<Product> EenProduct(int id) => opslag.Zoek(id) is { } p ? p : NotFound();
[HttpPost]
public ActionResult<Product> Maak(Product nieuw)
{
opslag.Voeg(nieuw);
return CreatedAtRoute("ProductOphalenCtrl", new { id = nieuw.Id }, nieuw);
}
}
builder.Services.AddControllers();
...
app.MapControllers();
[ApiController] zet handige gedragingen aan: automatische 400 bij ongeldige invoer (les 5), body-binding zonder [FromBody],
en ProblemDetails bij fouten.
Voorspel eerst: je registreert beide versies tegelijk op /v1/producten. Wat gebeurt er bij GET /v1/producten?
Uitkomst
Een exception: AmbiguousMatchException: The request matched multiple endpoints. Twee endpoints met dezelfde methode en route
kunnen niet naast elkaar bestaan. Kies per route รฉรฉn stijl.
Vergelijking
| Minimal API's | Controllers | |
|---|---|---|
| Opzet | functies, weinig ceremonie | klassen, attributen |
| Prestaties | iets sneller, AOT-vriendelijk | iets meer overhead |
| Filters | endpoint filters | action filters, rijk ecosysteem |
| Model validatie | zelf of met een filter (les 5) | automatisch met [ApiController] |
| Bekend bij teams | nieuwer | veel bestaande code en voorbeelden |
Beide zijn prima. Deze cursus gebruikt minimal API's in groepen per resource: kort, expliciet en de richting waarin ASP.NET Core zich ontwikkelt. Kom je in een team met controllers, dan herken je alles.
Mappenstructuur
Winkel.Api/
โโโ Program.cs
โโโ Endpoints/ ProductEndpoints.cs, BestellingEndpoints.cs
โโโ Domein/ Product.cs, Bestelling.cs
โโโ Contracten/ DTO's (les 4)
โโโ Data/ DbContext (les 8)
โโโ Diensten/ ProductOpslag.cs, ...
Typische fouten
AmbiguousMatchException: twee endpoints op dezelfde route en methode.- Controller wordt niet gevonden:
AddControllers()ofMapControllers()vergeten, of de klasse is nietpublic. CreatedAtRoutefaalt met "No route matches": de naam inWithName/Name =klopt niet.
Oefenen
- Verplaats de endpoints uit les 2 naar
ProductEndpointsmetTypedResultsen eenProductOpslag-singleton. - Voeg
DELETE /v1/producten/{id}toe met returntypeResults<NoContent, NotFound>. - Schrijf dezelfde DELETE als controller-actie (in een tijdelijke
/v1/ctrl/producten) en vergelijk de hoeveelheid code. - Maak een unit-test (zonder webserver) die
EenProduct(99, opslag)aanroept en controleert dat het resultaatNotFoundis.
Modelantwoord oefening 1
public class ProductOpslag
{
private readonly List<Product> _items = [new(1, "Toetsenbord", 39.95m), new(2, "Muis", 19.95m)];
private readonly object _slot = new();
public List<Product> Alle() { lock (_slot) return [.. _items]; }
public Product? Zoek(int id) { lock (_slot) return _items.FirstOrDefault(p => p.Id == id); }
public void Voeg(Product p) { lock (_slot) _items.Add(p); }
public bool Verwijder(int id) { lock (_slot) return _items.RemoveAll(p => p.Id == id) > 0; }
}
Het lock is nodig: een singleton wordt door gelijktijdige verzoeken gedeeld.
Modelantwoord oefening 2
static Results<NoContent, NotFound> Verwijder(int id, ProductOpslag opslag) =>
opslag.Verwijder(id) ? TypedResults.NoContent() : TypedResults.NotFound();
Modelantwoord oefening 3
[HttpDelete("{id:int}")]
public IActionResult Verwijder(int id) => opslag.Verwijder(id) ? NoContent() : NotFound();
Bijna even kort; het verschil zit vooral in de klasse eromheen en de attributen.
Modelantwoord oefening 4
[Fact]
public void Onbekend_product_geeft_NotFound()
{
var resultaat = ProductEndpoints.EenProduct(99, new ProductOpslag());
Assert.IsType<NotFound>(resultaat.Result);
}
Maak EenProduct hiervoor internal en zet InternalsVisibleTo voor het testproject, of public. Met TypedResults kun je
handlers testen als gewone functies.
Lokaal oefenen
Geen live lab. Controleer met je .http-bestand dat alle endpoints nog werken na het verhuizen.
Samenvatting
- Minimal API's en controllers zijn gelijkwaardig; kies per route รฉรฉn stijl.
MapGroup+ extensiemethodes per resource houdenProgram.csklein;TypedResultsmaakt antwoorden expliciet en testbaar.- Volgende les: DTO's en model binding.