Abhängigkeitsinjektion in ASP.NET Core Blazor

Hinweis

Dies ist nicht die neueste Version dieses Artikels. Die aktuelle Version finden Sie in der .NET 10-Version dieses Artikels.

Warnung

Diese Version von ASP.NET Core wird nicht mehr unterstützt. Weitere Informationen finden Sie in der .NET- und .NET Core-Supportrichtlinie. Die aktuelle Version finden Sie in der .NET 10-Version dieses Artikels.

Von Rainer Stropek und Mike Rousos

In diesem Artikel wird erläutert, wie Blazor-Apps Dienste in Komponenten einbinden können.

Abhängigkeitsinjektion (DI) ist eine Technik für den Zugriff auf Dienste, die an einer zentralen Stelle konfiguriert werden:

  • Dienste, die beim Framework registriert wurden, können direkt in Razor-Komponenten injiziert werden.
  • Blazor-Apps definieren und registrieren benutzerdefinierte Dienste und machen sie über DI in der gesamten App verfügbar.

Hinweis

Lesen Sie zunächst Dependency Injection in ASP.NET Core, ehe Sie dieses Thema lesen.

Standarddienste

Die in der folgenden Tabelle aufgeführten Dienste werden häufig in Blazor-Apps verwendet.

Dienst Lebensdauer Beschreibung
HttpClient Bereichsbezogen

Stellt Methoden zum Senden von HTTP-Anforderungen und Empfangen von HTTP-Antworten aus einer Ressource bereit, die von einem URI identifiziert wird.

Clientseitig wird eine Instanz von HttpClient durch die App in der Program-Datei registriert und verwendet den Browser zur Abwicklung des HTTP-Datenverkehrs im Hintergrund.

Serverseitig ist nicht standardmäßig ein HttpClient als Dienst konfiguriert. Geben Sie im serverseitigen Code einen HttpClient an.

Weitere Informationen finden Sie unter Aufrufen einer Web-API über eine ASP.NET Core Blazor-App.

HttpClient wird als bereichsbezogener Dienst, nicht als Singleton registriert. Weitere Informationen finden Sie im Abschnitt zur Dienstlebensdauer.

IJSRuntime

clientseitig: Singleton

Serverseitig: Bereichsbezogen

Das Blazor Framework registriert IJSRuntime im Dienstcontainer der App.

Stellt eine Instanz einer JavaScript-Laufzeit dar, in der JavaScript-Aufrufe verteilt werden. Weitere Informationen finden Sie unter Aufrufen von JavaScript-Funktionen über .NET-Methoden in Blazor in ASP.NET Core.

Wenn Sie den Dienst in einen Singletondienst auf dem Server einbinden möchten, wählen Sie einen der folgenden Ansätze:

  • Ändern Sie die Dienstregistrierung so, dass sie mit der Registrierung von IJSRuntime übereinstimmt. Dies ist sinnvoll, wenn der Dienst mit benutzerspezifischen Zuständen umgehen soll.
  • Übergeben Sie IJSRuntime an die Implementierung des Singletondiensts als Argument seiner Methodenaufrufe, anstatt das Element in das Singleton einzufügen.
NavigationManager

Clientseitig: Singleton

Serverseitig: Begrenzt

Das Blazor Framework registriert NavigationManager im Dienstcontainer der App.

Enthält Hilfsprogramme für die Arbeit mit URIs und dem Navigationszustand. Weitere Informationen finden Sie unter URI- und Navigationszustands-Hilfsfunktionen.

Zusätzliche vom Blazor Framework registrierte Dienste werden in der Dokumentation beschrieben, in der sie verwendet werden, um Blazor Features zu beschreiben, z. B. Konfiguration und Protokollierung.

Ein benutzerdefinierter Dienstanbieter stellt nicht automatisch die in der Tabelle aufgeführten Standarddienste bereit. Wenn Sie einen benutzerdefinierten Dienstanbieter verwenden und einen der Dienste benötigen, die in der Tabelle angezeigt werden, fügen Sie dem neuen Dienstanbieter die erforderlichen Dienste hinzu.

Hinzufügen clientseitiger Dienste

Konfigurieren Sie Dienste für die Dienstsammlung der App in der Program-Datei. Im folgenden Beispiel ist die ExampleDependency-Implementierung für IExampleDependency registriert:

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

await builder.Build().RunAsync();

Nachdem der Host erstellt wurde, stehen Dienste im Stammbereich der Abhängigkeitsinjektion zur Verfügung, bevor Komponenten gerendert werden. Dies kann für die Ausführung der Initialisierungslogik vor dem Rendern von Inhalten nützlich sein:

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();

Der Host stellt eine zentrale Konfigurationsinstanz für die App zur Verfügung. Ausgehend vom vorhergehenden Beispiel wird die URL des Wetterdiensts von einer Standardkonfigurationsquelle (z. B. appsettings.json) an InitializeWeatherAsync übergeben:

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();

Hinzufügen serverseitiger Dienste

Nachdem Sie eine neue App erstellt haben, untersuchen Sie einen Teil der Program-Datei:

var builder = WebApplication.CreateBuilder(args);

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

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

Die builder-Variable stellt einen WebApplicationBuilder mit einer IServiceCollection dar. Dabei handelt es sich um eine Liste von Dienstdeskriptorobjekten. Dienste werden durch die Bereitstellung von Dienstdeskriptoren in die Dienstsammlung aufgenommen. Das folgende Beispiel veranschaulicht das Konzept mit der IDataAccess-Schnittstelle und seiner konkreten Implementierung DataAccess:

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

Nachdem Sie eine neue App erstellt haben, sehen Sie sich die Startup.ConfigureServices-Methode in Startup.cs an:

using Microsoft.Extensions.DependencyInjection;

...

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

Der Methode ConfigureServices wird ein IServiceCollection-Objekt übergeben, das eine Liste von Dienstdeskriptorobjekten ist. Dienste werden in der ConfigureServices-Methode durch Bereitstellung von Dienstdeskriptoren in der Dienstsammlung hinzugefügt. Das folgende Beispiel veranschaulicht das Konzept mit der IDataAccess-Schnittstelle und seiner konkreten Implementierung DataAccess:

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

Registrieren allgemeiner Dienste

Wenn einer oder mehrere allgemeine Dienste client- und serverseitig benötigt werden, können Sie die allgemeinen Registrierungen der Dienste in einer clientseitigen Methode platzieren und die Methode aufrufen, um die Dienste in beiden Projekten zu registrieren.

Integrieren Sie zunächst die gemeinsamen Dienstregistrierungen in einer separaten Methode. Erstellen Sie zum Beispiel clientseitig eine ConfigureCommonServices-Methode:

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

Rufen Sie für die clientseitige Program-Datei ConfigureCommonServices auf, um die allgemeinen Dienste zu registrieren:

var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);

Rufen Sie in der serverseitigen Program-Datei ConfigureCommonServices auf, um die allgemeinen Dienste zu registrieren:

var builder = WebApplication.CreateBuilder(args);

...

Client.Program.ConfigureCommonServices(builder.Services);

Ein Beispiel für diesen Ansatz finden Sie unter Zusätzliche Sicherheitsszenarien für ASP.NET Core Blazor WebAssembly.

Clientseitige Dienste, bei denen beim Prerendering ein Fehler auftritt

Dieser Abschnitt gilt nur für WebAssembly-Komponenten in Blazor Web Apps.

Blazor Web Apps führen in der Regel das Prerendering für clientseitige WebAssembly-Komponenten durch. Die Ausführung einer App mit einem erforderlichen Dienst, der nur im .Client-Projekt registriert ist, kann zu einem Laufzeitfehler ähnlich dem folgenden führen, wenn eine Komponente versucht, den erforderlichen Dienst während des Prerenderingvorgangs zu verwenden:

InvalidOperationException: Für {PROPERTY} des Typs '{ASSEMBLY}}.Client.Pages.{COMPONENT NAME}' kann kein Wert angegeben werden. Es gibt keinen registrierten Dienst vom Typ '{DIENST}'.

Verwenden Sie einen der folgenden Ansätze, um das Problem zu beheben:

  • Registrieren Sie den Dienst im Hauptprojekt, um ihn während des Prerenderingvorgangs für die Komponente verfügbar zu machen.
  • Wenn die Voreinstellung für die Komponente nicht erforderlich ist, deaktivieren Sie die Voreinstellung, indem Sie den Anweisungen in Prerender ASP.NET Core-Komponenten Razorfolgen. Wenn Sie diesen Ansatz übernehmen, müssen Sie den Dienst nicht im Hauptprojekt registrieren.

Weitere Informationen finden Sie im Abschnitt Client-side services fail to resolve during prerendering des Prerendering-Artikels, der später in der Blazor Dokumentation erscheint.

Lebensdauer von Diensten

Die Dienste können mit den in der folgenden Tabelle angegebenen Lebensdauern konfiguriert werden.

Lebensdauer Beschreibung
Scoped

Clientseitig gibt es derzeit kein Konzept von DI-Scopes. Scoped-registrierte Dienste verhalten sich wie Singleton-Dienste.

Die serverseitige Entwicklung unterstützt die Scoped-Lebensdauer über HTTP-Anforderungen hinweg, jedoch nicht über SignalR-Verbindungs-/Circuit-Nachrichten zwischen Komponenten, die auf dem Client geladen werden. Beim Navigieren zwischen Seiten oder Ansichten oder von einer Seite oder Ansicht zu einer Komponente verarbeitet der Razor Pages- bzw. MVC-Teil der App bereichsbezogene Dienste normal und erstellt die Dienste für jede HTTP-Anforderung neu. Beim Navigieren zwischen Komponenten auf dem Client werden bereichsbezogene Dienste nicht neu erstellt. In diesem Fall erfolgt die Kommunikation mit dem Server über die SignalR-Verbindung der Benutzerleitung, nicht über HTTP-Anforderungen. In den folgenden Komponentenszenarien auf dem Client werden bereichsbezogene Dienste neu erstellt, weil für den Benutzer eine neue Leitung erstellt wird:

  • Der Benutzer schließt das Browserfenster. Der Benutzer öffnet ein neues Fenster und navigiert zur App zurück.
  • Der*die Benutzer*in schließt einen Tab der App in einem Browserfenster. Der Benutzer öffnet eine neue Registerkarte und navigiert zur App zurück.
  • Der Benutzer wählt die Schaltfläche des Browsers zum erneuten Laden oder Aktualisieren aus.

Weitere Informationen zum Beibehalten des Benutzerstatus in serverseitigen Apps finden Sie unter ASP.NET Core Blazor State Management Overview und ASP.NET Core Blazor serverseitige Zustandsverwaltung.

Singleton Die Abhängigkeitsinjektion erstellt eine einzige Instanz des Diensts. Alle Komponenten, die einen Singleton-Dienst erfordern, erhalten dieselbe Instanz des Diensts.
Transient Immer wenn eine Komponente eine Instanz eines Transient-Diensts aus dem Dienstcontainer erhält, erhält sie eine neue Instanz des Diensts.

Das DI-System basiert auf dem DI-System in ASP.NET Core. Weitere Informationen finden Sie unter Abhängigkeitsinjektion in ASP.NET Core.

Anfordern eines Diensts in einer Komponente

Zum Einfügen von Diensten in Komponenten unterstützt BlazorKonstrukturinjektion und Eigenschafteninjektion.

Konstruktorinjektion

Nachdem die Dienste der Dienstsammlung hinzugefügt wurden, injizieren Sie einen oder mehrere Dienste per Konstruktorinjektion in Komponenten. Im folgenden Beispiel wird der Dienst NavigationManager eingefügt.

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");
    }
}

Eigenschaftsinjektion

Nachdem die Dienste der Dienstsammlung hinzugefügt wurden, fügen Sie einen oder mehrere Dienste mit der @injectRazor-Anweisung ein, die zwei Parameter hat:

  • Typ: Der Typ des einzufügenden Diensts.
  • Eigenschaft: Der Name der Eigenschaft, die den eingefügten App-Dienst erhält. Die Eigenschaft erfordert keine manuelle Erstellung. Der Compiler erstellt die Eigenschaft.

Weitere Informationen finden Sie unter Abhängigkeitsinjektion in Ansichten in ASP.NET Core.

Verwenden Sie mehrere @inject-Anweisungen, um verschiedene Dienste einzufügen.

Im folgenden Beispiel wird die Verwendung der @inject-Anweisung erläutert. Der Dienst, der Services.NavigationManager implementiert, wird in die Eigenschaft Navigation der Komponente eingefügt. Beachten Sie, dass der Code nur die Abstraktion NavigationManager verwendet.

PropertyInjection.razor:

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

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

Intern verwendet die generierte Eigenschaft (Navigation) das [Inject]-Attribut. In der Regel wird dieses Attribut nicht direkt verwendet. Wenn eine Basisklasse für Komponenten erforderlich ist und die injizierten Eigenschaften auch für die Basisklasse erforderlich sind, fügen Sie das Attribut [Inject] manuell hinzu:

using Microsoft.AspNetCore.Components;

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

    ...
}

Hinweis

Da injizierte Dienste voraussichtlich verfügbar sind, wird in .NET 6 oder höher das default-Literal mit dem Nullvergebungsoperator (default!) zugewiesen. Weitere Informationen finden Sie unter Nullable-Verweistypen (NRTs) und statische Analyse des .NET-Compilers mit NULL-Status.

In Komponenten, die von einer Basisklasse abgeleitet sind, ist die @inject-Anweisung nicht erforderlich. Das InjectAttribute der Basisklasse ist ausreichend. Die Komponente erfordert nur die @inherits-Anweisung. Im folgenden Beispiel sind alle eingefügten Dienste der CustomComponentBase für die Demo-Komponente verfügbar:

@page "/demo"
@inherits CustomComponentBase

Dienstinjektion über eine Top-Level-Importdatei (_Imports.razor)

Dieser Abschnitt gilt nur für Blazor Web Apps.

Eine Importdatei auf oberster Ebene im Ordner Components (Components/_Imports.razor) fügt ihre Verweise in alle Komponenten in der Ordnerhierarchie ein, die die App-Komponente (App.razor) enthält. Die App Komponente wird immer statisch gerendert, auch wenn die Voreinstellung einer Seitenkomponente deaktiviert ist. Daher führt das Einfügen von Diensten über die Importdatei der obersten Ebene dazu, zwei Instanzen des Diensts in Seitenkomponenten aufzulösen.

Um dieses Szenario zu beheben, fügen Sie den Dienst in eine neue Importdatei ein, die im Ordner Pages (Components/Pages/_Imports.razor) platziert wird. Von diesem Speicherort aus wird der Dienst nur einmal in Seitenkomponenten aufgelöst.

DI in Diensten verwenden

Komplexe Dienste können zusätzliche Dienste erfordern. Im folgenden Beispiel ist für DataAccess der HttpClient-Standarddienst erforderlich. @inject (oder das Attribut [Inject]) ist nicht für die Verwendung in Diensten verfügbar. Stattdessen muss die Konstruktorinjektion verwendet werden. Erforderliche Dienste werden hinzugefügt, indem dem Konstruktor des Dienstes Parameter hinzugefügt werden. Wenn DI den Dienst erstellt, identifiziert sie die im Konstruktor benötigten Dienste und stellt sie entsprechend bereit. Im folgenden Beispiel erhält der Konstruktor per DI ein HttpClient. HttpClient ist ein Standarddienst.

using System.Net.Http;

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

    ...
}

Die Konstruktorinjektion wird von primären Konstruktoren in C# 12 (.NET 8) oder höher unterstützt:

using System.Net.Http;

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

Voraussetzungen für die Konstruktorinjektion:

  • Es muss einen Konstruktor geben, dessen Argumente alle durch DI aufgelöst werden können. Zusätzliche Parameter, die nicht durch DI abgedeckt sind, sind zulässig, wenn sie Standardwerte festlegen.
  • Der anwendbare Konstruktor muss public sein.
  • Es muss ein anwendbarer Konstruktor vorhanden sein. Bei einer Mehrdeutigkeit löst DI eine Ausnahme aus.

Einfügen von schlüsselbasierten Diensten in Komponenten

Blazor unterstützt das Einfügen schlüsselbasierter Dienste mithilfe des [Inject]-Attributs. Schlüssel ermöglichen es, bei der Abhängigkeitsinjektion die Registrierung und Nutzung von Diensten einzugrenzen. Verwenden Sie die InjectAttribute.Key-Eigenschaft, um den Schlüssel für den Dienst anzugeben, der eingefügt werden soll:

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

Hilfs-Basis-Komponentenklassen zur Verwaltung eines DI-Scopes

In ASP.NET Core-Apps ohne Blazor werden bereichsbezogene und vorübergehende Dienste in der Regel der aktuellen Anforderung zugeordnet. Nachdem die Anfrage abgeschlossen ist, werden bereichsgebundene und transiente Dienste vom DI-System freigegeben.

In interaktiven serverseitigen Blazor-Apps ist der DI-Bereich für die Dauer der Leitung (die SignalR-Verbindung zwischen Client und Server) gültig. Dies kann dazu führen, dass die Lebensdauer bereichsbezogener und verwerfbarer vorübergehender Dienste viel länger ist als die für eine einzelne Komponente erwartete Lebensdauer. Fügen Sie daher einen bereichsbezogenen Dienst nicht direkt in eine Komponente ein, wenn Sie beabsichtigen, die Dienstlebensdauer auf die Lebensdauer der Komponente abzustimmen. Für vorübergehende Dienste, die in eine Komponente eingefügt werden, die nicht IDisposable implementieren, wird beim Verwerfen der Komponente eine Garbage Collection ausgeführt. Eingefügte vorübergehende Dienste, die IDisposable implementieren, werden jedoch für die Lebensdauer der Leitung vom DI-Container beibehalten. Dadurch wird die Garbage Collection für den Dienst beim Verwerfen der Komponente verhindert, und es kommt zu einem Arbeitsspeicherverlust. Ein alternativer Ansatz für bereichsbezogene Dienste, die auf dem Typ OwningComponentBase basieren, wird weiter unten in diesem Abschnitt beschrieben, und verfügbare vorübergehende Dienste sollten überhaupt nicht verwendet werden. Weitere Informationen finden Sie unter Entwurf zur Lösung vorübergehender Einwegobjekte auf Blazor Server (dotnet/aspnetcore #26676).

Auch in clientseitigen Blazor-Apps, die nicht über einen Circuit ausgeführt werden, werden Dienste, die mit einer Scoped-Lebensdauer registriert sind, als Singletons behandelt, sodass sie länger leben als Scoped-Dienste in typischen ASP.NET Core-Apps. Clientseitige transiente Dienste, die entsorgt werden müssen, haben außerdem eine längere Lebensdauer als die Komponenten, in die sie injiziert werden, da der DI-Container, der Verweise auf solche Dienste enthält, während der gesamten Lebensdauer der App bestehen bleibt und dadurch die Speicherbereinigung dieser Dienste verhindert. Obwohl langlebige transiente Dienste, die entsorgt werden müssen, auf dem Server problematischer sind, sollten sie auch nicht als Clientdienste registriert werden. Die Verwendung des Typs OwningComponentBase wird auch für clientseitig begrenzte Dienste empfohlen, um ihre Lebensdauer zu steuern, und transiente Dienste, die entsorgt werden müssen, sollten überhaupt nicht verwendet werden.

Ein Ansatz, der die Lebensdauer eines Dienstes begrenzt, ist die Verwendung des OwningComponentBase-Typs. OwningComponentBase ist ein abstrakter Typ, der von ComponentBase abgeleitet ist und einen DI-Bereich erstellt, der der Lebensdauer der Komponente entspricht. Mit diesem Gültigkeitsbereich kann eine Komponente Dienste mit einer auf diesen Bereich beschränkten Lebensdauer injizieren, die genauso lange bestehen wie die Komponente. Wenn die Komponente zerstört wird, werden auch die Dienste des bereichsbezogenen Dienstanbieters der Komponente entsorgt. Dies kann für Dienste nützlich sein, die in einer Komponente wiederverwendet werden, aber nicht komponentenübergreifend genutzt werden.

Zwei Versionen des Typs OwningComponentBase sind verfügbar und werden in den nächsten beiden Abschnitten beschrieben:

OwningComponentBase

OwningComponentBase ist eine abstrakte, disponierbare, vom Typ ComponentBase abgeleitete Klasse mit einer geschützten ScopedServices-Eigenschaft vom Typ IServiceProvider. Der Anbieter kann zur Auflösung von Diensten verwendet werden, die der Lebensdauer der Komponente zugeordnet sind.

DI-Dienste, die mit @inject oder dem Attribut [Inject] in die Komponente injiziert werden, werden nicht im Geltungsbereich der Komponente erstellt. Um den Gültigkeitsbereich der Komponente zu verwenden, müssen Dienste mit ScopedServices entweder über GetRequiredService oder GetService aufgelöst werden. Alle Diensten, die unter Verwendung des ScopedServices-Anbieters aufgelöst werden, haben ihre Abhängigkeiten im Bereich der Komponente.

Das folgende Beispiel veranschaulicht den Unterschied zwischen der direkten Injektion eines bereichsbezogenen Diensts und der Dienstauflösung mit ScopedServices auf dem Server. Die folgende Schnittstelle und Implementierung für eine Zeitreiseklasse enthalten eine DT-Eigenschaft, die einen DateTime-Wert enthält. Die Implementierung ruft DateTime.Now auf, um DT festzulegen, wenn die TimeTravel-Klasse instanziiert wird.

ITimeTravel.cs:

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

TimeTravel.cs:

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

Der Dienst wird in der serverseitigen Program-Datei als bereichsbezogen registriert. Serverseitige Dienste mit Bereichsbindung haben eine Lebensdauer entsprechend der Dauer des Circuit.

In der Program-Datei:

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

In der folgenden TimeTravel-Komponente:

  • Der Zeitreisedienst wird direkt mit @inject als TimeTravel1 injiziert.
  • Der Dienst wird auch separat mit ScopedServices und GetRequiredService als TimeTravel2 aufgelöst.

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>();
    }
}

Bei der ersten Navigation zur TimeTravel-Komponente wird der Zeitreisedienst zweimal instanziiert, wenn die Komponente geladen wird und TimeTravel1 und TimeTravel2 den gleichen Anfangswert haben:

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

Beim Navigieren von der TimeTravel-Komponente zu einer anderen Komponente und zurück zur TimeTravel-Komponente:

  • wird TimeTravel1 die gleiche Dienstinstanz bereitgestellt, die beim ersten Laden der Komponente erstellt wurde, sodass der Wert von DT unverändert bleibt.
  • TimeTravel2 erhält in TimeTravel2 eine neue ITimeTravel-Dienstinstanz mit einem neuen DT-Wert.

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

TimeTravel1 ist an die Benutzerleitung gebunden, die intakt bleibt und erst entsorgt wird, wenn die zugrunde liegende Leitung dekonstruiert wird. Beispielsweise wird der Dienst gelöscht, wenn die Verbindung während des Aufbewahrungszeitraums für getrennte Verbindungen getrennt bleibt.

Trotz der bereichsbezogenen Dienstregistrierung in der Program-Datei und der Lebensdauer der Benutzerleitung erhält TimeTravel2 bei jeder Initialisierung der Komponente eine neue ITimeTravel-Dienstinstanz.

OwningComponentBase<TService>

OwningComponentBase<TService> leitet sich von OwningComponentBase ab und fügt die Eigenschaft Service hinzu, die eine Instanz von T aus dem bereichsbezogenen DI-Anbieter zurückgibt. Dieser Typ ist eine praktische Möglichkeit, auf bereichsgebundene Dienste zuzugreifen, ohne eine Instanz von IServiceProvider zu verwenden, wenn die Anwendung einen primären Dienst aus dem DI-Container benötigt, der den Bereich der Komponente verwendet. Die Eigenschaft ScopedServices ist verfügbar, sodass die App bei Bedarf auch Dienste anderer Typen abrufen kann.

@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>

Clientseitige temporäre Wegwerfobjekte erkennen

Benutzerdefinierter Code kann einer clientseitigen Blazor-App hinzugefügt werden, um transiente Einwegdienste in einer App zu erkennen, für die OwningComponentBase verwendet werden sollte. Dieser Ansatz ist nützlich, wenn Sie befürchten, dass Code, der der App künftig hinzugefügt wird, einen oder mehrere transiente disposable Dienste verwendet, einschließlich Diensten, die von Bibliotheken hinzugefügt werden. Democode ist im Blazor-Beispiele-Repository auf GitHub verfügbar (Downloadanleitung).

Überprüfen Sie Folgendes in .NET 6 oder höheren Versionen des BlazorSample_WebAssembly-Beispiels:

  • DetectIncorrectUsagesOfTransientDisposables.cs
  • Services/TransientDisposableService.cs
  • In Program.cs:
    • Der Services-Namespace der App wird oben in der Datei bereitgestellt (using BlazorSample.Services;).
    • DetectIncorrectUsageOfTransients wird unmittelbar aufgerufen, nachdem builder von WebAssemblyHostBuilder.CreateDefault zugewiesen wurde.
    • TransientDisposableService ist registriert (builder.Services.AddTransient<TransientDisposableService>();).
    • EnableTransientDisposableDetection wird auf dem erstellten Host in der Verarbeitungspipeline der Anwendung aufgerufen (host.EnableTransientDisposableDetection();).
  • Die App registriert den Dienst TransientDisposableService, ohne eine Ausnahme zu auslösen. Beim Versuch, den Dienst in TransientService.razor aufzulösen, wird jedoch InvalidOperationException ausgelöst, wenn das Framework versucht, eine Instanz von TransientDisposableService zu erstellen.

Erkennen serverseitiger kurzlebiger Disposable-Objekte

Benutzerdefinierter Code kann einer serverseitigen Blazor-App hinzugefügt werden, um serverseitige vorübergehende Dienste in einer App zu erkennen, die OwningComponentBase verwenden sollte. Dieser Ansatz ist nützlich, wenn Sie befürchten, dass Code, der der App künftig hinzugefügt wird, einen oder mehrere transiente disponierbare Dienste verwendet, auch solche, die von Bibliotheken hinzugefügt werden. Democode ist im Blazor-Beispiele-Repository auf GitHub verfügbar (Downloadanleitung).

Überprüfen Sie Folgendes in .NET 8 oder höheren Versionen des BlazorSample_BlazorWebApp-Beispiels:

Überprüfen Sie Folgendes in .NET 6 oder .NET 7 des BlazorSample_Server-Beispiels:

  • DetectIncorrectUsagesOfTransientDisposables.cs
  • Services/TransitiveTransientDisposableDependency.cs:
  • In Program.cs:
    • Der Services-Namespace der App wird oben in der Datei bereitgestellt (using BlazorSample.Services;).
    • DetectIncorrectUsageOfTransients wird im Host-Builder aufgerufen (builder.DetectIncorrectUsageOfTransients();).
    • Der TransientDependency-Dienst ist registriert (builder.Services.AddTransient<TransientDependency>();).
    • TransitiveTransientDisposableDependency ist für ITransitiveTransientDisposableDependency registriert (builder.Services.AddTransient<ITransitiveTransientDisposableDependency, TransitiveTransientDisposableDependency>();).
  • Die App registriert den Dienst TransientDependency, ohne eine Ausnahme zu auslösen. Beim Versuch, den Dienst in TransientService.razor aufzulösen, wird jedoch InvalidOperationException ausgelöst, wenn das Framework versucht, eine Instanz von TransientDependency zu erstellen.

Vorübergehende Dienstregistrierungen für IHttpClientFactory/HttpClient-Handler

Für IHttpClientFactory/HttpClient-Handler werden vorübergehende Dienstregistrierungen empfohlen. Wenn die App IHttpClientFactory/HttpClient-Handler enthält und zum Hinzufügen der Unterstützung für die Authentifizierung IRemoteAuthenticationBuilder<TRemoteAuthenticationState,TAccount> verwendet, werden auch die folgenden vorübergehenden verwerfbaren Elemente für die clientseitige Authentifizierung erkannt. Dies wird erwartet und kann ignoriert werden:

Andere Instanzen von IHttpClientFactory/HttpClient werden ebenfalls erkannt. Diese Instanzen können auch ignoriert werden.

Die Blazor-Beispiel-Apps im Blazor-Beispiele-Repository auf GitHub (Downloadanleitung) veranschaulichen den Code zum Erkennen von vorübergehend verwerfbaren Diensten. Der Code ist jedoch deaktiviert, da die Beispiel-Apps IHttpClientFactory/HttpClient-Handler enthalten.

So können Sie den Democode aktivieren und seine Ausführung sehen:

  • Entkommentieren Sie die Zeilen für transiente Disposable-Objekte in Program.cs.

  • Entfernen Sie die bedingte Überprüfung in NavMenu.razor, die verhindert, dass die TransientService-Komponente in der Navigationsseitenleiste der App angezeigt wird:

    - && (c.Name != "TransientService")
    
  • Führen Sie die Beispiel-App aus, und navigieren Sie zur TransientService-Komponente unter /transient-service.

Die Blazor-Beispiel-Apps im BlazorGitHub-Repository „samples“ (Herunterladen) zeigen, wie sich transiente Disposable-Objekte im Code erkennen lassen. Führen Sie die Beispiel-App aus, und navigieren Sie zur TransientService-Komponente unter /transient-service.

Verwenden eines Entity Framework Core (EF Core) DbContext von DI

Weitere Informationen finden Sie unter ASP.NET Core Blazor mit Entity Framework Core (EF Core).

Zugriff auf serverseitige Blazor-Dienste aus einem anderen DI-Bereich

Schaltkreisaktivitätshandler bieten einen Ansatz für den Zugriff auf bereichsbezogene Blazor Dienste aus anderen Nicht-Abhängigkeitsinjektionen (DI)-Blazor Bereichen.

Vor der Veröffentlichung von ASP.NET Core in .NET 8 erforderte der Zugriff auf Dienste mit auf den Circuit begrenztem Gültigkeitsbereich aus anderen Dependency-Injection-Bereichen einen benutzerdefinierten Basiskomponententyp. Mit Leitungsaktivitätshandlern ist kein benutzerdefinierter Basiskomponententyp erforderlich, wie im folgenden Beispiel veranschaulicht:

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;
    }
}

Rufen Sie AddCircuitServicesAccessor in der Datei Program der App auf:

builder.Services.AddCircuitServicesAccessor();

Greifen Sie auf die Dienste im Leitungsbereich zu, indem Sie CircuitServicesAccessor dort einfügen, wo es erforderlich ist.

Ein Beispiel dafür, wie Sie aus einem mit eingerichteten Blazor auf AuthenticationStateProvider zugreifen, finden Sie beim Ansatz mit dem Circuit Activity Handler in DelegatingHandler.

Es kann Situationen geben, in denen eine Razor-Komponente asynchrone Methoden aufruft, die Code in einem anderen DI-Scope ausführen. Ohne den richtigen Ansatz haben diese DI-Bereiche keinen Zugriff auf die Dienste von Blazor (z. B IJSRuntime und Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage).

Mit HttpClient erstellte IHttpClientFactory-Instanzen verfügen beispielsweise über einen eigenen DI-Dienstbereich. Daher können HttpMessageHandler-Instanzen, die für den HttpClient konfiguriert sind, Blazor-Dienste nicht direkt injizieren.

Erstellen Sie eine BlazorServiceAccessor-Klasse, die eine AsyncLocal-Instanz zum Speichern des BlazorIServiceProvider für den aktuellen asynchronen Kontext definiert. Eine BlazorServiceAccessor-Instanz kann innerhalb eines anderen DI-Dienstbereichs abgerufen werden, um auf Blazor-Dienste zuzugreifen.

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; }
    }
}

Um den Wert von BlazorServiceAccessor.Services automatisch festzulegen, wenn eine async-Komponentenmethode aufgerufen wird, erstellen Sie eine benutzerdefinierte Basiskomponente, die die drei primären asynchronen Einstiegspunkte erneut im Razor-Komponentencode implementiert:

Die folgende Klasse veranschaulicht die Implementierung für die Basiskomponente.

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;
        }
    }
}

Für alle Komponenten, die CustomComponentBase erweitern, ist BlazorServiceAccessor.Services automatisch auf den IServiceProvider im aktuellen Blazor-DI-Bereich festgelegt.

Fügen Sie abschließend in der Program-Datei den BlazorServiceAccessor als bereichsbezogenen Dienst hinzu:

builder.Services.AddScoped<BlazorServiceAccessor>();

Fügen Sie abschließend in Startup.ConfigureServices von Startup.csBlazorServiceAccessor als bereichsbezogenen Dienst hinzu:

services.AddScoped<BlazorServiceAccessor>();

Zusätzliche Ressourcen