🌐
🔍 100%
👁️

🎙️ Sélectionner une voix

🎙️ Select a voice

Chapitre 5 : Entity Framework Core, CRUD, Relations & LINQ Async

Module : Programmation .Net C#

Niveau : 4ème Génie Informatique (Spécialité Data Science)

Pré-requis : Avoir réalisé les TP 5 et TP 6.

Introduction

Jusqu'au TP 4, nos données vivaient dans des listes en mémoire (`List<T>`). Dès que le serveur redémarrait, tout était perdu. Ce chapitre théorique formalise les concepts acquis durant les TP 5 et 6 : la persistance durable grâce à Entity Framework Core, la gestion des relations (Jointures) et l'interrogation de la base de données via LINQ Asynchrone.

1. Le Problème de la "Mismatche" (Objet vs Relationnel)

Il existe un fossé conceptuel entre le monde Objet (C#) et le monde Relationnel (SQL).

Objets C# EF Core (ORM) Traducteur Bidirectionnel Tables SQL

Le rôle de l'ORM : Un pont entre deux mondes

L'ORM (Object-Relational Mapping) automatise cette traduction :

2. Configuration du Modèle : Annotations et Conventions

Pour faire correspondre vos classes C# et vos tables SQL (comme SensorData et Location), EF Core utilise 2 niveaux de logique.

A. Conventions (Le Standard Automatique)

Si vous respectez les règles de nommage de Microsoft, EF Core déduit la structure tout seul :

B. Data Annotations (Le Pratique)

Ce sont des attributs placés directement sur le code C# pour forcer un comportement ou ajouter des règles de validation.

Annotation Effet SQL & Validation
[Key] Désigne explicitement la Clé Primaire.
[Table("T_Capteurs")] Force le nom de la table SQL (outrepasse le nom de la classe).
[Required] Force la colonne à NOT NULL. Bloque l'insertion si vide.
[NotMapped] Ignore la propriété (EF ne créera aucune colonne SQL pour celle-ci).

3. L'Approche "Code-First" et Migrations

Dans cette approche, le Code C# est la source de vérité absolue.

C# Model ⚙️ Add-Migration SQL DB Update-Database

4. Le DbContext et son Injection

Le DbContext représente une session de travail avec la base de données (Unité de Travail / Unit of Work).

💡 Analogie : Le Panier d'Achat

Le DbContext fonctionne comme un panier sur un site e-commerce :

L'Injection du DbContext (DI)

Dans ASP.NET Core, le DbContext est injecté via l'Injection de Dépendances avec le cycle de vie Scoped (une instance par requête Web ou par session Blazor). Vous ne devez jamais faire de new AppDbContext() vous-même.

5. LINQ to Entities : Requêtage Avancé

C'est ici que toute la puissance d'EF Core réside (concept appliqué au TP 6).

A. L'Asynchronisme obligatoire (`ToListAsync`)

La base de données est un système externe (disque dur, réseau). Si vous utilisez .ToList() (synchrone), le serveur Web gèle en attendant la réponse. Vous devez toujours utiliser .ToListAsync() (ou CountAsync, etc.) combiné à await pour libérer le thread principal pendant que la base de données travaille.

B. Le Chargement des Relations (`Include`)

Par défaut, EF Core ne charge pas les relations pour économiser la mémoire (Lazy Loading). Si vous demandez un SensorData, sa propriété Location sera null.
Pour forcer le chargement de la relation, il faut utiliser Include(), qui se traduira par une jointure SQL (JOIN).

// Code C# dans le Service
var data = await _context.Sensors
    .Include(s => s.Location) // Charge la localisation !
    .Where(s => s.Value > 30)
    .ToListAsync();
SELECT "s"."Id", "s"."Name", "s"."Value", "s"."LocationId", "l"."Id", "l"."Name" FROM "Sensors" AS "s" LEFT JOIN "Locations" AS "l" ON "s"."LocationId" = "l"."Id" WHERE "s"."Value" > 30.0

C. L'Agrégation Côté Serveur (Push-down computing)

Si vous voulez calculer la moyenne des températures, vous pourriez tout charger avec ToListAsync() puis utiliser le LINQ classique en mémoire. C'est une grave erreur si vous avez des millions de lignes !
La bonne pratique est de laisser le moteur SQL faire le calcul lourd :

// EF Core traduit ceci en : SELECT AVG("Value") FROM "Sensors"
double moyenne = await _context.Sensors.AverageAsync(s => s.Value);

6. Les Opérations CRUD avec EF Core

Le terme CRUD représente les quatre opérations de base de la persistance des données : Create, Read, Update, Delete. Voici comment les réaliser avec Entity Framework Core de façon asynchrone.

A. Create (Ajouter des données)

Pour ajouter un nouvel enregistrement, on instancie un objet C# classique, on l'ajoute au DbContext (Tracking) et on sauvegarde.

public async Task AddSensorAsync(SensorData newSensor)
{
    // 1. Ajout dans le "panier" (Tracking)
    _context.Sensors.Add(newSensor);
    
    // 2. Exécution de l'INSERT SQL en base
    await _context.SaveChangesAsync();
}

B. Read (Lire des données)

La lecture utilise LINQ (comme vu précédemment). On peut lire tous les enregistrements, en filtrer certains, ou chercher un élément par sa Clé Primaire de façon optimisée.

public async Task<List<SensorData>> GetAllSensorsAsync()
{
    // SELECT * FROM Sensors
    return await _context.Sensors.ToListAsync();
}

public async Task<SensorData?> GetSensorByIdAsync(int id)
{
    // Très optimisé pour la recherche par clé primaire
    return await _context.Sensors.FindAsync(id);
}

C. Update (Mettre à jour des données)

Il existe deux approches : le suivi automatique (modification d'un objet déjà chargé via une requête LINQ) ou la mise à jour explicite pour un objet détaché (reçu d'un formulaire par exemple).

1. Suivi Automatique (Tracking)

public async Task UpdateSensorValueAsync(int id, double newValue)
{
    // EF Core charge et "suit" l'objet en mémoire
    var sensor = await _context.Sensors.FindAsync(id);
    if (sensor != null)
    {
        // Modification de la propriété détectée automatiquement
        sensor.Value = newValue;
        
        // Exécute un UPDATE seulement sur les champs modifiés
        await _context.SaveChangesAsync();
    }
}

2. Mise à Jour Explicite

public async Task UpdateSensorAsync(SensorData sensor)
{
    // Marque explicitement TOUT l'objet comme modifié
    _context.Sensors.Update(sensor);
    
    // Exécution de l'UPDATE SQL en base pour toutes les colonnes
    await _context.SaveChangesAsync();
}

D. Delete (Supprimer des données)

Pour supprimer une donnée, il faut d'abord l'avoir sous forme d'objet C#, puis l'enlever du DbContext.

public async Task DeleteSensorAsync(int id)
{
    // 1. Trouver l'objet à supprimer
    var sensor = await _context.Sensors.FindAsync(id);
    if (sensor != null)
    {
        // 2. Le retirer du "panier"
        _context.Sensors.Remove(sensor);
        
        // 3. Exécution du DELETE SQL en base
        await _context.SaveChangesAsync();
    }
}

7. Flexibilité de EF Core : Les Providers (SGBD)

L'une des plus grandes forces d'Entity Framework Core est son agnosticisme vis-à-vis des bases de données. Le moteur EF Core est indépendant du système de gestion de base de données (SGBD) utilisé.

Vous pouvez développer et tester votre application avec un SGBD léger comme SQLite, puis passer en production sur un SGBD d'entreprise comme PostgreSQL ou SQL Server sans réécrire vos requêtes LINQ ou vos classes C#.

Comment changer de base de données ?

  1. Installer le Provider correspondant : Chaque SGBD possède son propre package NuGet ("Provider").
    • Microsoft.EntityFrameworkCore.Sqlite (SQLite)
    • Microsoft.EntityFrameworkCore.SqlServer (SQL Server)
    • Npgsql.EntityFrameworkCore.PostgreSQL (PostgreSQL)
    • Pomelo.EntityFrameworkCore.MySql (MySQL / MariaDB)
    • Oracle.EntityFrameworkCore (Oracle)
    • MongoDB.EntityFrameworkCore (MongoDB - NoSQL)
  2. Modifier la configuration dans Program.cs : Changez simplement la méthode d'extension utilisée lors de l'injection du DbContext.
    // Passage de SQLite...
    builder.Services.AddDbContext<AppDbContext>(options =>
        options.UseSqlite("Data Source=app.db"));
    
    // ... à PostgreSQL en modifiant juste une ligne !
    builder.Services.AddDbContext<AppDbContext>(options =>
        options.UseNpgsql("Host=localhost;Database=mydb;Username=postgres;Password=pwd"));
  3. Générer de nouvelles Migrations : Le code SQL généré par EF Core dépend du SGBD (ex: AUTOINCREMENT vs SERIAL). Lors d'un changement, il faut :
    • Supprimer l'ancien dossier Migrations.
    • Générer la nouvelle base : dotnet ef migrations add InitialPostgres.
    • Appliquer : dotnet ef database update.

✅ Synthèse Ingénieur

Tableau Comparatif : SQL Brut vs EF Core (CRUD & Plus)

Critère / Opération SQL Brut (Raw SQL) EF Core (LINQ / Orienté Objet)
Paradigme Relationnel (Tables, Lignes) Orienté Objet (Classes, Propriétés)
Create (Insertion) INSERT INTO Sensors (Name) VALUES ('X') _context.Sensors.Add(new Sensor { Name="X" });
Read (Lecture) SELECT * FROM Sensors _context.Sensors.ToList();
Update (Mise à jour) UPDATE Sensors SET Value=10 WHERE Id=1 sensor.Value = 10; (Tracking automatique)
Delete (Suppression) DELETE FROM Sensors WHERE Id=1 _context.Sensors.Remove(sensor);
Validation (Commit) COMMIT; _context.SaveChanges(); (Obligatoire pour C, U, D)
Filtrage WHERE Value > 30 .Where(s => s.Value > 30)
Jointure INNER JOIN Locations ON ... .Include(s => s.Location)
Sécurité Risque d'injection SQL si mal écrit Paramétrage automatique (Sécurisé par défaut)
Portabilité Code couplé au SGBD (T-SQL, PL/pgSQL...) Agnostique (Le code C# reste le même)

Vocabulaire & Bonnes Pratiques

Concept Définition & Bonne Pratique
ORM & LINQ Outils traduisant vos requêtes C# en requêtes SQL natives et sécurisées.
Provider (SGBD) Package NuGet permettant à EF Core de communiquer avec une base de données spécifique (ex: UseSqlite, UseNpgsql).
DbContext Votre session BDD. Toujours l'obtenir par Injection de Dépendance.
Include() Indispensable pour effectuer un JOIN et éviter que les propriétés de navigation ne soient nulles.
Async / Await Obligatoire (ToListAsync()) pour toute interaction avec la BDD afin d'éviter le blocage (Freeze) de l'interface utilisateur.
Agrégations SQL Toujours utiliser CountAsync() ou AverageAsync() directement sur le DbContext plutôt que de rapatrier les données en mémoire.

Chapter 5: Entity Framework Core, CRUD, Relations & LINQ Async

Module: .Net C# Programming

Level: 4th Year Computer Engineering (Data Science Specialty)

Prerequisite: TP 5 and TP 6 completed.

Introduction

Up until TP 4, our data lived in in-memory lists (`List<T>`). Once the server restarted, everything was lost. This theoretical chapter formalizes the concepts learned during TP 5 and 6: durable persistence using Entity Framework Core, managing relationships (Joins), and querying the database using Asynchronous LINQ.

1. The Impedance Mismatch Problem (Object vs Relational)

There is a conceptual gap between the Object-Oriented world (C#) and the Relational world (SQL).

C# Objects EF Core (ORM) Bidirectional Translator SQL Tables

The role of the ORM: A bridge between two worlds

The ORM (Object-Relational Mapping) automates this translation:

2. Model Configuration: Annotations and Conventions

To map your C# classes and SQL tables (like SensorData and Location), EF Core relies on 2 logic levels.

A. Conventions (The Automatic Standard)

If you adhere to Microsoft's naming rules, EF Core deduces the structure automatically:

B. Data Annotations (The Practical Way)

These are attributes placed directly on C# code to enforce behavior or add validation rules.

Annotation SQL Effect & Validation
[Key] Explicitly designates the Primary Key.
[Table("T_Sensors")] Overrides the class name and forces the SQL table name.
[Required] Forces the column to be NOT NULL. Blocks inserts if empty.
[NotMapped] Ignores the property (EF will not create any SQL column for it).

3. The "Code-First" Approach and Migrations

In this approach, the C# Code is the absolute source of truth.

C# Model ⚙️ Add-Migration SQL DB Update-Database

4. The DbContext and its Injection

The DbContext represents a session of work with the database (Unit of Work).

💡 Analogy: The Shopping Cart

The DbContext acts like a shopping cart on an e-commerce website:

DbContext Injection (DI)

In ASP.NET Core, the DbContext is injected via Dependency Injection with a Scoped lifecycle (one instance per Web request or Blazor session). You must never instantiate it with new AppDbContext() yourself.

5. LINQ to Entities: Advanced Querying

This is where the full power of EF Core lies (applied in TP 6).

A. Mandatory Asynchrony (`ToListAsync`)

The database is an external system (hard drive, network). If you use .ToList() (synchronous), the Web server thread freezes while waiting for the response. You must always use .ToListAsync() (or CountAsync, etc.) paired with await to free the main thread while the database works.

B. Loading Relationships (`Include`)

By default, EF Core does not load relationships to save memory (Lazy Loading). If you query a SensorData, its Location property will be null.
To force loading the relationship, you must use Include(), which translates to a SQL JOIN.

// C# Code inside the Service
var data = await _context.Sensors
    .Include(s => s.Location) // Loads the Location!
    .Where(s => s.Value > 30)
    .ToListAsync();
SELECT "s"."Id", "s"."Name", "s"."Value", "s"."LocationId", "l"."Id", "l"."Name" FROM "Sensors" AS "s" LEFT JOIN "Locations" AS "l" ON "s"."LocationId" = "l"."Id" WHERE "s"."Value" > 30.0

C. Server-Side Aggregation (Push-down computing)

If you want to calculate the average temperature, you could load everything using ToListAsync() and then use classic in-memory LINQ. This is a terrible mistake if you have millions of rows!
The best practice is to let the SQL engine do the heavy lifting:

// EF Core translates this to: SELECT AVG("Value") FROM "Sensors"
double average = await _context.Sensors.AverageAsync(s => s.Value);

6. CRUD Operations with EF Core

The term CRUD represents the four basic operations of data persistence: Create, Read, Update, Delete. Here is how to perform them asynchronously using Entity Framework Core.

A. Create (Adding data)

To add a new record, instantiate a classic C# object, add it to the DbContext (Tracking), and save.

public async Task AddSensorAsync(SensorData newSensor)
{
    // 1. Add to the "shopping cart" (Tracking)
    _context.Sensors.Add(newSensor);
    
    // 2. Execute the INSERT SQL query in DB
    await _context.SaveChangesAsync();
}

B. Read (Reading data)

Reading uses LINQ (as seen previously). You can read all records, filter some, or search for an item by its Primary Key in an optimized way.

public async Task<List<SensorData>> GetAllSensorsAsync()
{
    // SELECT * FROM Sensors
    return await _context.Sensors.ToListAsync();
}

public async Task<SensorData?> GetSensorByIdAsync(int id)
{
    // Highly optimized for primary key search
    return await _context.Sensors.FindAsync(id);
}

C. Update (Modifying data)

There are two approaches: automatic tracking (modifying an object already loaded via a LINQ query) or explicit updating for a detached object (e.g., received from a form).

1. Automatic Tracking

public async Task UpdateSensorValueAsync(int id, double newValue)
{
    // EF Core loads and "tracks" the object in memory
    var sensor = await _context.Sensors.FindAsync(id);
    if (sensor != null)
    {
        // Modification of the property is automatically detected
        sensor.Value = newValue;
        
        // Executes an UPDATE only on modified fields
        await _context.SaveChangesAsync();
    }
}

2. Explicit Update

public async Task UpdateSensorAsync(SensorData sensor)
{
    // Explicitly marks the ENTIRE object as modified
    _context.Sensors.Update(sensor);
    
    // Execute the UPDATE SQL query in DB for all columns
    await _context.SaveChangesAsync();
}

D. Delete (Removing data)

To delete data, you first need to have it as a C# object, then remove it from the DbContext.

public async Task DeleteSensorAsync(int id)
{
    // 1. Find the object to delete
    var sensor = await _context.Sensors.FindAsync(id);
    if (sensor != null)
    {
        // 2. Remove it from the "shopping cart"
        _context.Sensors.Remove(sensor);
        
        // 3. Execute the DELETE SQL query in DB
        await _context.SaveChangesAsync();
    }
}

7. EF Core Flexibility: DBMS Providers

One of the greatest strengths of Entity Framework Core is its database agnosticism. The EF Core engine is independent of the underlying Database Management System (DBMS).

You can develop and test your application using a lightweight DBMS like SQLite, and switch to an enterprise DBMS like PostgreSQL or SQL Server for production without rewriting your LINQ queries or C# classes.

How to switch databases?

  1. Install the corresponding Provider: Each DBMS has its own NuGet package ("Provider").
    • Microsoft.EntityFrameworkCore.Sqlite (SQLite)
    • Microsoft.EntityFrameworkCore.SqlServer (SQL Server)
    • Npgsql.EntityFrameworkCore.PostgreSQL (PostgreSQL)
    • Pomelo.EntityFrameworkCore.MySql (MySQL / MariaDB)
    • Oracle.EntityFrameworkCore (Oracle)
    • MongoDB.EntityFrameworkCore (MongoDB - NoSQL)
  2. Update the configuration in Program.cs: Simply change the extension method used when injecting the DbContext.
    // Switching from SQLite...
    builder.Services.AddDbContext<AppDbContext>(options =>
        options.UseSqlite("Data Source=app.db"));
    
    // ... to PostgreSQL by changing just one line!
    builder.Services.AddDbContext<AppDbContext>(options =>
        options.UseNpgsql("Host=localhost;Database=mydb;Username=postgres;Password=pwd"));
  3. Generate new Migrations: The SQL code generated by EF Core depends on the DBMS (e.g., AUTOINCREMENT vs SERIAL). When switching, you must:
    • Delete the old Migrations folder.
    • Generate the new baseline: dotnet ef migrations add InitialPostgres.
    • Apply it: dotnet ef database update.

✅ Engineering Synthesis

Comparative Table: Raw SQL vs EF Core (CRUD & More)

Criterion / Operation Raw SQL EF Core (LINQ / Object-Oriented)
Paradigm Relational (Tables, Rows) Object-Oriented (Classes, Properties)
Create (Insert) INSERT INTO Sensors (Name) VALUES ('X') _context.Sensors.Add(new Sensor { Name="X" });
Read (Select) SELECT * FROM Sensors _context.Sensors.ToList();
Update UPDATE Sensors SET Value=10 WHERE Id=1 sensor.Value = 10; (Automatic Tracking)
Delete DELETE FROM Sensors WHERE Id=1 _context.Sensors.Remove(sensor);
Validation (Commit) COMMIT; _context.SaveChanges(); (Mandatory for C, U, D)
Filtering WHERE Value > 30 .Where(s => s.Value > 30)
Joining INNER JOIN Locations ON ... .Include(s => s.Location)
Security SQL Injection risk if poorly written Automatic parameterization (Secure by default)
Portability Coupled to specific DBMS syntax Agnostic (C# code remains exactly the same)

Vocabulary & Best Practices

Concept Definition & Best Practice
ORM & LINQ Tools transforming C# queries into secure native SQL queries.
Provider (DBMS) NuGet package allowing EF Core to translate and communicate with a specific database engine.
DbContext Your DB session. Always obtain it through Dependency Injection.
Include() Essential to perform a JOIN and prevent navigation properties from being null.
Async / Await Mandatory (ToListAsync()) for any DB interaction to prevent UI freezing.
SQL Aggregations Always call CountAsync() or AverageAsync() directly on the DbContext rather than fetching data into memory first.