Module : Programmation .Net C#
Durée : 2h
Objectif : Connecter le Service métier à Entity Framework Core. Remplacer les fausses données par des requêtes SQL réelles via LINQ, et gérer l'asynchronisme pour fluidifier l'interface utilisateur.
app.db doit exister et contenir les
données
de test,
notamment pour SensorData, Location, etc.).Actuellement, notre SensorService possède une liste codée en dur. Nous allons la
supprimer et
demander à ASP.NET de nous fournir le AppDbContext pour interroger SQLite.
L'architecture de bout en bout de l'application
1. Ouvrez le fichier Services/SensorService.cs.
2. Supprimez complètement la fausse liste _sensors.
3. Ajoutez un constructeur pour injecter le AppDbContext :
using DashboardData.Models;
using DashboardData.Data; // Pour accéder à AppDbContext
using Microsoft.EntityFrameworkCore; // Indispensable pour les méthodes Async d'EF Core
namespace DashboardData.Services;
public class SensorService : ISensorService
{
private readonly AppDbContext _context;
// L'Injection de dépendance fait le travail ici :
// Quand Blazor crée le SensorService, il lui passe automatiquement le DbContext.
public SensorService(AppDbContext context)
{
_context = context;
}
// ... la suite au prochain point
}
Une base de données est un système externe. La lecture prend du temps. Il faut
utiliser des
méthodes asynchrones (Async) pour ne pas bloquer l'interface Web pendant la
requête.
1. Mettez à jour la méthode GetSensorsAsync dans SensorService.cs.
Nous
allons
utiliser Include pour récupérer la localisation associée à chaque
capteur :
public async Task<List<SensorData>> GetSensorsAsync()
{
// EF Core traduit Include par un JOIN SQL vers la table Location
return await _context.Sensors
.Include(s => s.Location)
.ToListAsync();
}
2. Mettez également à jour la méthode d'ajout :
public async Task AddSensorAsync(SensorData sensor)
{
// 1. On prépare l'ajout en mémoire
_context.Sensors.Add(sensor);
// 2. On valide la transaction (Génère le INSERT INTO SQL)
await _context.SaveChangesAsync();
}
(⚠️ Assurez-vous que l'interface ISensorService correspond bien à ces
signatures asynchrones
!)
3. Testez l'application : Lancez dotnet watch.
La puissance d'EF Core est de traduire le code C# en SQL. Si vous filtrez avec LINQ, le filtrage se fera dans le moteur SQLite, ce qui est infiniment plus rapide que de tout charger en RAM.
1. Dans ISensorService.cs, ajoutez la signature d'une nouvelle méthode :
Task<List<SensorData>> GetCriticalSensorsAsync(double threshold);
2. Dans SensorService.cs, implémentez cette méthode :
public async Task<List<SensorData>> GetCriticalSensorsAsync(double threshold)
{
return await _context.Sensors
.Include(s => s.Location)
.Where(s => s.Value > threshold) // Traduit en : WHERE Value > @threshold
.OrderByDescending(s => s.Value) // Traduit en : ORDER BY Value DESC
.ToListAsync(); // Déclenche l'exécution SQL
}
Intégrons cette nouvelle fonctionnalité d'alerte et affichons la localisation dans notre vue.
1. Ouvrez Pages/MyDashboard.razor.
2. Ajoutez des boutons pour basculer les filtres et adaptez le tableau :
@page "/dashboard"
@rendermode InteractiveServer
@using DashboardData.Models
@using DashboardData.Services
@inject ISensorService SensorService
<button class="btn btn-secondary mb-3" @onclick="LoadAll">Toutes les sondes</button>
<button class="btn btn-danger mb-3" @onclick="LoadCritical">Alertes (> 30.0)</button>
@if (Sensors == null)
{
<p>Chargement en cours...</p>
}
else
{
<table class="table table-striped">
<thead>
<tr>
<th>Capteur</th>
<th>Lieu</th>
<th>Valeur Courante</th>
</tr>
</thead>
<tbody>
@foreach (var sensor in Sensors)
{
<tr>
<td>@sensor.Name</td>
<!-- Grâce au Include(), Location n'est pas nul ! -->
<td>@(sensor.Location?.Name ?? "Non défini")</td>
<td>@sensor.Value</td>
</tr>
}
</tbody>
</table>
}
@code {
private List<SensorData>? Sensors;
protected override async Task OnInitializedAsync()
{
await LoadAll();
}
private async Task LoadAll()
{
Sensors = await SensorService.GetSensorsAsync();
}
private async Task LoadCritical()
{
Sensors = await SensorService.GetCriticalSensorsAsync(30.0);
}
}
Au lieu de ramener toutes les lignes en mémoire pour faire une moyenne, demandez à SQLite de faire le calcul et de vous renvoyer un seul nombre (très optimisé !).
1. Dans ISensorService et SensorService, créez trois nouvelles
méthodes
asynchrones
:
Task<int> GetTotalCountAsync() :
utilise .CountAsync()Task GetAverageValueAsync() : utilise
.AverageAsync(s => s.Value)
Task GetMaxValueAsync() : utilise
.MaxAsync(s => s.Value)
Exemple d'implémentation pour la moyenne :
public async Task<double> GetAverageValueAsync()
{
// Si la base est vide, AverageAsync plante. On met une sécurité.
if (!await _context.Sensors.AnyAsync()) return 0;
return await _context.Sensors.AverageAsync(s => s.Value);
}
Dans MyDashboard.razor, affichez ces trois indicateurs sous forme de "Cartes" (Cards Bootstrap)
au-dessus de votre tableau.
@code, déclarez trois variables (TotalSondes,
Moyenne, Max).
OnInitializedAsync, appelez vos nouveaux services pour remplir ces
variables.<div class="row mb-4">
<div class="col-md-4">
<div class="card text-white bg-primary">
<div class="card-body">
<h5 class="card-title">Total Sondes</h5>
<h2>@TotalSondes</h2>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-white bg-success">
<div class="card-body">
<h5 class="card-title">Moyenne Globale</h5>
<h2>@Moyenne.ToString("F1") °C</h2> <!-- F1 = 1 chiffre après la virgule -->
</div>
</div>
</div>
<!-- Faites la même chose pour le MAX en rouge (bg-danger) -->
</div>
Félicitations, vous avez maintenant une application "Full-Stack" complète : Base de données ↔ Service Métier ↔ Interface Web.
AppDbContext directement dans le constructeur de votre Service.
...Async() des
méthodes LINQ (ex: ToListAsync(), CountAsync()).Include() : Permet de récupérer l'objet
`Location` (Clé
étrangère) en même temps que le capteur.Where() exécuté
sur un
DbSet
se transforme en WHERE SQL. C'est le moteur de la DB qui travaille !
Module: C# .Net Programming
Duration: 2h
Objective: Connect the business Service to Entity Framework Core. Replace mock data with real SQL queries via LINQ, and manage asynchrony to keep the user interface responsive.
app.db must exist and contain the seeded test data for
SensorData, Location, etc.).
Currently, our SensorService has a hardcoded list. We are going to delete it and ask ASP.NET to
provide us with the AppDbContext to query SQLite.
End-to-End Application Architecture
1. Open the file Services/SensorService.cs.
2. Completely delete the fake _sensors list.
3. Add a constructor to inject the AppDbContext:
using DashboardData.Models;
using DashboardData.Data; // To access AppDbContext
using Microsoft.EntityFrameworkCore; // Essential for EF Core Async methods
namespace DashboardData.Services;
public class SensorService : ISensorService
{
private readonly AppDbContext _context;
// Dependency Injection does the work here:
// When Blazor creates the SensorService, it automatically passes the DbContext to it.
public SensorService(AppDbContext context)
{
_context = context;
}
// ... continuation in the next point
}
A database is an external system. Reading takes time. You must use asynchronous methods
(Async) so as not to block the Web interface during the query.
1. Update the GetSensorsAsync method in SensorService.cs. We'll use
Include to retrieve the related Location for each sensor:
public async Task<List<SensorData>> GetSensorsAsync()
{
// EF Core translates Include into a SQL JOIN to the Location table
return await _context.Sensors
.Include(s => s.Location)
.ToListAsync();
}
2. Also update the add method:
public async Task AddSensorAsync(SensorData sensor)
{
// 1. Prepare the addition in memory
_context.Sensors.Add(sensor);
// 2. Validate the transaction (Generates the SQL INSERT INTO)
await _context.SaveChangesAsync();
}
(⚠️ Make sure the ISensorService interface reflects these async
signatures!)
3. Test the application: Run dotnet watch.
The power of EF Core is translating C# code into SQL. If you filter with LINQ, the filtering will happen in the SQLite engine, which is infinitely faster than loading everything into RAM.
1. In ISensorService.cs, add the signature for a new method:
Task<List<SensorData>> GetCriticalSensorsAsync(double threshold);
2. In SensorService.cs, implement this method:
public async Task<List<SensorData>> GetCriticalSensorsAsync(double threshold)
{
return await _context.Sensors
.Include(s => s.Location)
.Where(s => s.Value > threshold) // Translated to: WHERE Value > @threshold
.OrderByDescending(s => s.Value) // Translated to: ORDER BY Value DESC
.ToListAsync(); // Triggers SQL execution
}
Let's update our UI to display this alert feature along with the location data.
1. Open Pages/MyDashboard.razor.
2. Add buttons to toggle filters and adapt the table:
@page "/dashboard"
@rendermode InteractiveServer
@using DashboardData.Models
@using DashboardData.Services
@inject ISensorService SensorService
<button class="btn btn-secondary mb-3" @onclick="LoadAll">All Sensors</button>
<button class="btn btn-danger mb-3" @onclick="LoadCritical">Alerts (> 30.0)</button>
@if (Sensors == null)
{
<p>Loading...</p>
}
else
{
<table class="table table-striped">
<thead>
<tr>
<th>Sensor</th>
<th>Location</th>
<th>Current Value</th>
</tr>
</thead>
<tbody>
@foreach (var sensor in Sensors)
{
<tr>
<td>@sensor.Name</td>
<!-- Thanks to Include(), Location is not null! -->
<td>@(sensor.Location?.Name ?? "Undefined")</td>
<td>@sensor.Value</td>
</tr>
}
</tbody>
</table>
}
@code {
private List<SensorData>? Sensors;
protected override async Task OnInitializedAsync()
{
await LoadAll();
}
private async Task LoadAll()
{
Sensors = await SensorService.GetSensorsAsync();
}
private async Task LoadCritical()
{
Sensors = await SensorService.GetCriticalSensorsAsync(30.0);
}
}
Instead of bringing all rows into memory to calculate an average, you will ask SQLite to do the calculation and return a single number (highly optimized!).
1. In ISensorService and SensorService, create three new asynchronous methods:
Task<int> GetTotalCountAsync() :
uses .CountAsync()Task GetAverageValueAsync() : uses
.AverageAsync(s => s.Value)
Task GetMaxValueAsync() : uses
.MaxAsync(s => s.Value)
Example implementation for the average:
public async Task<double> GetAverageValueAsync()
{
// If the database is empty, AverageAsync crashes. We put a safeguard.
if (!await _context.Sensors.AnyAsync()) return 0;
return await _context.Sensors.AverageAsync(s => s.Value);
}
In MyDashboard.razor, display these three indicators as Bootstrap "Cards" above your table.
@code area, declare three variables (TotalSensors,
Average, Max).
OnInitializedAsync method, call your new services to fill these variables.<div class="row mb-4">
<div class="col-md-4">
<div class="card text-white bg-primary">
<div class="card-body">
<h5 class="card-title">Total Sensors</h5>
<h2>@TotalSensors</h2>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-white bg-success">
<div class="card-body">
<h5 class="card-title">Global Average</h5>
<h2>@Average.ToString("F1") °C</h2> <!-- F1 = 1 decimal place -->
</div>
</div>
</div>
<!-- Do the same for MAX in red (bg-danger) -->
</div>
Congratulations, you now have a complete "Full-Stack" application: Database ↔ Business Service ↔ Web Interface.
AppDbContext
directly in the constructor of your Service....Async() versions of LINQ
methods (e.g., ToListAsync(), CountAsync()) for network/disk operations.
Include(): Allows retrieving the related
Location object (Foreign Key) at the same time as the sensor.
Where() executed on a DbSet
transforms into a SQL WHERE. The database engine does the heavy lifting!