📌 Article principal de la série : C# et .NET : développement d’applications modernes pour développeurs
Ce tutoriel fait partie de la série C# et .NET. Le tutoriel Installer .NET 9 et créer son premier programme C# pas à pas est le prérequis direct.
ASP.NET Core est l’un des frameworks web les plus performants disponibles en 2025, cross-platform, open-source, et construit pour les containers et le cloud. Ce tutoriel construit une API REST complète avec ASP.NET Core 9 en utilisant l’approche Minimal APIs — la plus directe et la moins cérémonieuse pour les nouvelles APIs. On ajoutera Entity Framework Core avec SQLite pour la persistance des données, la validation des entrées, la gestion structurée des erreurs, et on testera tout avec curl. À la fin, vous disposerez d’une API REST production-ready avec persistance réelle en base de données.
Prérequis
- SDK .NET 9 installé (voir Installer .NET 9 et créer son premier programme C# pas à pas)
- Notions de base en C# : types, classes, collections, async/await
- curl installé pour tester l’API (inclus sur Linux, macOS et Windows 10+)
- Niveau : débutant à intermédiaire
- Temps estimé : 60 à 90 minutes
Étape 1 — Créer le projet ASP.NET Core Web API
ASP.NET Core propose deux styles de développement d’API : les Minimal APIs (introduites dans .NET 6), qui définissent les endpoints directement dans Program.cs avec un minimum de code, et les Controller-based APIs (MVC classique), qui organisent les endpoints dans des classes Controller séparées. Pour ce tutoriel, on utilise les Minimal APIs — elles sont parfaites pour apprendre les concepts fondamentaux sans infrastructure supplémentaire, et elles sont le choix recommandé par Microsoft pour les nouvelles APIs simples à moyennes en .NET 6+.
# Créer le projet ASP.NET Core avec le template webapi minimal
dotnet new webapi -n api-produits --use-minimal-apis
cd api-produits
# Voir les fichiers générés
ls -la
Le flag --use-minimal-apis génère un projet sans les classes Controller du MVC classique. La structure générée est la suivante :
api-produits/
├── api-produits.csproj ← Fichier projet MSBuild
├── Program.cs ← Point d'entrée + définition des endpoints
├── appsettings.json ← Configuration de l'application (prod)
├── appsettings.Development.json ← Configuration spécifique au développement
├── Properties/
│ └── launchSettings.json ← Configuration de lancement (ports, env vars)
└── obj/ ← Fichiers générés (ignorés par git)
Ouvrez Program.cs — le template génère un exemple avec des données météo. Nous allons remplacer ce contenu par notre API de produits. Avant cela, observez la structure : le fichier commence par var builder = WebApplication.CreateBuilder(args) (configuration des services), suivi de var app = builder.Build() (construction de l’application), puis la définition des endpoints et enfin app.Run(). Ce pattern builder est le cœur du démarrage d’une application ASP.NET Core.
Étape 2 — Ajouter Entity Framework Core et SQLite
Entity Framework Core (EF Core) est l’ORM officiel de Microsoft pour .NET. SQLite est une base de données relationnelle embarquée, stockée dans un seul fichier — idéale pour le développement et les tests car elle ne nécessite aucune installation de serveur de base de données séparé.
# Ajouter les paquets NuGet nécessaires
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
# Swashbuckle (Swagger UI) — non inclus par défaut depuis .NET 9
dotnet add package Swashbuckle.AspNetCore
# Installer l'outil de migration EF Core (global, une seule fois)
dotnet tool install --global dotnet-ef
# Vérifier l'installation de l'outil
dotnet ef --version
Microsoft.EntityFrameworkCore.Sqlite inclut EF Core et le provider SQLite. Microsoft.EntityFrameworkCore.Design est nécessaire pour les outils en ligne de commande dotnet ef (migrations, scaffolding). Swashbuckle.AspNetCore ajoute Swagger / Swagger UI — à partir de .NET 9, ce package n’est plus inclus par défaut dans le template webapi, il faut donc l’ajouter explicitement si vous voulez l’interface interactive à /swagger. L’outil global dotnet-ef s’installe une seule fois sur la machine et est utilisable dans tous les projets — vérifiez sa présence avec dotnet ef --version qui doit afficher 9.0.x. Si la commande n’est pas reconnue après installation, fermez et rouvrez le terminal ou ajoutez ~/.dotnet/tools au PATH.
Étape 3 — Créer le modèle et le DbContext
Créez un fichier Models.cs qui contient le modèle de données et le contexte EF Core. Regrouper ces éléments dans un fichier unique est acceptable pour un projet de taille petite à moyenne :
// Models.cs
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
namespace ApiProduits;
// Entité persistée en base de données
public class Produit
{
public int Id { get; set; }
[Required(ErrorMessage = "Le nom est obligatoire")]
[MaxLength(200, ErrorMessage = "Le nom ne doit pas dépasser 200 caractères")]
public string Nom { get; set; } = string.Empty;
[Range(0.01, double.MaxValue, ErrorMessage = "Le prix doit être positif")]
public decimal Prix { get; set; }
[Range(0, int.MaxValue, ErrorMessage = "Le stock ne peut pas être négatif")]
public int Stock { get; set; }
public DateTime CreeLe { get; set; } = DateTime.UtcNow;
}
// DTO pour la création/modification (séparé de l'entité)
public record ProduitDto(
[Required] string Nom,
[Range(0.01, double.MaxValue)] decimal Prix,
[Range(0, int.MaxValue)] int Stock
);
// Contexte EF Core : pont entre les classes C# et la base de données
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Produit> Produits { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Données initiales (seed data)
modelBuilder.Entity<Produit>().HasData(
// Date fixe : DateTime.UtcNow ici provoquerait une nouvelle migration à chaque build
new Produit { Id = 1, Nom = "Laptop ThinkPad X1", Prix = 850_000m, Stock = 10, CreeLe = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc) },
new Produit { Id = 2, Nom = "Souris USB ergonomique", Prix = 15_000m, Stock = 50, CreeLe = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc) },
new Produit { Id = 3, Nom = "Clé USB 64GB", Prix = 8_000m, Stock = 200, CreeLe = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc) }
);
}
}
La classe Produit est l’entité EF Core — EF Core la mappe sur une table SQLite. Les annotations [Required], [MaxLength] et [Range] viennent de System.ComponentModel.DataAnnotations et servent à la fois à la validation des entrées et à la génération du schéma de base de données. Le DTO ProduitDto (record positionnel) est l’objet que le client envoie dans le corps des requêtes POST/PUT — distinct de l’entité pour éviter que le client ne puisse forcer Id ou CreeLe. La méthode OnModelCreating avec HasData définit des données initiales insérées automatiquement lors de la migration.
Étape 4 — Configurer les services dans Program.cs
Remplacez le contenu de Program.cs par la configuration complète de l’application. On procède en deux phases : enregistrement des services (avant builder.Build()), puis définition des middlewares et endpoints (après Build()) :
// Program.cs
using ApiProduits;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// --- Enregistrement des services ---
// EF Core avec SQLite (fichier produits.db dans le répertoire du projet)
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlite("Data Source=produits.db"));
// Support de la validation des données (Data Annotations)
builder.Services.AddEndpointsApiExplorer();
// Swagger/OpenAPI (documentation interactive de l'API)
builder.Services.AddSwaggerGen();
var app = builder.Build();
// --- Middlewares ---
// Appliquer les migrations et seed data au démarrage (développement uniquement)
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.Migrate();
}
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(); // Interface interactive à /swagger
}
app.UseHttpsRedirection();
// --- Endpoints Minimal API ---
// GET /api/produits — Lister tous les produits
app.MapGet("/api/produits", async (AppDbContext db) =>
await db.Produits.OrderBy(p => p.Nom).ToListAsync())
.WithName("ListerProduits")
.WithTags("Produits");
// GET /api/produits/{id} — Récupérer un produit par ID
app.MapGet("/api/produits/{id:int}", async (int id, AppDbContext db) =>
await db.Produits.FindAsync(id) is Produit p
? Results.Ok(p)
: Results.NotFound(new { erreur = $"Produit #{id} introuvable" }))
.WithName("ObtenirProduit")
.WithTags("Produits");
// POST /api/produits — Créer un produit
app.MapPost("/api/produits", async (ProduitDto dto, AppDbContext db) =>
{
var produit = new Produit
{
Nom = dto.Nom.Trim(),
Prix = dto.Prix,
Stock = dto.Stock,
CreeLe = DateTime.UtcNow
};
db.Produits.Add(produit);
await db.SaveChangesAsync();
return Results.Created($"/api/produits/{produit.Id}", produit);
})
.WithName("CreerProduit")
.WithTags("Produits");
// PUT /api/produits/{id} — Modifier un produit
app.MapPut("/api/produits/{id:int}", async (int id, ProduitDto dto, AppDbContext db) =>
{
var produit = await db.Produits.FindAsync(id);
if (produit is null) return Results.NotFound(new { erreur = $"Produit #{id} introuvable" });
produit.Nom = dto.Nom.Trim();
produit.Prix = dto.Prix;
produit.Stock = dto.Stock;
await db.SaveChangesAsync();
return Results.Ok(produit);
})
.WithName("ModifierProduit")
.WithTags("Produits");
// DELETE /api/produits/{id} — Supprimer un produit
app.MapDelete("/api/produits/{id:int}", async (int id, AppDbContext db) =>
{
var produit = await db.Produits.FindAsync(id);
if (produit is null) return Results.NotFound(new { erreur = $"Produit #{id} introuvable" });
db.Produits.Remove(produit);
await db.SaveChangesAsync();
return Results.Ok(new { message = $"Produit #{id} supprimé" });
})
.WithName("SupprimerProduit")
.WithTags("Produits");
// GET /api/produits/recherche?nom=xxx — Recherche par nom
app.MapGet("/api/produits/recherche", async (string nom, AppDbContext db) =>
await db.Produits
.Where(p => p.Nom.ToLower().Contains(nom.ToLower()))
.ToListAsync())
.WithName("RechercherProduits")
.WithTags("Produits");
app.Run();
Décortiquons les éléments clés. builder.Services.AddDbContext<AppDbContext> enregistre le contexte EF Core dans le conteneur d’injection de dépendances — ASP.NET Core injecte automatiquement une instance scopée par requête HTTP dans chaque endpoint qui la déclare comme paramètre. Le bloc using (var scope = ...) applique les migrations de base de données au démarrage, créant le fichier SQLite produits.db et les tables si elles n’existent pas. La syntaxe await db.Produits.FindAsync(id) is Produit p ? Results.Ok(p) : Results.NotFound(...) utilise le pattern matching inline pour retourner le produit ou un 404. Results.Created retourne HTTP 201 avec l’URL du nouveau ressource dans l’en-tête Location — conforme aux standards REST. WithName et WithTags organisent la documentation Swagger générée automatiquement.
Étape 5 — Créer et appliquer la migration EF Core
EF Core utilise des migrations pour créer et modifier le schéma de base de données de façon contrôlée et reproductible. Une migration est une classe C# générée automatiquement qui décrit les changements à appliquer à la base de données.
# Générer la migration initiale
dotnet ef migrations add InitialCreate
# Vérifier les fichiers générés
ls Migrations/
La commande dotnet ef migrations add InitialCreate analyse le modèle EF Core (les classes DbSet dans AppDbContext) et génère un fichier de migration dans le dossier Migrations/. Ce fichier contient deux méthodes : Up() (créer les tables) et Down() (supprimer les tables — pour rollback). Examinez le fichier généré — vous verrez que EF Core a traduit les annotations [MaxLength] et [Required] en contraintes SQL. La migration sera appliquée automatiquement au démarrage de l’application grâce au bloc db.Database.Migrate() que nous avons ajouté dans Program.cs.
Étape 6 — Lancer et tester l’API
Lancez l’application et testez l’ensemble des endpoints. Le premier démarrage crée le fichier SQLite et applique la migration avec les données initiales :
# Lancer l'API
dotnet run
# Vérifier l'URL dans les logs (souvent http://localhost:5000 ou https://localhost:7000)
Dans les logs du terminal, repérez la ligne Now listening on: http://localhost:XXXX — c’est le port sur lequel l’API écoute. Le template webapi configure souvent HTTP sur le port 5000 et HTTPS sur 7000. Ouvrez http://localhost:5000/swagger dans votre navigateur pour accéder à l’interface Swagger — elle liste tous les endpoints et permet de les tester interactivement depuis le navigateur. Testez ensuite avec curl depuis un second terminal :
BASE="http://localhost:5000/api/produits"
echo "=== GET tous les produits ==="
curl -s $BASE | python3 -m json.tool
echo "=== GET produit #1 ==="
curl -s $BASE/1 | python3 -m json.tool
echo "=== POST créer un produit ==="
curl -s -X POST $BASE \
-H "Content-Type: application/json" \
-d '{"nom":"Moniteur 24 pouces","prix":180000,"stock":8}' \
| python3 -m json.tool
echo "=== PUT modifier produit #1 ==="
curl -s -X PUT $BASE/1 \
-H "Content-Type: application/json" \
-d '{"nom":"Laptop ThinkPad X1 Carbon","prix":920000,"stock":8}' \
| python3 -m json.tool
echo "=== GET recherche par nom ==="
curl -s "$BASE/recherche?nom=laptop" | python3 -m json.tool
echo "=== DELETE produit #3 ==="
curl -s -X DELETE $BASE/3 | python3 -m json.tool
echo "=== GET après suppression ==="
curl -s $BASE | python3 -m json.tool
La sortie du premier GET doit afficher les 3 produits de seed data. Le POST retourne le nouveau produit avec son Id généré (4) et CreeLe renseigné. Le PUT retourne le produit #1 avec le nouveau nom et prix. La recherche retourne uniquement le laptop. Le DELETE retourne le message de confirmation. Le GET final montre 3 produits (seed #3 supprimé, nouveau #4 ajouté). Si vous arrêtez et relancez l’application, les données sont toujours là — elles sont persistées dans produits.db, contrairement au dictionnaire en mémoire des exemples Flask ou Spring Boot de ce tutoriel. Ouvrez ce fichier avec un outil comme DB Browser for SQLite pour visualiser les tables directement.
Étape 7 — Ajouter la validation et la gestion globale des erreurs
Les Minimal APIs d’ASP.NET Core 9 supportent la validation automatique des DTOs marqués avec des Data Annotations via les filtres de validation. Ajoutez la validation et un gestionnaire d’erreurs global dans Program.cs, juste après var app = builder.Build() :
// Gestionnaire d'exceptions global (ajouter après builder.Build())
app.UseExceptionHandler(errorApp => errorApp.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new
{
erreur = "Une erreur interne s'est produite",
statut = 500
});
}));
// Filtre de validation pour les Minimal APIs (ajouter sur les endpoints POST/PUT)
// Dans app.MapPost, ajouter après WithTags("Produits") :
// .WithParameterValidation()
// Requiert le paquet : dotnet add package MinimalApis.Extensions
Pour les Minimal APIs ASP.NET Core 9, la validation automatique complète des DTOs nécessite soit d’utiliser les controllers MVC (qui l’intègrent nativement via [ApiController]), soit d’ajouter la validation manuellement dans chaque endpoint avec Validator.TryValidateObject, soit d’utiliser le paquet communautaire MinimalApis.Extensions. Pour la validation basique, ajoutez ce bloc dans chaque endpoint POST/PUT avant de persister :
// Validation manuelle dans app.MapPost :
app.MapPost("/api/produits", async (ProduitDto dto, AppDbContext db) =>
{
// Validation explicite
if (string.IsNullOrWhiteSpace(dto.Nom))
return Results.BadRequest(new { erreur = "Le nom est obligatoire" });
if (dto.Prix <= 0)
return Results.BadRequest(new { erreur = "Le prix doit être positif" });
if (dto.Stock < 0)
return Results.BadRequest(new { erreur = "Le stock ne peut pas être négatif" });
var produit = new Produit { Nom = dto.Nom.Trim(), Prix = dto.Prix, Stock = dto.Stock };
db.Produits.Add(produit);
await db.SaveChangesAsync();
return Results.Created($"/api/produits/{produit.Id}", produit);
});
Testez la validation avec curl :
# Test prix négatif
curl -s -X POST $BASE \
-H "Content-Type: application/json" \
-d '{"nom":"Test","prix":-100,"stock":1}' \
| python3 -m json.tool
# Test nom vide
curl -s -X POST $BASE \
-H "Content-Type: application/json" \
-d '{"nom":" ","prix":5000,"stock":1}' \
| python3 -m json.tool
Les deux appels doivent retourner une réponse 400 avec le message d’erreur correspondant. Le corps JSON clair ({"erreur": "Le prix doit être positif"}) permet au client de comprendre précisément ce qui n’a pas fonctionné — bien supérieur aux réponses d’erreur génériques qui ne facilitent pas le débogage.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
dotnet-ef: command not found |
Outil global non installé ou PATH non chargé | dotnet tool install --global dotnet-ef, puis relancer le terminal |
Unable to create a 'DbContext' lors de la migration |
EF Core ne trouve pas le DbContext dans le projet |
Vérifier que AppDbContext est dans le même assembly que Program.cs |
Swagger inaccessible à /swagger |
SwaggerUI non activé hors Development | Vérifier la variable d’environnement ASPNETCORE_ENVIRONMENT=Development |
| Port HTTPS (7000) en erreur de certificat | Certificat de développement non approuvé | dotnet dev-certs https --trust puis relancer; ou utiliser HTTP (5000) en dev |
| Seed data dupliquée à chaque démarrage | HasData avec EnsureCreated au lieu de migrations |
Utiliser uniquement db.Database.Migrate() — EF Core évite les doublons avec les migrations |
Tutoriels frères
- Installer .NET 9 et créer son premier programme C# pas à pas — Prérequis de ce tutoriel : SDK .NET, CLI dotnet, premier projet console
Pour aller plus loin
- 🔝 Retour à l’article principal : C# et .NET : développement d’applications modernes pour développeurs
- Documentation Minimal APIs — Microsoft Learn
- Getting Started avec EF Core — Microsoft Learn
- Migrations EF Core — Documentation officielle
- Spécification OpenAPI 3.x
FAQ
- Quand choisir Minimal APIs plutôt que les Controllers MVC ?
- Minimal APIs sont recommandées pour les APIs simples à moyennes avec peu d’endpoints, les microservices, les projets où la lisibilité du code de démarrage est prioritaire, et les équipes débutant avec ASP.NET Core. Les Controllers MVC sont préférables pour les APIs complexes avec beaucoup d’endpoints, les projets nécessitant des filtres globaux par controller, les équipes habituées au pattern MVC, et les applications qui servent à la fois une API et des vues HTML (Razor Pages ou MVC Views).
- Comment passer SQLite à PostgreSQL en production ?
- Remplacez
Microsoft.EntityFrameworkCore.SqliteparNpgsql.EntityFrameworkCore.PostgreSQL, changezUseSqliteenUseNpgsqldansProgram.cs, et mettez à jour la chaîne de connexion dansappsettings.json. Les migrations existantes restent valides — EF Core génère le SQL adapté au provider actif. Utilisez des profils de configuration (appsettings.Development.jsonpour SQLite,appsettings.Production.jsonpour PostgreSQL) pour éviter de modifier le code source. - ASP.NET Core supporte-t-il l’authentification JWT ?
- Oui, nativement. Ajoutez le paquet
Microsoft.AspNetCore.Authentication.JwtBearer, configurez l’authentification dansbuilder.Servicesavec votre clé secrète et les paramètres de validation du token, puis protégez les endpoints avec.RequireAuthorization()sur les Minimal APIs ou[Authorize]sur les controllers. ASP.NET Core supporte aussi OAuth 2.0/OIDC pour l’intégration avec des fournisseurs d’identité externes (Microsoft Entra ID, Google, Auth0).