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.
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.
Il existe un fossé conceptuel entre le monde Objet (C#) et le monde Relationnel (SQL).
Sensor.Location.Name).
Le rôle de l'ORM : Un pont entre deux mondes
L'ORM (Object-Relational Mapping) automatise cette traduction :
Pour faire correspondre vos classes C# et vos tables SQL (comme SensorData et
Location), EF Core utilise 2 niveaux de logique.
Si vous respectez les règles de nommage de Microsoft, EF Core déduit la structure tout seul :
Id ou [NomDeClasse]Id devient automatiquement la Clé
Primaire (PK).LocationId accompagnée d'un objet Location crée automatiquement
une relation 1-N (Clé Étrangère).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). |
Dans cette approche, le Code C# est la source de vérité absolue.
Le DbContext représente une session de travail avec la base de données (Unité
de Travail / Unit of Work).
Le DbContext fonctionne comme un panier sur un site e-commerce :
await _context.SaveChangesAsync() (le passage en caisse)
que toutes les requêtes SQL (INSERT, UPDATE, DELETE) sont générées et exécutées d'un coup.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.
C'est ici que toute la puissance d'EF Core réside (concept appliqué au TP 6).
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.
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();
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);
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.
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();
}
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);
}
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).
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();
}
}
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();
}
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();
}
}
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#.
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)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"));
AUTOINCREMENT vs SERIAL). Lors d'un changement, il faut :
Migrations.dotnet ef migrations add InitialPostgres.dotnet ef database update.| 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) |
| 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. |
Module: .Net C# Programming
Level: 4th Year Computer Engineering (Data Science Specialty)
Prerequisite: TP 5 and TP 6 completed.
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.
There is a conceptual gap between the Object-Oriented world (C#) and the Relational world (SQL).
Sensor.Location.Name).
The role of the ORM: A bridge between two worlds
The ORM (Object-Relational Mapping) automates this translation:
To map your C# classes and SQL tables (like SensorData and Location), EF Core
relies on 2 logic levels.
If you adhere to Microsoft's naming rules, EF Core deduces the structure automatically:
Id or [ClassName]Id automatically becomes the Primary Key
(PK).LocationId property alongside a Location object automatically generates a
1-N relationship (Foreign Key).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). |
In this approach, the C# Code is the absolute source of truth.
The DbContext represents a session of work with the database (Unit of Work).
The DbContext acts like a shopping cart on an e-commerce website:
await _context.SaveChangesAsync() (checking out) that all SQL
queries (INSERT, UPDATE, DELETE) are generated and executed at once.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.
This is where the full power of EF Core lies (applied in TP 6).
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.
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();
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);
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.
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();
}
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);
}
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).
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();
}
}
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();
}
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();
}
}
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.
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)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"));
AUTOINCREMENT vs SERIAL). When switching, you must:
Migrations folder.dotnet ef migrations add InitialPostgres.dotnet ef database update.| 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) |
| 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. |