Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1021 minIntermédiaire

Entity Framework Core avec MySQL

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • dire ce qu'apporte un ORM et quand préférer le SQL écrit à la main ;
  • choisir un fournisseur MySQL compatible avec EF Core 10 ;
  • décrire le modèle dans un DbContext ;
  • lire et modifier les données avec LINQ et SaveChanges ;
  • faire évoluer la base avec les migrations.

4 exercices corrigés

Entity Framework Core (EF Core) est l'ORM de Microsoft pour .NET : un outil qui fait correspondre des classes C# à des tables, et qui écrit à ta place le SQL des lectures et des écritures. Tu interroges la base avec LINQ (db.Patients.Where(p => p.Nom.StartsWith("Du"))), EF Core traduit en SELECT paramétré, exécute, et te rend des objets. Ce chapitre installe EF Core 10 avec MySQL, décrit le modèle de GSB Ordonnances et montre ce que l'ORM simplifie, et ce qu'il cache.

ADO.NET ou ORM ?

Le DAO du chapitre précédent écrit chaque requête et recopie chaque colonne à la main. EF Core supprime ce travail répétitif, mais ajoute une couche à comprendre.

ADO.NET (MySqlConnector)EF Core
SQLÉcrit par toi, visibleGénéré à partir de LINQ
Recopie colonne → propriétéÀ la main (GetString("nom"))Automatique
JointuresÉcrites en SQLInclude ou navigation (o.Patient.Nom)
Suivi des modificationsAucun : un UPDATE par modificationSaveChanges enregistre ce qui a changé
Évolution du schémaScripts SQLMigrations générées
PerformanceMaximale, maîtriséeTrès bonne, à surveiller (requêtes générées)
À l'examenAttendu sur papier (requêtes paramétrées)Bonus dans ta réalisation, à savoir expliquer

Les deux coexistent souvent dans une même application : EF Core pour le CRUD courant, une requête SQL écrite à la main pour un rapport complexe. Au BTS, commence par ADO.NET pour comprendre ce qui se passe, puis passe à EF Core si ton projet s'y prête : la commission te demandera de lire le SQL que génère ton ORM.

Choisir le fournisseur MySQL

EF Core parle à chaque SGBD par un fournisseur (provider). Pour MySQL, la situation en novembre 2026 est la suivante :

PaquetÉditeurEF Core 10Remarque
MySql.EntityFrameworkCore 10.0OracleOuiFournisseur officiel de MySQL ; s'appuie sur MySql.Data ; méthode UseMySQL
Pomelo.EntityFrameworkCore.MySql 9.0CommunautéPas de version stableLe plus utilisé jusqu'à EF Core 9, s'appuie sur MySqlConnector, gère aussi MariaDB

Ce cours utilise MySql.EntityFrameworkCore 10.0, le seul fournisseur stable pour EF Core 10 au moment où ce cours est écrit. Vérifie sur NuGet avant de commencer : si Pomelo publie une version compatible EF Core 10, elle sera un excellent choix, surtout avec MariaDB, que le fournisseur d'Oracle ne cible pas officiellement. Les versions des paquets EF Core doivent toutes avoir le même numéro majeur que le fournisseur, sinon la restauration NuGet signale un conflit (avertissements NU1107 ou NU1608).

bash
dotnet add Gsb.Donnees package MySql.EntityFrameworkCore
dotnet add Gsb.Donnees package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-ef

Le second paquet et l'outil dotnet-ef servent aux migrations.

Décrire le modèle : le DbContext

Le DbContext représente une session avec la base. Il expose une propriété DbSet<T> par table et décrit, dans OnModelCreating, comment les classes correspondent aux tables. Les classes du métier (Patient, Medecin, Ordonnance) servent directement d'entités : elles ne dépendent pas d'EF Core.

csharp
using Gsb.Metier;
using Microsoft.EntityFrameworkCore;

namespace Gsb.Donnees;

public class GsbContext(DbContextOptions<GsbContext> options) : DbContext(options)
{
    public DbSet<Patient> Patients => Set<Patient>();
    public DbSet<Medecin> Medecins => Set<Medecin>();
    public DbSet<Ordonnance> Ordonnances => Set<Ordonnance>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Patient>(e =>
        {
            e.ToTable("patient");
            e.HasKey(p => p.NumPatient);
            e.Property(p => p.Nom).HasMaxLength(50).IsRequired();
            e.Property(p => p.Prenom).HasMaxLength(50);
            e.Property(p => p.DateNaissance).HasColumnType("date");
            e.Property(p => p.NumeroSecu).HasMaxLength(13).IsFixedLength();
            e.HasIndex(p => p.NumeroSecu).IsUnique();
        });

        modelBuilder.Entity<Medecin>(e =>
        {
            e.ToTable("medecin");
            e.HasKey(m => m.NumMedecin);
            e.Property(m => m.Nom).HasMaxLength(50);
            e.Ignore(m => m.NomComplet);   // propriété calculée, pas de colonne
        });

        modelBuilder.Entity<Ordonnance>(e =>
        {
            e.ToTable("ordonnance");
            e.HasKey(o => o.NumOrdonnance);
            e.HasOne(o => o.Patient).WithMany().HasForeignKey(o => o.NumPatient)
                .OnDelete(DeleteBehavior.Restrict);
            e.HasOne(o => o.Medecin).WithMany().HasForeignKey(o => o.NumMedecin)
                .OnDelete(DeleteBehavior.Restrict);
            e.OwnsMany(o => o.Lignes, l =>
            {
                l.ToTable("contenir");
                l.WithOwner().HasForeignKey("NumOrdonnance");
                l.HasKey("NumOrdonnance", nameof(LigneOrdonnance.CodeMedicament));
            });
        });
    }
}
InstructionSens
ToTable("patient")Le nom de la table. Sans lui, EF prendrait le nom du DbSet (Patients)
HasKeyLa clé primaire. Une propriété nommée Id ou PatientId serait reconnue seule ; NumPatient ne l'est pas
HasMaxLength, IsRequiredTaille et NOT NULL, utilisés par les migrations et la validation
HasIndex(…).IsUnique()L'index unique sur le numéro de sécurité sociale
HasOne(…).WithMany().HasForeignKey(…)La clé étrangère : une ordonnance a un patient, un patient a plusieurs ordonnances
OnDelete(DeleteBehavior.Restrict)Interdit de supprimer un patient qui a des ordonnances
OwnsMany(o => o.Lignes, …)Les lignes appartiennent à l'ordonnance, rangées dans contenir avec une clé composée

Les noms de colonnes de C# (NumPatient) et de la base (numPatient) ne diffèrent que par la casse. MySQL ne tient pas compte de la casse des noms de colonnes, donc la correspondance fonctionne. Pour les noms de tables, la casse compte sur un serveur Linux : d'où le ToTable("patient") en minuscules.

Configurer la connexion

Le contexte reçoit ses options par le constructeur. On les construit une fois, au démarrage, à partir de la chaîne de connexion :

csharp
DbContextOptions<GsbContext> options = new DbContextOptionsBuilder<GsbContext>()
    .UseMySQL(chaine)   // SQL en majuscules : c'est le nom de la méthode du fournisseur d'Oracle
    .Options;

Un contexte par opération. Un DbContext n'est pas prévu pour vivre longtemps : il garde en mémoire tous les objets qu'il a chargés, ne voit pas les modifications faites par les autres postes, et ne supporte pas deux requêtes en même temps. Dans une application WinForms, crée un contexte pour chaque opération (ou pour chaque fenêtre de saisie), et libère-le avec await using.

Voici le dépôt EF Core qui remplace PatientDao. Il implémente la même interface : ni le service ni les formulaires ne changent.

csharp
public class EfPatientRepository(DbContextOptions<GsbContext> options) : IPatientRepository
{
    public async Task<List<Patient>> ListerAsync()
    {
        await using var db = new GsbContext(options);
        return await db.Patients
            .AsNoTracking()
            .OrderBy(p => p.Nom).ThenBy(p => p.Prenom)
            .ToListAsync();
    }

    public async Task<Patient?> TrouverAsync(int numPatient)
    {
        await using var db = new GsbContext(options);
        return await db.Patients.FindAsync(numPatient);
    }

    public async Task<bool> NumeroSecuExisteAsync(string numeroSecu, int saufNumPatient)
    {
        await using var db = new GsbContext(options);
        return await db.Patients.AnyAsync(p => p.NumeroSecu == numeroSecu && p.NumPatient != saufNumPatient);
    }

    public async Task<int> AjouterAsync(Patient patient)
    {
        await using var db = new GsbContext(options);
        db.Patients.Add(patient);
        await db.SaveChangesAsync();   // INSERT, puis NumPatient reçoit la valeur AUTO_INCREMENT
        return patient.NumPatient;
    }

    public async Task ModifierAsync(Patient patient)
    {
        await using var db = new GsbContext(options);
        db.Patients.Update(patient);   // marque toutes les colonnes comme modifiées
        await db.SaveChangesAsync();
    }

    public async Task SupprimerAsync(int numPatient)
    {
        await using var db = new GsbContext(options);
        await db.Patients.Where(p => p.NumPatient == numPatient).ExecuteDeleteAsync();
    }
}

Dans Program.cs, une seule ligne change : new PatientService(new EfPatientRepository(options)).

Lire avec LINQ

Une requête EF Core s'écrit avec les méthodes LINQ ; elle n'est envoyée à la base qu'au moment où l'on demande le résultat (ToListAsync, FirstAsync, CountAsync, AnyAsync…).

csharp
await using var db = new GsbContext(options);

// les ordonnances d'octobre, avec leur patient et leur médecin
List<Ordonnance> recentes = await db.Ordonnances
    .Include(o => o.Patient)
    .Include(o => o.Medecin)
    .Where(o => o.DateEmission >= new DateTime(2026, 10, 1))
    .OrderByDescending(o => o.DateEmission)
    .ToListAsync();

Include charge les objets liés dans la même requête (une jointure). Sans lui, o.Patient vaudrait null. Les lignes de l'ordonnance, déclarées avec OwnsMany, sont chargées automatiquement.

Pour un écran de liste, inutile de charger des entités complètes : une projection avec Select ne lit que les colonnes utiles, directement dans le DTO.

csharp
List<OrdonnanceLisible> lisibles = await db.Ordonnances
    .OrderByDescending(o => o.DateEmission)
    .Select(o => new OrdonnanceLisible(o.NumOrdonnance, o.DateEmission,
        o.Patient!.Nom + " " + (o.Patient.Prenom ?? ""), "Dr " + o.Medecin!.Nom))
    .ToListAsync();

EF Core traduit la navigation o.Patient!.Nom en jointure. Avec le jeu d'essai, la liste contient l'ordonnance 2 (Petit Léa, Dr Nguyen), puis l'ordonnance 1 (Dupont Marie, Dr Martin).

SQLLINQ avec EF Core
WHERE nom = 'Dupont'.Where(p => p.Nom == "Dupont")
WHERE nom LIKE 'Du%'.Where(p => p.Nom.StartsWith("Du"))
ORDER BY nom, prenom.OrderBy(p => p.Nom).ThenBy(p => p.Prenom)
SELECT COUNT(*).CountAsync()
LIMIT 10.Take(10)
GROUP BY + COUNT.GroupBy(o => o.Medecin!.Nom).Select(g => new { g.Key, Nombre = g.Count() })
NOT EXISTS (…).Where(p => !db.Ordonnances.Any(o => o.NumPatient == p.NumPatient))

Le SQL généré

EF Core n'a rien de magique : il produit du SQL, et tu dois savoir le lire. ToQueryString() affiche la requête sans l'exécuter.

csharp
int num = 2;
Console.WriteLine(db.Ordonnances.Include(o => o.Patient).Where(o => o.NumMedecin == num).ToQueryString());

Avec le fournisseur MySQL d'Oracle, on obtient :

sql
SET @num = 2;

SELECT `o`.`NumOrdonnance`, `o`.`DateEmission`, `o`.`NumMedecin`, `o`.`NumPatient`, `p`.`NumPatient`,
       `c`.`NumOrdonnance`, `c`.`CodeMedicament`, `c`.`DureeJours`, `c`.`Posologie`,
       `p`.`DateNaissance`, `p`.`Nom`, `p`.`NumeroSecu`, `p`.`Prenom`
FROM `ordonnance` AS `o`
INNER JOIN `patient` AS `p` ON `o`.`NumPatient` = `p`.`NumPatient`
LEFT JOIN `contenir` AS `c` ON `o`.`NumOrdonnance` = `c`.`NumOrdonnance`
WHERE `o`.`NumMedecin` = @num
ORDER BY `o`.`NumOrdonnance`, `p`.`NumPatient`, `c`.`NumOrdonnance`

Trois choses à remarquer : la variable num devient un paramètre @num (EF Core protège de l'injection SQL tant que tu n'assembles pas toi-même du SQL brut) ; Include produit un INNER JOIN ; les lignes possédées (contenir) sont jointes automatiquement par un LEFT JOIN.

Écrire : le suivi des modifications et SaveChanges

Un contexte suit les entités qu'il a chargées. Modifie une propriété, puis appelle SaveChangesAsync() : EF Core compare avec l'état d'origine et n'écrit que ce qui a changé.

csharp
await using var db = new GsbContext(options);
Patient marie = await db.Patients.FirstAsync(p => p.Nom == "Dupont");
marie.Prenom = "Marie-Anne";                 // l'entité passe à l'état Modified
int lignes = await db.SaveChangesAsync();    // UPDATE patient SET Prenom = @p0 WHERE NumPatient = @p1
OpérationCode EF Core
Ajouterdb.Patients.Add(patient); puis SaveChangesAsync()
Modifier une entité chargéechanger la propriété, puis SaveChangesAsync()
Modifier une entité venue d'ailleurs (d'un formulaire)db.Patients.Update(patient); puis SaveChangesAsync()
Supprimerdb.Patients.Remove(patient); puis SaveChangesAsync(), ou ExecuteDeleteAsync()
Modifier en masse, sans charger.Where(…).ExecuteUpdateAsync(s => s.SetProperty(m => m.Specialite, "Pédiatrie générale"))

SaveChangesAsync enregistre toutes les modifications en attente dans une seule transaction : ajouter une ordonnance et ses lignes, puis appeler SaveChangesAsync une fois, donne le même résultat que la transaction écrite à la main au chapitre 9.

Pour une liste en lecture seule, ajoute AsNoTracking() : le contexte ne garde pas de copie des objets, la requête est plus rapide et consomme moins de mémoire.

Quand la base refuse une écriture, SaveChangesAsync lève une DbUpdateException dont InnerException contient l'exception du pilote (avec le fournisseur d'Oracle, une MySql.Data.MySqlClient.MySqlException, dont la propriété Number vaut 1062 pour un doublon ou 1451 pour une ligne référencée).

Faire évoluer la base : les migrations

Une migration est une classe C# générée par EF Core qui décrit un changement du schéma (créer une table, ajouter une colonne) et sait l'appliquer ou l'annuler. Les migrations conviennent quand le modèle C# est la référence (approche code first).

L'outil dotnet ef doit pouvoir créer un contexte sans lancer l'application. Pour une application WinForms, on lui fournit une fabrique de conception, qui lit la chaîne de connexion dans une variable d'environnement :

csharp
using Microsoft.EntityFrameworkCore.Design;

public class GsbContextFactory : IDesignTimeDbContextFactory<GsbContext>
{
    public GsbContext CreateDbContext(string[] args)
    {
        string chaine = Environment.GetEnvironmentVariable("GSB_ConnectionStrings__Gsb")
            ?? throw new InvalidOperationException("Définis la variable GSB_ConnectionStrings__Gsb.");
        var options = new DbContextOptionsBuilder<GsbContext>().UseMySQL(chaine).Options;
        return new GsbContext(options);
    }
}

Puis, depuis le dossier de la solution :

bash
dotnet ef migrations add Initiale --project Gsb.Donnees     # génère la migration à partir du modèle
dotnet ef migrations script --project Gsb.Donnees           # affiche le SQL qu'elle exécutera
dotnet ef database update --project Gsb.Donnees             # l'applique à la base

Relis toujours le script généré avant de l'appliquer à une base qui contient des données : une colonne renommée peut être traduite par une suppression suivie d'une création.

Base existante. Si la base existe déjà (le cas le plus fréquent au BTS : tu as écrit le script SQL d'abord), tu n'as pas besoin de migrations. Mappe tes classes sur les tables existantes comme ci-dessus, ou fais générer les classes et le contexte à partir de la base (approche database first) :

bash
dotnet ef dbcontext scaffold "Server=localhost;Database=gsb_ordonnances;User ID=gsb_dev;Password=…" MySql.EntityFrameworkCore --output-dir Entites --project Gsb.Donnees

Exercices

Exercice 1 sur 4 Traduire en LINQ Réussi À revoir

Écris avec EF Core (contexte db) les requêtes suivantes, en renvoyant des listes.

  1. Les patients nés avant 2000, triés par date de naissance.
  2. Le nombre de médecins pédiatres.
  3. Le nom des patients qui n'ont aucune ordonnance.
Voir la réponseMasquer la réponse de l'exercice 1
csharp
// 1.
List<Patient> anciens = await db.Patients
    .Where(p => p.DateNaissance < new DateTime(2000, 1, 1))
    .OrderBy(p => p.DateNaissance)
    .ToListAsync();

// 2.
int pediatres = await db.Medecins.CountAsync(m => m.Specialite == "Pédiatrie");

// 3.
List<string> sansOrdonnance = await db.Patients
    .Where(p => !db.Ordonnances.Any(o => o.NumPatient == p.NumPatient))
    .Select(p => p.Nom)
    .ToListAsync();

Avec le jeu d'essai : 1. Dupont Marie seule (née en 1990 ; une date NULL n'est jamais « avant 2000 ») ; 2. un médecin, Claire Nguyen ; 3. ["Bernard"].

Tu avais trouvé ?

Exercice 2 sur 4 La cascade cachée Réussi À revoir

Un camarade a écrit son contexte sans OnDelete. Il teste la suppression de Marie Dupont, qui a une ordonnance : la suppression réussit. Qu'est-il arrivé à l'ordonnance 1 et à ses deux lignes ? Comment l'éviter ?

Voir la réponseMasquer la réponse de l'exercice 2

Pour une clé étrangère obligatoire (NumPatient est un int, pas un int?), EF Core configure par défaut ON DELETE CASCADE dans les migrations et supprime aussi les entités suivies : l'ordonnance 1 a été supprimée avec le patient, et ses lignes dans contenir aussi (elles appartiennent à l'ordonnance). L'historique médical est perdu.

On l'évite en écrivant .OnDelete(DeleteBehavior.Restrict) sur la relation : la suppression échoue alors avec une erreur de clé étrangère, que l'application traduit en message (« ce patient a des ordonnances »).

Tu avais trouvé ?

Exercice 3 sur 4 Combien de requêtes ? Réussi À revoir

Combien de requêtes SQL ce code envoie-t-il au serveur, et qu'affiche-t-il ?

csharp
await using var db = new GsbContext(options);
var ordonnances = await db.Ordonnances.ToListAsync();
foreach (var o in ordonnances)
{
    Console.WriteLine(o.Patient?.Nom ?? "?");
}
Voir la réponseMasquer la réponse de l'exercice 3

Une seule requête (le ToListAsync). Mais comme Include(o => o.Patient) manque et que le chargement différé est désactivé, o.Patient vaut null pour chaque ordonnance : le programme affiche « ? » autant de fois qu'il y a d'ordonnances (deux avec le jeu d'essai). La correction est d'ajouter .Include(o => o.Patient) avant ToListAsync(), ce qui garde une seule requête, avec jointure.

Tu avais trouvé ?

Exercice 4 sur 4 ORM ou SQL ? Réussi À revoir

Pour ta réalisation d'E6, tu hésites entre EF Core et MySqlConnector. La base contient une procédure stockée et un trigger qui calcule un total. Donne deux arguments pour chaque solution et dis ce que tu choisirais.

Voir la réponseMasquer la réponse de l'exercice 4

EF Core : moins de code répétitif (pas de recopie colonne par colonne) ; requêtes LINQ vérifiées par le compilateur ; transactions et paramètres gérés automatiquement ; migrations pour faire évoluer le schéma.

MySqlConnector : le SQL est visible et maîtrisé, ce qui est rassurant pendant l'heure de réalisation de l'E6 où tu dois modifier vite une requête ; appeler la procédure stockée et profiter du trigger est direct ; pas de fournisseur à choisir selon la version d'EF Core.

Un choix raisonnable pour une réalisation de BTS : MySqlConnector avec des DAO, que tu maîtrises de bout en bout, et éventuellement EF Core pour une seconde réalisation, en sachant expliquer le SQL qu'il génère. L'important est de justifier le choix devant la commission.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Consommer une API REST en C# : HttpClient et JSONRevoir le sommaire du cours