MVAESTECH
Guide .NET · 06 · Applications

Publié le · par Mathieu Vaes

API REST avec ASP.NET Core

Une API est un contrat public. Sa qualité se juge sur la prévisibilité : codes HTTP justes, erreurs normalisées, versionnement, idempotence et comportement sous charge.

Chapitres du guide
En clair

Une API REST, c'est un guichet : le client envoie une demande HTTP (« donne-moi la commande 42 »), le serveur répond avec un code (200 = OK, 404 = introuvable…) et des données au format JSON. Avant d'arriver au code qui la traite, la demande passe une série de contrôles en file : sécurité, droits, quotas.

Le pipeline de middleware Junior

Chaque requête traverse les middlewares dans l'ordre d'enregistrement, atteint l'endpoint, puis la réponse remonte dans l'ordre inverse. L'ordre est sémantique : l'authentification doit précéder l'autorisation, la gestion d'erreurs doit envelopper tout le reste.

Kestrelou IIS ExceptionHandler HSTS /HTTPS Routing CORS AuthNqui ? AuthZa le droit ? RateLimiter Endpoint requête → ← réponse (ordre inverse) Un middleware peut court-circuiter : le rate limiter renvoie 429 sans jamais atteindre l'endpoint

Program.cs — ordre recommandé

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("orders:write", p => p.RequireClaim("scope", "orders.write"));
builder.Services.AddRateLimiter(o => o.AddFixedWindowLimiter("api", w =>
    { w.PermitLimit = 100; w.Window = TimeSpan.FromMinutes(1); }));
builder.Services.AddOpenApi();                 // OpenAPI intégré depuis .NET 9
builder.Services.AddHealthChecks().AddDbContextCheck<ShopDbContext>();

var app = builder.Build();

app.UseExceptionHandler();                     // 1. enveloppe tout
if (!app.Environment.IsDevelopment()) app.UseHsts();
app.UseHttpsRedirection();
app.UseCors("front");                          // avant l'auth pour les preflight
app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter();

app.MapOpenApi();
app.MapHealthChecks("/health/ready");
app.MapOrders();                               // groupes d'endpoints par fonctionnalité
app.Run();

Controllers, Minimal APIs ou FastEndpoints Junior

ApprocheForcesLimitesPour
Controllers MVCConnue de tous, filtres, conventionsContrôleurs qui grossissent, réflexionExistant, grosses équipes hétérogènes
Minimal APIsLégères, rapides, compatibles AOT, TypedResultsÀ organiser soi-même (MapGroup, extensions)Nouveaux services, vertical slices
FastEndpointsPattern REPR (un endpoint = une classe), validation intégréeDépendance tierceVertical slices structurées

Le contrat HTTP Junior

MéthodeSémantiqueIdempotenteSuccès typique
GETLireOui200, 304
POSTCréer, déclencher une actionNon → clé d'idempotence201 + Location, 202
PUTRemplacer entièrementOui200, 204
PATCHModifier partiellementPas garanti200, 204
DELETESupprimerOui204
CodeQuand
400Requête mal formée, validation échouée (avec errors par champ)
401 / 403Non authentifié / authentifié sans droit
404Ressource introuvable (ou masquée pour ne pas révéler son existence)
409Conflit d'état (commande déjà validée, doublon)
412If-Match ne correspond plus : modification concurrente
422Syntaxe correcte mais règle métier violée (choix d'équipe vs 400, à documenter)
429Quota dépassé, avec Retry-After
500 / 503Erreur interne (jamais de stack trace exposée) / indisponible temporairement

Erreurs normalisées : ProblemDetails (RFC 9457)

{
  "type": "https://api.shop.fr/errors/order-already-submitted",
  "title": "Commande déjà validée",
  "status": 409,
  "detail": "La commande 7f3c… ne peut plus être modifiée.",
  "instance": "/orders/7f3c…/lines",
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
public static class OrderEndpoints
{
    public static void MapOrders(this IEndpointRouteBuilder app)
    {
        var g = app.MapGroup("/v1/orders").WithTags("Orders").RequireAuthorization();

        g.MapGet("/{id:guid}", async Task<Results<Ok<OrderDto>, NotFound>> (
            Guid id, IOrderQueries q, CancellationToken ct) =>
            await q.GetAsync(id, ct) is { } dto ? TypedResults.Ok(dto) : TypedResults.NotFound());

        g.MapPost("/", async Task<Results<Created<OrderDto>, ValidationProblem>> (
            CreateOrder cmd, ISender sender, CancellationToken ct) =>
        {
            var dto = await sender.Send(cmd, ct);
            return TypedResults.Created($"/v1/orders/{dto.Id}", dto);
        }).RequireAuthorization("orders:write").RequireRateLimiting("api");
    }
}

Points de conception

  • Versionnement : dans l'URL (/v1/) pour la lisibilité, ou par en-tête ; package Asp.Versioning.Http. Ne jamais casser un contrat publié.
  • Pagination : par curseur (?after=…&limit=50) pour les gros volumes, l'offset devient lent et instable quand les données bougent.
  • Concurrence optimiste : renvoyer un ETag, exiger If-Match sur PUT/PATCH, répondre 412 en cas de conflit.
  • Idempotence des POST : en-tête Idempotency-Key stocké avec la réponse ; un rejeu renvoie la même réponse sans retraiter.
  • DTO dédiés : ne jamais exposer les entités EF (sur-exposition, boucles de sérialisation, couplage au schéma).

Injection de dépendances : les durées de vie Confirmé

DuréeInstanceExemples
SingletonUne pour toute l'appli — doit être thread-safeConfiguration, cache, HttpClient via factory, clients Pub/Sub
ScopedUne par requête HTTP (ou par scope créé)DbContext, unité de travail, utilisateur courant
TransientNouvelle à chaque résolutionServices légers sans état
Piège
  • Dépendance captive : un singleton qui reçoit un service scoped (un DbContext) le garde vivant pour toujours, partagé entre requêtes. Activez ValidateScopes et ValidateOnBuild (actifs par défaut en Development). Dans un BackgroundService, créez un scope avec IServiceScopeFactory.
  • new HttpClient() par appel : épuisement des sockets. Un HttpClient statique éternel : DNS jamais rafraîchi. Utilisez IHttpClientFactory (clients typés).
  • Options non validées : une clé de config manquante découverte au premier appel en production. .ValidateDataAnnotations().ValidateOnStart().

À retenir

  • Ordre des middlewares = comportement : erreurs, HTTPS, routing, CORS, authN, authZ, rate limit.
  • Codes HTTP exacts, erreurs en ProblemDetails, POST rendus idempotents.
  • Scoped pour le DbContext, jamais injecté dans un singleton.

Un projet .NET à cadrer ou à reprendre ?

API REST documentées et testées, applications ASP.NET MVC (y compris VB.NET), migration de services anciens vers une architecture claire : parlons de votre code.