Minimal APIs + EF Core : une API .NET 8 propre
Controller , le routage par attributs et une bonne part de la plomberie de liaison. Un endpoint devient une méthode qui reçoit ses dépendances en paramètres. Le risque est connu : sans découpage, tout finit empilé dans Program.cs . La suite montre une API adossée à EF Core qui reste lisible et testable à mesure qu'elle grossit.
Découper en groupes de routes
Program.cs , il ne reste qu'un app.MapTodos(); par ressource. WithTags alimente la documentation, WithOpenApi enrichit chaque endpoint généré. Le groupe accepte aussi un AddEndpointFilter qui s'applique d'un coup à toutes ses routes, ce qui sert pour l'autorisation ou la validation commune.
{id:int} du template travaille dès le routage : une requête /todos/abc ne trouve aucune route qui matche et repart en 404 sans jamais atteindre le handler. Poser les contraintes dans le template plutôt que dans le corps évite un int.TryParse défensif à l'entrée de chaque méthode.
Résultats typés
Results.Ok(...) renvoie un IResult opaque. TypedResults.Ok(...) renvoie un Ok<T> concret, et cette différence débloque les unions de résultats. Une signature comme Results<Ok<TodoDto>, NotFound> déclare les deux issues possibles : le compilateur vérifie que le handler ne retourne rien d'autre, et OpenAPI publie les deux codes HTTP sans le moindre attribut [ProducesResponseType] .
401 par exemple, se fait en élargissant la signature de retour, et le corps du handler cesse de compiler tant que ce cas n'est pas traité. C'est une contrainte utile : le contrat HTTP vit dans le type, pas dans un commentaire.
Liaison de modèle et validation
int id qui correspond à un segment de route vient de l'URL, un type primitif sans correspondance vient de la query string, un type complexe vient du corps JSON, et un service enregistré dans le conteneur est injecté directement. Quand la signature s'allonge, [AsParameters] regroupe plusieurs paramètres dans un struct dédié.
[Required] posé sur une propriété du DTO est ignoré au binding. Deux voies : valider à la main dans le handler, comme ci-dessous, ou brancher un AddEndpointFilter qui inspecte l'argument via context.GetArgument<CreateTodoRequest>(0) et renvoie un ValidationProblem sans appeler next quand le modèle est invalide. FluentValidation se greffe au même endroit.
TypedResults.ValidationProblem répond un 400 au format ProblemDetails (RFC 7807), le même corps que produirait automatiquement un contrôleur annoté [ApiController] . Le client voit donc la même structure d'erreur, qu'il tape une route Minimal API ou un contrôleur classique.
EF Core : DbContext, requêtes, migrations
AddDbContext<AppDbContext> enregistre le contexte avec une durée de vie scoped : une instance par requête HTTP, injectée dans le handler comme n'importe quel service. Ce choix n'est pas cosmétique : un DbContext n'est pas thread-safe et ne doit pas être partagé entre requêtes, et la portée scoped garantit exactement cette isolation. Pour des débits élevés, AddDbContextPool recycle les instances au lieu d'en allouer une neuve à chaque appel.
AsNoTracking désactive le suivi des changements : EF Core ne construit pas de snapshots pour comparer plus tard, ce qui allège les requêtes qui ne font que renvoyer des données. Le Select vers un record TodoDto va plus loin : il limite les colonnes réellement chargées au lieu de matérialiser l'entité entière avant de la mapper en mémoire. C'est la différence entre un SELECT Id, Title, IsDone et un SELECT * suivi d'une projection côté client.
db.Todos.Add(entity) puis un seul SaveChangesAsync valident la transaction. EF Core émet l' INSERT et récupère la clé générée dans entity.Id , disponible aussitôt pour construire l'URL du Created .
dotnet ef migrations add InitialCreate produit un fichier versionné, appliqué au démarrage avec db.Database.MigrateAsync() . EnsureCreated crée bien les tables mais ignore complètement l'historique de migrations, ce qui rend la première vraie migration impossible ensuite ; à réserver aux bases jetables. Le workflow complet est décrit dans le guide des migrations EF Core .
Minimal API ou contrôleur
[ApiController] et sa validation automatique, sur les filtres MVC (action, résultat, exception), sur le model binding riche des formulaires, ou sur des conventions d'équipe déjà en place.
Garder le tout testable
AppDbContext sur le provider SQLite in-memory, on appelle le handler, on inspecte le résultat. TypedResults aide là aussi, puisque le type de retour concret expose directement le StatusCode et la valeur, sans désérialiser une réponse.
WebApplicationFactory<Program> démarre l'application en mémoire et laisse frapper les vrais endpoints via un HttpClient , filtres et liaison compris. Avec des top-level statements, la classe Program générée est interne : un public partial class Program { } en fin de fichier suffit à la rendre visible depuis le projet de test.
Une Minimal API bien tenue n'a rien d'un prototype. Le style enlève la cérémonie héritée des contrôleurs ; la conception, elle, reste entièrement à votre charge.