Partager via


Injection de dépendances ASP.NET Core Blazor

Remarque

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.

Avertissement

Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la Stratégie de prise en charge de .NET et .NET Core. Pour la version actuelle, consultez la version .NET 8 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 8 de cet article.

Par Rainer Stropek et Mike Rousos

Cet article explique comment les applications Blazor peuvent injecter des services dans des composants.

L’injection de dépendances (DI) est une technique permettant d’accéder aux services configurés dans un emplacement central :

  • Les services inscrits dans le framework peuvent être injectés directement dans les composants Razor.
  • Les applications Blazor définissent et inscrivent des services personnalisés et les rendent disponibles dans l’ensemble de l’application via la DI.

Remarque

Nous vous recommandons de lire Injection de dépendances dans ASP.NET Core avant de lire cette rubrique.

Services par défaut

Les services indiqués dans le tableau suivant sont couramment utilisés dans les applications Blazor.

Service Durée de vie Description
HttpClient Délimité

Fournit des méthodes pour envoyer des requêtes HTTP et recevoir des réponses HTTP d’une ressource identifiée par un URI.

Côté client, une instance de HttpClient est inscrite par l’application dans le fichier Program et utilise le navigateur pour gérer le trafic HTTP en arrière-plan.

Côté client, aucun HttpClient n’est configuré comme service par défaut. Dans du code côté serveur, indiquez un HttpClient.

Pour plus d’informations, consultez Appeler une API web à partir d’une application Blazor ASP.NET Core.

Un HttpClient est inscrit en tant que service délimité, et non en tant que singleton. Pour plus d’informations, consultez la section Durée de vie du service.

IJSRuntime

Côté client : singleton

Côté serveur : délimité

Le framework Blazor inscrit IJSRuntime dans le conteneur de service de l’application.

Représente une instance d’un runtime JavaScript où les appels JavaScript sont distribués. Pour plus d’informations, consultez Appeler des fonctions JavaScript à partir de méthodes .NET dans ASP.NET Core Blazor.

Lorsque vous souhaitez injecter le service dans un service singleton sur le serveur, adoptez l’une des approches suivantes :

  • Modifiez l’inscription du service sur « délimité » pour qu’elle corresponde à l’inscription de IJSRuntime, ce qui est approprié si le service traite avec l’état spécifique de l’utilisateur.
  • Passez le IJSRuntime dans l’implémentation du service singleton comme argument de ses appels de méthode au lieu de l’injecter dans le singleton.
NavigationManager

Côté client : singleton

Côté serveur : délimité

Le framework Blazor inscrit NavigationManager dans le conteneur de service de l’application.

Contient des aides pour l’utilisation des URI et de l’état de navigation. Pour plus d’informations, consultez URI et assistants d’état de navigation.

Les services supplémentaires inscrits par le framework Blazor sont décrits dans la documentation où ils sont utilisés pour décrire des fonctionnalités Blazor, comme la configuration et la journalisation.

Un fournisseur de services personnalisé ne fournit pas automatiquement les services par défaut répertoriés dans le tableau. Si vous utilisez un fournisseur de services personnalisé et que vous avez besoin de l’un des services indiqués dans le tableau, ajoutez les services requis au nouveau fournisseur de services.

Ajouter des services côté client

Configurez les services pour la collection de services de l’application dans le fichier Program. Dans l’exemple suivant, l’implémentation ExampleDependency est inscrite pour IExampleDependency :

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<IExampleDependency, ExampleDependency>();
...

await builder.Build().RunAsync();

Une fois l’hôte généré, les services sont disponibles à partir de l’étendue de DI racine avant que tous les composants soient rendus. Cela peut être utile pour exécuter la logique d’initialisation avant le rendu du contenu :

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<WeatherService>();
...

var host = builder.Build();

var weatherService = host.Services.GetRequiredService<WeatherService>();
await weatherService.InitializeWeatherAsync();

await host.RunAsync();

L’hôte fournit une instance de configuration centrale pour l’application. À partir de l’exemple précédent, l’URL du service météo est passée d’une source de configuration par défaut (par exemple, appsettings.json) à InitializeWeatherAsync :

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<WeatherService>();
...

var host = builder.Build();

var weatherService = host.Services.GetRequiredService<WeatherService>();
await weatherService.InitializeWeatherAsync(
    host.Configuration["WeatherServiceUrl"]);

await host.RunAsync();

Ajouter des services côté serveur

Après avoir créé une application, examinez une partie du fichier Program :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();

La variable builder représente un WebApplicationBuilder avec une IServiceCollection, qui est une liste d’objets de descripteur de service. Les services sont ajoutés en fournissant des descripteurs de service à la collection de services. L’exemple suivant illustre le concept avec l’interface IDataAccess et son implémentation concrète DataAccess :

builder.Services.AddSingleton<IDataAccess, DataAccess>();

Après avoir créé une application, examinez la méthode Startup.ConfigureServices dans Startup.cs :

using Microsoft.Extensions.DependencyInjection;

...

public void ConfigureServices(IServiceCollection services)
{
    ...
}

La méthode ConfigureServices est passée à une IServiceCollection, qui est une liste d’objets de descripteur de service. Les services sont ajoutés dans la méthode ConfigureServices en fournissant des descripteurs de service à la collection de services. L’exemple suivant illustre le concept avec l’interface IDataAccess et son implémentation concrète DataAccess :

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IDataAccess, DataAccess>();
}

Inscrire des services communs

Si un ou plusieurs services communs sont nécessaires côté client et côté serveur, vous pouvez placer les inscriptions des services communs dans une méthode côté client et appeler la méthode pour inscrire les services dans les deux projets.

Tout d’abord, factorisez les inscriptions de service communes dans une méthode distincte. Par exemple, créez une méthode ConfigureCommonServices côté client :

public static void ConfigureCommonServices(IServiceCollection services)
{
    services.Add...;
}

Pour le fichier Program côté client, appelez ConfigureCommonServices pour inscrire les services communs :

var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);

Dans le fichier Program côté serveur, appelez ConfigureCommonServices pour inscrire les services communs :

var builder = WebApplication.CreateBuilder(args);

...

Client.Program.ConfigureCommonServices(builder.Services);

Pour obtenir un exemple de cette approche, consultez Scénarios de sécurité supplémentaires ASP.NET Core Blazor WebAssembly.

Services côté client qui échouent lors du prérendu

Cette section s’applique seulement aux composants WebAssembly dans les Blazor Web App.

Les Blazor Web App effectuent normalement le prérendu des composants WebAssembly côté client. Si une application est exécutée avec un service requis inscrit seulement dans le projet .Client, l’exécution de l’application génère une erreur de runtime similaire à l’erreur suivante quand un composant tente d’utiliser le service requis lors du prérendu :

InvalidOperationException : Impossible de fournir une valeur pour {PROPRIÉTÉ} sur le type « {ASSEMBLY}}.Client.Pages.{NOM DU COMPOSANT} ». Il n’existe pas de service inscrit de type « {SERVICE} ».

Pour résoudre ce problème, utilisez l’une des approches suivantes :

  • Inscrivez le service dans le projet principal pour le rendre disponible lors de la prérendu des composants.
  • Si le prérendu n’est pas requis pour le composant, désactivez le prérendu en suivant les instructions fournies dans Modes de prérendu d’ASP.NETBlazor. Si vous adoptez cette approche, vous n’avez pas besoin d’inscrire le service dans le projet principal.

Pour plus d’informations, consultez Les services côté client ne peuvent pas être résolus lors du prérendu.

Durée de vie d’un service

Les services peuvent être configurés avec les durées de vie indiquées dans le tableau suivant.

Durée de vie Description
Scoped

Pour l’heure, il n’existe pas de concept d’étendues d’injection de dépendances côté client. Les services inscrits par Scoped se comportent comme des services Singleton.

Le développement côté serveur prend en charge la durée de vie Scoped entre les requêtes HTTP, mais pas entre les messages de connexion/circuit SignalR parmi les composants qui sont chargés sur le client. La partie Razor Pages ou MVC de l’application traite normalement les services délimités et recrée les services pour chaque requête HTTP lors de la navigation entre pages ou vues, ou d’une page ou vue à un composant. Les services délimités ne sont pas reconstruits lors de la navigation entre les composants sur le client, où la communication vers le serveur a lieu sur la connexion SignalR du circuit de l’utilisateur, et non via des requêtes HTTP. Dans les scénarios de composant suivants sur le client, les services délimités sont reconstruits, car un nouveau circuit est créé pour l’utilisateur :

  • L’utilisateur ferme la fenêtre du navigateur. L’utilisateur ouvre une nouvelle fenêtre et retourne à l’application.
  • L’utilisateur ferme un onglet de l’application dans une fenêtre de navigateur. L’utilisateur ouvre un nouvel onglet et retourne à l’application.
  • L’utilisateur sélectionne le bouton Recharger/Actualiser du navigateur.

Pour plus d’informations sur la préservation de l’état utilisateur dans les applications côté serveur, consultez Gestion de l’état ASP.NET Core Blazor.

Singleton La DI crée une seule instance du service. Tous les composants nécessitant un service Singleton reçoivent la même instance du service.
Transient Chaque fois qu’un composant obtient une instance d’un service Transient à partir du conteneur de service, il reçoit une nouvelle instance du service.

Le système de DI est basé sur le système d’authentification unique dans ASP.NET Core. Pour plus d’informations, consultez Injection de dépendances dans ASP.NET Core.

Demander un service dans un composant

Pour injecter des services dans des composants, Blazor prend en charge l’injection de constructeurs et l’injection de propriétés.

Injection de constructeurs

Une fois les services ajoutés à la collection de services, injectez un ou plusieurs services dans des composants avec l’injection de constructeur. L’exemple suivant injecte le service NavigationManager.

ConstructorInjection.razor :

@page "/constructor-injection"

<button @onclick="HandleClick">
    Take me to the Counter component
</button>

ConstructorInjection.razor.cs :

using Microsoft.AspNetCore.Components;

public partial class ConstructorInjection(NavigationManager navigation)
{
    private void HandleClick()
    {
        navigation.NavigateTo("/counter");
    }
}

Injection de propriétés

Une fois les services ajoutés à la collection de services, injectez un ou plusieurs services dans les composants à l’aide de la directive @injectRazor, qui dispose de deux paramètres :

  • Type : le type du service à injecter.
  • Propriété : le nom de la propriété recevant le service d’application injecté. La propriété ne nécessite pas de création manuelle. Le compilateur crée la propriété.

Pour plus d’informations, consultez Injection de dépendances dans des vues dans ASP.NET Core.

Utilisez plusieurs instructions @inject pour injecter différents services.

L'exemple suivant montre comment utiliser la directive @inject. Le service implémentant Services.NavigationManager est injecté dans le Navigation de propriétés du composant. Vous remarquerez que le code utilise uniquement l’abstraction NavigationManager.

PropertyInjection.razor :

@page "/property-injection"
@inject NavigationManager Navigation

<button @onclick="@(() => Navigation.NavigateTo("/counter"))">
    Take me to the Counter component
</button>

En interne, la propriété générée (Navigation) utilise l’attribut [Inject]. En règle générale, cet attribut n’est pas utilisé directement. Si une classe de base est requise pour les composants et que des propriétés injectées sont également requises pour la classe de base, ajoutez manuellement l’attribut [Inject] :

using Microsoft.AspNetCore.Components;

public class ComponentBase : IComponent
{
    [Inject]
    protected NavigationManager Navigation { get; set; } = default!;

    ...
}

Remarque

Comme les services injectés sont censés être disponibles, le littéral par défaut avec l’opérateur null-forgiving (default!) est affecté dans .NET 6 ou version ultérieure. Pour plus d’informations, consultez Analyse statique des types de référence nullables (NRT) et de l’état null du compilateur .NET.

Dans les composants dérivés de la classe de base, la directive @inject n’est pas obligatoire. Le InjectAttribute de la classe de base est suffisant. Le composant nécessite uniquement la directive @inherits. Dans l’exemple suivant, tous les services injectés de CustomComponentBase sont disponibles pour le composant Demo :

@page "/demo"
@inherits CustomComponentBase

Utiliser la DI dans les services

Les services complexes peuvent nécessiter des services supplémentaires. Dans l’exemple suivant, DataAccess nécessite le service HttpClient par défaut. @inject (ou l’attribut [Inject]) n’est pas disponible pour une utilisation dans les services. L’injection de constructeur doit être utilisée à la place. Les services requis sont ajoutés en ajoutant des paramètres au constructeur du service. Lorsque la DI crée le service, elle reconnaît les services nécessaires dans le constructeur et les fournit en conséquence. Dans l’exemple suivant, le constructeur reçoit HttpClient via la DI. HttpClient est un service par défaut.

using System.Net.Http;

public class DataAccess : IDataAccess
{
    public DataAccess(HttpClient http)
    {
        ...
    }

    ...
}

L’injection de constructeur est prise en charge avec les constructeurs principaux en C# 12 (.NET 8) ou version ultérieure :

using System.Net.Http;

public class DataAccess(HttpClient http) : IDataAccess
{
    ...
}

Prérequis pour l’injection de constructeur :

  • Il doit exister un constructeur dont les arguments peuvent tous être remplis par la DI. Les paramètres supplémentaires non couverts par la DI sont autorisés s’ils spécifient des valeurs par défaut.
  • Le constructeur applicable doit être public.
  • Un constructeur applicable doit exister. En cas d’ambiguïté, la DI lève une exception.

Injecter des services à clé dans les composants

Blazor prend en charge l’injection de services à clé à l’aide de l’attribut [Inject]. Les clés permettent de définir l’étendue de l'inscription et de la consommation des services lors de l'utilisation de l'injection de dépendances. Utilisez la propriété InjectAttribute.Key pour spécifier la clé du service à injecter :

[Inject(Key = "my-service")]
public IMyService MyService { get; set; }

Classes de composants de base de l’utilitaire pour gérer une étendue de DI

Dans les applications ASP.NET Core non-Blazor, les services délimités et temporaires sont généralement limités à la requête actuelle. Une fois la requête terminée, les services délimités et temporaires sont supprimés par le système de DI.

Dans les applications Blazor côté serveur interactives, l’étendue de l’injection de dépendances couvre la durée du circuit (la connexion SignalR entre le client et le serveur), ce qui peut allonger la durée de vie des services délimités et temporaires supprimables bien au-delà de la durée de vie d’un seul composant. Par conséquent, n’injectez pas directement un service délimité dans un composant si vous voulez que la durée de vie du service corresponde à la durée de vie du composant. Les services temporaires injectés dans un composant qui n’implémentent pas IDisposable sont supprimés quand le composant est supprimé. Cependant, les services temporaires injectés qui implémentent IDisposable sont gérés par le conteneur d’injection de dépendances pendant la durée de vie du circuit, ce qui empêche la garbage collection du service quand le composant est supprimé et aboutit à une fuite de mémoire. Une approche alternative pour les services délimités en fonction du type OwningComponentBase est décrite plus loin dans cette section, et les services temporaires supprimables ne doivent pas être utilisés du tout. Pour plus d’informations, consultez Concevoir pour résoudre les éléments temporaires supprimables sur Blazor Server (dotnet/aspnetcore #26676).

Même dans les applications Blazor côté client qui ne fonctionnent pas sur un circuit, les services inscrits avec une durée de vie délimitée sont traités comme des singletons ; leur durée de vie est donc plus longue que celle des services délimités dans les applications ASP.NET Core classiques. Les services temporaires supprimables côté client vivent également plus longtemps que les composants dans lesquels ils sont injectés, car le conteneur d’injection de dépendances, qui contient des références aux services supprimables, existe pendant toute la durée de vie de l’application, empêchant la garbage collection sur les services. Bien que les services temporaires supprimables avec une durée de vie longue soient plus préoccupants sur le serveur, vous devez aussi les éviter en tant qu’inscriptions de service client. L’utilisation du type OwningComponentBase est également recommandée pour les services délimités côté client pour contrôler la durée de vie du service, et les services temporaires supprimables ne doivent pas être utilisés du tout.

Une approche qui limite la durée de vie d’un service est l’utilisation du type OwningComponentBase. OwningComponentBase est un type abstrait dérivé de ComponentBase qui crée une étendue de DI correspondant à la durée de vie du composant. En utilisant cette étendue, un composant peut injecter des services ayant une durée de vie délimitée et faire en sorte que leur durée de vie corresponde à celle du composant. Lorsque le composant est détruit, les services du fournisseur de services délimités du composant sont également supprimés. Ceci peut être utile pour les services réutilisés au sein d’un composant mais pas partagés entre des composants.

Deux versions de type OwningComponentBase sont disponibles et décrites dans les deux sections suivantes :

OwningComponentBase

OwningComponentBase est un enfant abstrait et jetable du type ComponentBase avec une propriété ScopedServices protégée de type IServiceProvider. Le fournisseur peut être utilisé pour résoudre les services qui sont limités à la durée de vie du composant.

Les services de DI injectés dans le composant à l’aide de @inject ou de l’attribut [Inject] ne sont pas créés dans l’étendue du composant. Pour utiliser l’étendue du composant, les services doivent être résolus à l’aide de ScopedServices avec GetRequiredService ou GetService. Tous les services résolus à l’aide du fournisseur ScopedServices ont leurs dépendances fournies dans l’étendue du composant.

L’exemple suivant illustre la différence entre l’injection directe d’un service délimité et la résolution d’un service en utilisant ScopedServices sur le serveur. L’interface et l’implémentation suivantes pour une classe de voyage dans le temps incluent une propriété DT pour contenir une valeur DateTime. L’implémentation appelle DateTime.Now pour définir DT lorsque la classe TimeTravel est instanciée.

ITimeTravel.cs :

public interface ITimeTravel
{
    public DateTime DT { get; set; }
}

TimeTravel.cs :

public class TimeTravel : ITimeTravel
{
    public DateTime DT { get; set; } = DateTime.Now;
}

Le service est inscrit comme étant délimité dans le fichier Program côté serveur. Côté serveur, les services délimités ont une durée de vie égale à la durée du circuit.

Dans le fichier Program :

builder.Services.AddScoped<ITimeTravel, TimeTravel>();

Dans le composant TimeTravel suivant :

  • Le service de voyage dans le temps est directement injecté avec @inject en tant que TimeTravel1.
  • Le service est également résolu séparément avec ScopedServices et GetRequiredService en tant que TimeTravel2.

TimeTravel.razor :

@page "/time-travel"
@inject ITimeTravel TimeTravel1
@inherits OwningComponentBase

<h1><code>OwningComponentBase</code> Example</h1>

<ul>
    <li>TimeTravel1.DT: @TimeTravel1?.DT</li>
    <li>TimeTravel2.DT: @TimeTravel2?.DT</li>
</ul>

@code {
    private ITimeTravel TimeTravel2 { get; set; } = default!;

    protected override void OnInitialized()
    {
        TimeTravel2 = ScopedServices.GetRequiredService<ITimeTravel>();
    }
}
@page "/time-travel"
@inject ITimeTravel TimeTravel1
@inherits OwningComponentBase

<h1><code>OwningComponentBase</code> Example</h1>

<ul>
    <li>TimeTravel1.DT: @TimeTravel1?.DT</li>
    <li>TimeTravel2.DT: @TimeTravel2?.DT</li>
</ul>

@code {
    private ITimeTravel TimeTravel2 { get; set; } = default!;

    protected override void OnInitialized()
    {
        TimeTravel2 = ScopedServices.GetRequiredService<ITimeTravel>();
    }
}

Navigant initialement vers le composant TimeTravel, le service de voyage dans le temps est instancié deux fois lorsque le composant se charge, et TimeTravel1 et TimeTravel2 ont la même valeur initiale :

TimeTravel1.DT: 8/31/2022 2:54:45 PM
TimeTravel2.DT: 8/31/2022 2:54:45 PM

Lorsque vous naviguez loin du composant TimeTravel pour un autre composant et revenez au composant TimeTravel :

  • TimeTravel1 reçoit la même instance de service que celle créée lors du premier chargement du composant ; la valeur de DT reste donc la même.
  • TimeTravel2 obtient une nouvelle instance de service ITimeTravel dans TimeTravel2 avec une nouvelle valeur DT.

TimeTravel1.DT: 8/31/2022 2:54:45 PM
TimeTravel2.DT: 8/31/2022 2:54:48 PM

TimeTravel1 est lié au circuit de l’utilisateur, qui reste intact et n’est pas supprimé tant que le circuit sous-jacent n’est pas déconstruit. Par exemple, le service est supprimé si le circuit est déconnecté pendant la période de rétention du circuit déconnecté.

Malgré l’inscription de service délimité dans le fichier Program et la longévité du circuit de l’utilisateur, TimeTravel2 reçoit une nouvelle instance de service ITimeTravel chaque fois que le composant est initialisé.

OwningComponentBase<TService>

OwningComponentBase<TService> dérive de OwningComponentBase et ajoute une propriété Service qui retourne une instance de T à partir du fournisseur de DI délimitée. Ce type est un moyen pratique d’accéder aux services délimités sans utiliser d’instance de IServiceProvider quand l’application a besoin d’un service principal à partir du conteneur de DI avec l’étendue du composant. La propriété ScopedServices est disponible, afin que l’application puisse obtenir des services d’autres types, si nécessaire.

@page "/users"
@attribute [Authorize]
@inherits OwningComponentBase<AppDbContext>

<h1>Users (@Service.Users.Count())</h1>

<ul>
    @foreach (var user in Service.Users)
    {
        <li>@user.UserName</li>
    }
</ul>

Détecter les éléments jetables temporaires côté client

Vous pouvez ajouter du code personnalisé à une application Blazor côté client pour détecter les services temporaires jetables dans une application qui doit utiliser OwningComponentBase. Cette approche est utile si vous craignez que le code ajouté par la suite à l’application consomme un ou plusieurs services temporaires jetables, notamment des services ajoutés par des bibliothèques. Le code de démonstration est disponible dans les exemples de Blazorréférentiel GitHub des échantillons (comment télécharger).

Inspectez ce qui suit dans les versions .NET 6 et ultérieures de l’exemple BlazorSample_WebAssembly :

  • DetectIncorrectUsagesOfTransientDisposables.cs
  • Services/TransientDisposableService.cs
  • Dans : Program.cs
    • L’espace de noms Services de l’application est fourni en haut du fichier (using BlazorSample.Services;).
    • DetectIncorrectUsageOfTransients est appelé immédiatement après l’attribution de builder à partir de WebAssemblyHostBuilder.CreateDefault.
    • TransientDisposableService est inscrit (builder.Services.AddTransient<TransientDisposableService>();).
    • EnableTransientDisposableDetection est appelé sur l’hôte généré dans le pipeline de traitement de l’application (host.EnableTransientDisposableDetection();).
  • L’application inscrit le service TransientDisposableService sans lever d’exception. Toutefois, la tentative de résolution du service dans TransientService.razor lève InvalidOperationException quand l’infrastructure tente de construire une instance de TransientDisposableService.

Détecter les éléments jetables temporaires côté serveur

Vous pouvez ajouter du code personnalisé à une application Blazor côté serveur pour détecter les services temporaires jetables côté serveur dans une application qui doit utiliser OwningComponentBase. Cette approche est utile si vous craignez que le code ajouté par la suite à l’application consomme un ou plusieurs services temporaires jetables, notamment des services ajoutés par des bibliothèques. Le code de démonstration est disponible dans les exemples de Blazorréférentiel GitHub des échantillons (comment télécharger).

Inspectez ce qui suit dans les versions .NET 8 et ultérieures de l’exemple BlazorSample_BlazorWebApp :

Inspectez ce qui suit dans la version .NET 6 ou .NET 7 de l’exemple BlazorSample_Server :

  • DetectIncorrectUsagesOfTransientDisposables.cs
  • Services/TransitiveTransientDisposableDependency.cs :
  • Dans : Program.cs
    • L’espace de noms Services de l’application est fourni en haut du fichier (using BlazorSample.Services;).
    • DetectIncorrectUsageOfTransients est appelé sur le générateur d’hôte (builder.DetectIncorrectUsageOfTransients();).
    • Le service TransientDependency est inscrit (builder.Services.AddTransient<TransientDependency>();).
    • TransitiveTransientDisposableDependency est inscrit pour ITransitiveTransientDisposableDependency (builder.Services.AddTransient<ITransitiveTransientDisposableDependency, TransitiveTransientDisposableDependency>();).
  • L’application inscrit le service TransientDependency sans lever d’exception. Toutefois, la tentative de résolution du service dans TransientService.razor lève InvalidOperationException quand l’infrastructure tente de construire une instance de TransientDependency.

Inscriptions de services temporaires pour les gestionnaires IHttpClientFactory/HttpClient

Les inscriptions de service temporaires pour les gestionnaires IHttpClientFactory/HttpClient sont recommandées. Si l’application contient des gestionnaires IHttpClientFactory/HttpClient et utilise IRemoteAuthenticationBuilder<TRemoteAuthenticationState,TAccount> pour ajouter la prise en charge de l’authentification, les éléments temporaires supprimables suivants pour l’authentification côté client sont également découverts, ce qui est attendu et peut être ignoré :

D’autres instances de IHttpClientFactory/HttpClient sont également découvertes. Ces instances peuvent également être ignorées.

Les exemples d’applications Blazor dans le Blazorréférentiel GitHub d’échantillon (comment télécharger) illustrent le code pour détecter les jetables temporaires. Toutefois, le code est désactivé, car les exemples d’applications incluent des gestionnaires IHttpClientFactory/HttpClient.

Pour activer le code de démonstration et voir son fonctionnement :

  • Supprimez les marques de commentaire des lignes correspondant aux éléments supprimables temporaires dans Program.cs.

  • Supprimez la vérification conditionnelle dans NavLink.razor qui empêche le composant TransientService de s’afficher dans la barre latérale de navigation de l’application :

    - else if (name != "TransientService")
    + else
    
  • Exécutez l’exemple d’application et accédez au composant TransientService dans /transient-service.

Utilisation d’un DbContext Entity Framework Core (EF Core) à partir de la DI

Pour plus d’informations, consultez ASP.NET Core Blazor avec Entity Framework Core (EF Core).

Accéder aux services Blazor côté serveur à partir d’une étendue d’injection de dépendances différente

Les gestionnaires d’activités de circuit proposent une approche pour accéder aux services Blazor délimités à partir d’autres étendues d’injection de dépendances non Blazor, telles que les étendues créées à l’aide de IHttpClientFactory.

Avant la sortie d’ASP.NET Core dans .NET 8, l’accès aux services limités à un circuit à partir d’autres étendues d’injection de dépendances nécessitait l’utilisation d’un type de composant de base personnalisé. Avec les gestionnaires d’activités de circuit, il n’est pas nécessaire d’utiliser un type de composant de base personnalisé, comme le montre l’exemple suivant :

public class CircuitServicesAccessor
{
    static readonly AsyncLocal<IServiceProvider> blazorServices = new();

    public IServiceProvider? Services
    {
        get => blazorServices.Value;
        set => blazorServices.Value = value;
    }
}

public class ServicesAccessorCircuitHandler(
    IServiceProvider services, CircuitServicesAccessor servicesAccessor) 
    : CircuitHandler
{
    public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
        Func<CircuitInboundActivityContext, Task> next) => 
            async context =>
            {
                servicesAccessor.Services = services;
                await next(context);
                servicesAccessor.Services = null;
            };
}

public static class CircuitServicesServiceCollectionExtensions
{
    public static IServiceCollection AddCircuitServicesAccessor(
        this IServiceCollection services)
    {
        services.AddScoped<CircuitServicesAccessor>();
        services.AddScoped<CircuitHandler, ServicesAccessorCircuitHandler>();

        return services;
    }
}

Accédez aux services limités à un circuit en injectant le CircuitServicesAccessor là où il est nécessaire.

Pour voir un exemple qui montre comment accéder à AuthenticationStateProvider à partir d’un DelegatingHandler configuré à l’aide de IHttpClientFactory, consultez Autres scénarios de sécurité ASP.NET Core Blazor côté serveur.

Il peut arriver qu’un composant Razor appelle des méthodes asynchrones qui exécutent du code dans une autre étendue de DI. Sans l’approche appropriée, ces étendues de DI n’ont pas accès aux services de Blazor, comme IJSRuntime et Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.

Par exemple, les instances HttpClient créées à l’aide de IHttpClientFactory ont leur propre étendue de service DI. Par conséquent, les instances HttpMessageHandler configurées sur le HttpClient ne peuvent pas injecter directement des services Blazor.

Créez une classe BlazorServiceAccessor qui définit un AsyncLocal, qui stocke BlazorIServiceProvider pour le contexte asynchrone actuel. Une instance BlazorServiceAccessor peut être acquise à partir d’une autre étendue de service DI pour accéder aux services Blazor.

BlazorServiceAccessor.cs :

internal sealed class BlazorServiceAccessor
{
    private static readonly AsyncLocal<BlazorServiceHolder> s_currentServiceHolder = new();

    public IServiceProvider? Services
    {
        get => s_currentServiceHolder.Value?.Services;
        set
        {
            if (s_currentServiceHolder.Value is { } holder)
            {
                // Clear the current IServiceProvider trapped in the AsyncLocal.
                holder.Services = null;
            }

            if (value is not null)
            {
                // Use object indirection to hold the IServiceProvider in an AsyncLocal
                // so it can be cleared in all ExecutionContexts when it's cleared.
                s_currentServiceHolder.Value = new() { Services = value };
            }
        }
    }

    private sealed class BlazorServiceHolder
    {
        public IServiceProvider? Services { get; set; }
    }
}

Pour définir automatiquement la valeur de BlazorServiceAccessor.Services lorsqu’une méthode de composant async est appelée, créez un composant de base personnalisé qui implémente à nouveau les trois points d’entrée asynchrones principaux dans le code de composant Razor :

La classe suivante illustre l’implémentation du composant de base.

CustomComponentBase.cs :

using Microsoft.AspNetCore.Components;

public class CustomComponentBase : ComponentBase, IHandleEvent, IHandleAfterRender
{
    private bool hasCalledOnAfterRender;

    [Inject]
    private IServiceProvider Services { get; set; } = default!;

    [Inject]
    private BlazorServiceAccessor BlazorServiceAccessor { get; set; } = default!;

    public override Task SetParametersAsync(ParameterView parameters)
        => InvokeWithBlazorServiceContext(() => base.SetParametersAsync(parameters));

    Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
        => InvokeWithBlazorServiceContext(() =>
        {
            var task = callback.InvokeAsync(arg);
            var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
                task.Status != TaskStatus.Canceled;

            StateHasChanged();

            return shouldAwaitTask ?
                CallStateHasChangedOnAsyncCompletion(task) :
                Task.CompletedTask;
        });

    Task IHandleAfterRender.OnAfterRenderAsync()
        => InvokeWithBlazorServiceContext(() =>
        {
            var firstRender = !hasCalledOnAfterRender;
            hasCalledOnAfterRender |= true;

            OnAfterRender(firstRender);

            return OnAfterRenderAsync(firstRender);
        });

    private async Task CallStateHasChangedOnAsyncCompletion(Task task)
    {
        try
        {
            await task;
        }
        catch
        {
            if (task.IsCanceled)
            {
                return;
            }

            throw;
        }

        StateHasChanged();
    }

    private async Task InvokeWithBlazorServiceContext(Func<Task> func)
    {
        try
        {
            BlazorServiceAccessor.Services = Services;
            await func();
        }
        finally
        {
            BlazorServiceAccessor.Services = null;
        }
    }
}

Tous les composants qui étendent CustomComponentBase automatiquement ont BlazorServiceAccessor.Services défini sur le IServiceProvider dans l’étendue de DI Blazor actuelle.

Enfin, dans le fichier Program, ajoutez le BlazorServiceAccessor en tant que service délimité :

builder.Services.AddScoped<BlazorServiceAccessor>();

Enfin, dans la méthode Startup.ConfigureServices de Startup.cs, ajoutez le BlazorServiceAccessor en tant que service délimité :

services.AddScoped<BlazorServiceAccessor>();

Ressources supplémentaires