Blazor : injection de dépendances
@inject vs [Inject], enregistrement des services, les lifetimes Singleton/Scoped/Transient et le piège du Scoped par circuit en Blazor Server, plus le pattern state container.
Blazor : injection de dépendances
@inject est passé vite dans la syntaxe, noyé parmi les directives. Mais l'injection de dépendances (DI) est un pilier de .NET, et Blazor cache une subtilité importante sur les durées de vie. On creuse.
# Injecter un service
Deux façons d'obtenir un service dans un composant :
@* 1. La directive @inject, dans le fichier .razor *@
@inject ProduitService Produits
@code {
// 2. L'attribut [Inject], pratique en code-behind
[Inject] private ILogger<Catalogue> Logger { get; set; } = default!;
protected override void OnInitialized() =>
Logger.LogInformation("{Count} produits chargés", Produits.Count);
}@* 1. La directive @inject, dans le fichier .razor *@@inject ProduitService Produits @code { // 2. L'attribut [Inject], pratique en code-behind [Inject] private ILogger<Catalogue> Logger { get; set; } = default!; protected override void OnInitialized() => Logger.LogInformation("{Count} produits chargés", Produits.Count);}Les composants n'ont pas d'injection par constructeur : le framework les instancie lui-même. On injecte donc par propriété ([Inject]) ou via la directive @inject.
# Enregistrer un service
Un service se déclare dans Program.cs, contre son abstraction de préférence :
builder.Services.AddScoped<ProduitService>();
builder.Services.AddSingleton<IHorloge, HorlogeSysteme>();
builder.Services.AddHttpClient();builder.Services.AddScoped<ProduitService>();builder.Services.AddSingleton<IHorloge, HorlogeSysteme>();builder.Services.AddHttpClient();Le premier type est l'abstraction demandée par les composants (@inject IHorloge), le second l'implémentation concrète fournie.
# Les trois lifetimes
| Lifetime | Une instance par... | Pour quoi |
|---|---|---|
Singleton | application entière | configuration, cache, état sans dimension utilisateur |
Scoped | scope (voir le piège ci-dessous) | services liés à un utilisateur / une session |
Transient | injection (une neuve à chaque fois) | services légers et sans état |
# Le piège : Scoped n'est pas « par requête »
En ASP.NET classique, un service Scoped vit le temps d'une requête HTTP. En Blazor, ce n'est pas du tout ça :
- Blazor Server :
Scoped= par circuit, c'est-à-dire par connexion SignalR de l'utilisateur (voir l'hébergement). Le service vit tant que l'onglet reste connecté, à travers toutes les navigations. UnScopeds'y comporte plus comme un « singleton par utilisateur » que comme un service jetable. - Blazor WebAssembly : il n'y a qu'un utilisateur (le navigateur), donc
Scopedse comporte comme unSingleton.
Corollaire sécurité : ne stocke jamais l'état d'un utilisateur dans un Singleton en Blazor Server. Le singleton est partagé par tous les circuits, donc tous les utilisateurs : tu fuiterais les données de l'un vers l'autre. L'état par utilisateur va dans un Scoped.
# Un service d'état partagé (state container)
Le pattern courant pour partager de l'état entre composants éloignés : un service Scoped qui porte l'état et notifie ses abonnés.
public class PanierState
{
private readonly List<string> _articles = new();
public IReadOnlyList<string> Articles => _articles;
public event Action? OnChange;
public void Ajouter(string article)
{
_articles.Add(article);
OnChange?.Invoke(); // prévient les composants abonnés
}
}public class PanierState{ private readonly List<string> _articles = new(); public IReadOnlyList<string> Articles => _articles; public event Action? OnChange; public void Ajouter(string article) { _articles.Add(article); OnChange?.Invoke(); // prévient les composants abonnés }}@inject PanierState Panier
@implements IDisposable
<p>@Panier.Articles.Count article(s) dans le panier</p>
@code {
protected override void OnInitialized() => Panier.OnChange += StateHasChanged;
public void Dispose() => Panier.OnChange -= StateHasChanged;
}@inject PanierState Panier@implements IDisposable <p>@Panier.Articles.Count article(s) dans le panier</p> @code { protected override void OnInitialized() => Panier.OnChange += StateHasChanged; public void Dispose() => Panier.OnChange -= StateHasChanged;}Le composant s'abonne à OnChange et appelle StateHasChanged pour se re-rendre quand l'état bouge. Se désabonner dans Dispose est obligatoire : sans ça, le composant détruit reste référencé par l'événement, ce qui provoque une fuite mémoire.
# À retenir
- On injecte via
@inject(dans le.razor) ou[Inject](en code-behind) ; pas d'injection par constructeur pour les composants. - Les services s'enregistrent dans
Program.csen Singleton / Scoped / Transient. - Piège Blazor :
Scoped= par circuit en Server (pas par requête), et se comporte comme unSingletonen WebAssembly. - Ne mets jamais d'état utilisateur dans un
Singletonen Server : il est partagé par tous. - Le state container (service
Scoped+event OnChange+StateHasChanged) partage l'état entre composants ; désabonne-toi dansDispose.
Ce chapitre touche déjà au cycle de vie (OnInitialized, Dispose) : une page dédiée au cycle de vie du composant est le prochain approfondissement naturel.