Abhängigkeitsinjektion in ASP.NET Core Blazor

Von Rainer Stropek und Mike Rousos

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

Die Abhängigkeitsinjektion, die sogenannte Dependency Injection, ist ein Verfahren für den Zugriff auf Dienste, die an einer zentralen Stelle konfiguriert wurde:

  • Dienste, die beim Framework registriert wurden, können direkt in Komponenten von Blazor-Apps eingefügt werden.
  • Blazor-Apps definieren und registrieren benutzerdefinierte Dienste und machen sie über die Abhängigkeitsinjektion in der gesamten App verfügbar.

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.

Die Instanz von HttpClient in einer Blazor WebAssembly-App verwendet den Browser für die Behandlung des HTTP-Datenverkehrs im Hintergrund.

Blazor Server-Apps enthalten standardmäßig keinen HttpClient, der als Dienst konfiguriert ist. Stellen Sie einen HttpClient für eine Blazor Server-App bereit.

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

Blazor WebAssembly : Singleton

Blazor Server : Bereichsbezogen

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.
NavigationManager

Blazor WebAssembly : Singleton

Blazor Server : Bereichsbezogen

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

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 von Diensten zu einer Blazor WebAssembly-App

Konfigurieren Sie Dienste für die Dienstsammlung der App in Program.cs. 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, kann auf die Dienste vom Stammbereich der Abhängigkeitsinjektion aus zugegriffen werden, bevor Komponenten bereitgestellt 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 von Diensten zu einer Blazor Server-App

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

var builder = WebApplication.CreateBuilder(args);

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

Die builder-Variable stellt einen Microsoft.AspNetCore.Builder.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>();

Registrieren gemeinsamer Dienste in einer gehosteten Blazor WebAssembly-Lösung

Wenn mindestens ein gemeinsamer Dienst von den Projekten Server und Client einer gehosteten Blazor WebAssembly-Lösung benötigt wird, können Sie die gemeinsamen Dienstregistrierungen in einer Methode im Client-Projekt 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 beispielsweise eine ConfigureCommonServices-Methode im Client-Projekt:

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

Rufen Sie in der Datei Program.cs des Client-Projekts ConfigureCommonServices auf, um die gemeinsamen Dienste zu registrieren:

var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);

Rufen Sie in der Datei Program.cs des Server-Projekts ConfigureCommonServices auf, um die gemeinsamen Dienste für das Server-Projekt 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.

Lebensdauer von Diensten

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

Lebensdauer Beschreibung
Scoped

Blazor WebAssembly-Apps verfügen derzeit nicht über ein Konzept für Bereiche von Abhängigkeitsinjektionen. Scoped-registrierte Dienste verhalten sich wie Singleton-Dienste.

Das Blazor Server-Hostingmodell unterstützt die Scoped-Lebensdauer über HTTP-Anforderungen hinweg, nicht jedoch über SignalR-Verbindungs- bzw. Leitungsmeldungen zwischen Komponenten hinweg, die auf dem Client geladen sind. 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 eine Registerkarte 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 Blazor Server-Apps über bereichsbezogene Dienste hinweg finden Sie unter Blazor-Hostingmodell in ASP.NET Core.

Singleton Die Abhängigkeitsinjektion erstellt eine Einzelinstanz 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 Abhängigkeitsinjektionssystem basiert auf dem Abhängigkeitsinjektionssystem in ASP.NET Core. Weitere Informationen finden Sie unter Abhängigkeitsinjektion in ASP.NET Core.

Anfordern eines Diensts in einer Komponente

Nachdem die Dienste der Dienstsammlung hinzugefügt wurden, fügen Sie die 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.

Das folgende Beispiel veranschaulicht die Verwendung von @inject. Der Dienst, der Services.IDataAccess implementiert, wird in die Eigenschaft DataRepository der Komponente eingefügt. Beachten Sie, dass der Code nur die Abstraktion IDataAccess verwendet:

@page "/customer-list"
@inject IDataAccess DataRepository

@if (customers != null)
{
    <ul>
        @foreach (var customer in customers)
        {
            <li>@customer.FirstName @customer.LastName</li>
        }
    </ul>
}

@code {
    private IReadOnlyList<Customer>? customers;

    protected override async Task OnInitializedAsync()
    {
        customers = await DataRepository.GetAllCustomersAsync();
    }

    private class Customer
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
    }

    private interface IDataAccess
    {
        public Task<IReadOnlyList<Customer>> GetAllCustomersAsync();
    }
}

Intern verwendet die generierte Eigenschaft (DataRepository) das Attribut [Inject]. 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 IDataAccess DataRepository { get; set; }

    ...
}

Hinweis

Da von injizierten Diensten erwartet wird, dass sie verfügbar sind, dürfen Sie injizierte Dienste nicht als Nullwerte zulassend markieren. Weisen Sie stattdessen ein Standardliteral mit dem NULL-toleranten Operator (default!) zu. Beispiel:

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

Weitere Informationen finden Sie in den folgenden Ressourcen:

In Komponenten, die von der Basisklasse abgeleitet sind, ist die @inject-Direktive nicht erforderlich. Das InjectAttribute der Basisklasse ist ausreichend:

@page "/demo"
@inherits ComponentBase

<h1>Demo Component</h1>

Verwenden der Abhängigkeitsinjektion in Diensten

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 Constructor Injection verwendet werden. Erforderliche Dienste werden durch Hinzufügen von Parametern zum Konstruktor des Diensts hinzugefügt. Wenn die Abhängigkeitsinjektion den Dienst erstellt, erkennt sie die erforderlichen Dienste im Konstruktor und stellt sie entsprechend zur Verfügung. Im folgenden Beispiel empfängt der Konstruktor einen HttpClient über die Abhängigkeitsinjektion. HttpClient ist ein Standarddienst.

using System.Net.Http;

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

Voraussetzungen für die Constructor Injection:

  • Es muss einen Konstruktor geben, dessen Argumente alle von der Abhängigkeitsinjektion erfüllt werden können. Zusätzliche, nicht durch die Abhängigkeitsinjektion abgedeckte Parameter sind zulässig, wenn sie Standardwerte angeben.
  • Der anwendbare Konstruktor muss public sein.
  • Es muss ein anwendbarer Konstruktor vorhanden sein. Im Falle einer Mehrdeutigkeit löst die Abhängigkeitsinjektion eine Ausnahme aus.

Hilfsprogramm-Basiskomponentenklassen zur Verwaltung eines Bereichs für die Abhängigkeitsinjektion

In ASP.NET Core-Apps werden bereichsbezogene Dienste in der Regel der aktuellen Anforderung zugeordnet. Nachdem die Anforderung abgeschlossen ist, werden alle bereichsbezogenen oder vorübergehenden Dienste vom Abhängigkeitsinjektionssystem entsorgt. In Blazor Server-Apps besteht der Anforderungsbereich für die Dauer der Clientverbindung, was dazu führen kann, dass vorübergehende und bereichsbezogene Dienste viel länger als erwartet leben. In Blazor WebAssembly-Apps werden Dienste, die mit einer bereichsbezogenen Lebensdauer registriert sind, als Singletons behandelt, sodass sie länger leben als bereichsbezogene Dienste in typischen ASP.NET Core-Apps.

Hinweis

Informationen zum Erkennen vorübergehender verwerfbarer Dienste in einer App finden Sie in den folgenden Abschnitten:

Erkennen vorübergehend verwerfbarer Elemente in Blazor WebAssembly-AppsErkennen vorübergehend verwerfbarer Elemente in Blazor Server-Apps

Ein Ansatz, der die Lebensdauer eines Diensts in Blazor-Apps begrenzt, ist die Verwendung des Typs OwningComponentBase. OwningComponentBase ist ein abstrakter, von ComponentBase abgeleiteter Typ, der einen der Lebensdauer der Komponente entsprechenden Bereich für die Abhängigkeitsinjektion erstellt. Mit diesem Bereich ist es möglich, Abhängigkeitsinjektionsdienste mit einer bereichsbezogenen Lebensdauer zu nutzen und sie so lange wie die Komponente zu nutzen. Wenn die Komponente zerstört wird, werden auch die Dienste des bereichsbezogenen Dienstanbieters der Komponente entsorgt. Dies kann für Dienste nützlich sein, für die Folgendes gilt:

  • Sie sollten innerhalb einer Komponente wiederverwendet werden, da die vorübergehende Lebensdauer unangemessen ist.
  • Sie sollten nicht komponentenübergreifend freigegeben werden, da die Singleton-Lebensdauer unangemessen ist.

Zwei Versionen des Typs OwningComponentBase sind verfügbar:

  • OwningComponentBase ist ein abstraktes, verwerfbares, untergeordnetes Element vom Typ ComponentBase mit einer geschützten ScopedServices-Eigenschaft vom Typ IServiceProvider. Dieser 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 Bereich der Komponente zu verwenden, müssen die Dienste mit GetRequiredService oder GetService aufgelöst werden. Allen Diensten, die unter Verwendung des ScopedServices-Anbieters aufgelöst werden, werden ihre Abhängigkeiten aus demselben Bereich bereitgestellt.

    @page "/preferences"
    @using Microsoft.Extensions.DependencyInjection
    @inherits OwningComponentBase
    
    <h1>User @(UserService is not null ? $"({UserService.Name})" : string.Empty)</h1>
    
    <ul>
        @if (SettingService is not null)
        {
            foreach (var setting in SettingService.GetSettings())
            {
                <li>@setting.SettingName: @setting.SettingValue</li>
            }
        }
    </ul>
    
    @code {
        private IUserService? UserService { get; set; }
        private ISettingService? SettingService { get; set; }
    
        protected override void OnInitialized()
        {
            UserService = ScopedServices.GetRequiredService<IUserService>();
            SettingService = ScopedServices.GetRequiredService<ISettingService>();
        }
    
        public class Setting
        {
            public string? SettingName { get; set; }
            public string? SettingValue { get; set; }
        }
    
        public interface IUserService
        {
            public string? Name { get; set; }
        }
    
        public interface ISettingService
        {
            public IList<Setting> GetSettings();
        }
    }
    
  • OwningComponentBase<TService> ist abgeleitet von OwningComponentBase und fügt eine Eigenschaft Service hinzu, die eine Instanz von T vom bereichsbezogenen Abhängigkeitsinjektionsanbieter zurückgibt. Dieser Typ ist eine komfortable Methode für den Zugriff auf bereichsbezogene Dienste, ohne eine Instanz von IServiceProvider zu verwenden, wenn es einen primären Dienst gibt, den die Anwendung aus dem Abhängigkeitsinjektionscontainer unter Verwendung des Bereichs der Komponente benötigt. Die Eigenschaft ScopedServices ist verfügbar, sodass die App bei Bedarf auch Dienste mit anderen Typen erhalten 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>
    

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

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

Erkennen vorübergehender verwerfbarer Elemente in Blazor WebAssembly-Apps

Im folgenden Beispiel wird veranschaulicht, wie verwerfbare temporäre Dienste in einer App erkannt werden, die OwningComponentBase verwenden sollten. Weitere Informationen finden Sie im Abschnitt Hilfsprogramm-Basiskomponentenklassen zur Verwaltung eines Bereichs für die Abhängigkeitsinjektion.

DetectIncorrectUsagesOfTransientDisposables.cs für Blazor WebAssembly-Apps:

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection
{
    using BlazorWebAssemblyTransientDisposable;
    using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

    public static class WebHostBuilderTransientDisposableExtensions
    {
        public static WebAssemblyHostBuilder DetectIncorrectUsageOfTransients(
            this WebAssemblyHostBuilder builder)
        {
            builder
                .ConfigureContainer(
                    new DetectIncorrectUsageOfTransientDisposablesServiceFactory());

            return builder;
        }

        public static WebAssemblyHost EnableTransientDisposableDetection(
            this WebAssemblyHost webAssemblyHost)
        {
            webAssemblyHost.Services
                .GetRequiredService<ThrowOnTransientDisposable>().ShouldThrow = true;

            return webAssemblyHost;
        }
    }
}

namespace BlazorWebAssemblyTransientDisposable
{
    public class DetectIncorrectUsageOfTransientDisposablesServiceFactory 
        : IServiceProviderFactory<IServiceCollection>
    {
        public IServiceCollection CreateBuilder(IServiceCollection services) => 
            services;

        public IServiceProvider CreateServiceProvider(
            IServiceCollection containerBuilder)
        {
            var collection = new ServiceCollection();

            foreach (var descriptor in containerBuilder)
            {
                if (descriptor.Lifetime == ServiceLifetime.Transient &&
                    descriptor.ImplementationType != null && 
                    typeof(IDisposable).IsAssignableFrom(
                        descriptor.ImplementationType))
                {
                    collection.Add(CreatePatchedDescriptor(descriptor));
                }
                else if (descriptor.Lifetime == ServiceLifetime.Transient &&
                         descriptor.ImplementationFactory != null)
                {
                    collection.Add(CreatePatchedFactoryDescriptor(descriptor));
                }
                else
                {
                    collection.Add(descriptor);
                }
            }

            collection.AddScoped<ThrowOnTransientDisposable>();

            return collection.BuildServiceProvider();
        }

        private ServiceDescriptor CreatePatchedFactoryDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) =>
                {
                    var originalFactory = original.ImplementationFactory;
                    
                    if (originalFactory is null)
                    {
                        throw new InvalidOperationException(
                            "originalFactory is null.");
                    }

                    var originalResult = originalFactory(sp);

                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow && 
                        originalResult is IDisposable d)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            $"transient disposable service {d.GetType().Name} in " +
                            "the wrong scope. Use an 'OwningComponentBase<T>' " +
                            "component base class for the service 'T' you are " +
                            "trying to resolve.");
                    }

                    return originalResult;
                },
                original.Lifetime);

            return newDescriptor;
        }

        private ServiceDescriptor CreatePatchedDescriptor(ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) => {
                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                        "transient disposable service " +
                        $"{original.ImplementationType?.Name} in the wrong " +
                        "scope. Use an 'OwningComponentBase<T>' component base " +
                        "class for the service 'T' you are trying to resolve.");
                    }

                    if (original.ImplementationType is null)
                    {
                        throw new InvalidOperationException(
                            "ImplementationType is null.");
                    }

                    return ActivatorUtilities.CreateInstance(sp, 
                        original.ImplementationType);
                },
                ServiceLifetime.Transient);
    
            return newDescriptor;
        }
    }

    internal class ThrowOnTransientDisposable
    {
        public bool ShouldThrow { get; set; }
    }
}

TransientDisposable.cs:

public class TransientDisposable : IDisposable
{
    public void Dispose() => throw new NotImplementedException();
}

Im folgenden Beispiel wird TransientDisposable erkannt.

Program.cs:

using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using BlazorWebAssemblyTransientDisposable;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.DetectIncorrectUsageOfTransients();
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped(sp => 
    new HttpClient
    { 
        BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
    });

var host = builder.Build();
host.EnableTransientDisposableDetection();
await host.RunAsync();

Die App kann vorübergehende verwerfbare Elemente registrieren, ohne eine Ausnahme auszulösen. Der Versuch, ein vorübergehendes verwerfbares Ergebnis aufzulösen, führt jedoch zu einer InvalidOperationException, wie im folgenden Beispiel gezeigt.

Pages/TransientExample.razor:

@page "/transient-example"
@inject TransientDisposable TransientDisposable

<h1>Transient Disposable Detection</h1>

Navigieren Sie zur TransientExample-Komponente unter /transient-example, und eine InvalidOperationException wird ausgelöst, wenn das Framework versucht, eine Instanz von TransientDisposable zu erstellen:

System.InvalidOperationException: Es wurde versucht, den vorübergehenden verwerfbaren Dienst TransientDisposable im falschen Bereich aufzulösen. Verwenden Sie für den Dienst „T“, den Sie auflösen möchten, die Komponentenbasisklasse „OwningComponentBase<T>“.

Erkennen vorübergehender verwerfbarer Elemente in Blazor Server-Apps

Im folgenden Beispiel wird veranschaulicht, wie verwerfbare temporäre Dienste in einer App erkannt werden, die OwningComponentBase verwenden sollten. Weitere Informationen finden Sie im Abschnitt Hilfsprogramm-Basiskomponentenklassen zur Verwaltung eines Bereichs für die Abhängigkeitsinjektion.

DetectIncorrectUsagesOfTransientDisposables.cs:

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection
{
    using BlazorServerTransientDisposable;

    public static class WebHostBuilderTransientDisposableExtensions
    {
        public static WebApplicationBuilder DetectIncorrectUsageOfTransients(
            this WebApplicationBuilder builder)
        {
            builder.Host
                .UseServiceProviderFactory(
                    new DetectIncorrectUsageOfTransientDisposablesServiceFactory())
                .ConfigureServices(
                    s => s.TryAddEnumerable(ServiceDescriptor.Scoped<CircuitHandler,
                        ThrowOnTransientDisposableHandler>()));

            return builder;
        }
    }
}

namespace BlazorServerTransientDisposable
{
    internal class ThrowOnTransientDisposableHandler : CircuitHandler
    {
        public ThrowOnTransientDisposableHandler(
            ThrowOnTransientDisposable throwOnTransientDisposable)
        {
            throwOnTransientDisposable.ShouldThrow = true;
        }
    }

    public class DetectIncorrectUsageOfTransientDisposablesServiceFactory 
        : IServiceProviderFactory<IServiceCollection>
    {
        public IServiceCollection CreateBuilder(IServiceCollection services) => 
            services;

        public IServiceProvider CreateServiceProvider(
            IServiceCollection containerBuilder)
        {
            var collection = new ServiceCollection();

            foreach (var descriptor in containerBuilder)
            {
                if (descriptor.Lifetime == ServiceLifetime.Transient &&
                    descriptor.ImplementationType != null && 
                    typeof(IDisposable).IsAssignableFrom(
                        descriptor.ImplementationType))
                {
                    collection.Add(CreatePatchedDescriptor(descriptor));
                }
                else if (descriptor.Lifetime == ServiceLifetime.Transient &&
                         descriptor.ImplementationFactory != null)
                {
                    collection.Add(CreatePatchedFactoryDescriptor(descriptor));
                }
                else
                {
                    collection.Add(descriptor);
                }
            }

            collection.AddScoped<ThrowOnTransientDisposable>();

            return collection.BuildServiceProvider();
        }

        private ServiceDescriptor CreatePatchedFactoryDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) =>
                {
                    var originalFactory = original.ImplementationFactory;

                    if (originalFactory is null)
                    {
                        throw new InvalidOperationException(
                            "originalFactory is null.");
                    }

                    var originalResult = originalFactory(sp);

                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow && 
                        originalResult is IDisposable d)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            $"transient disposable service {d.GetType().Name} in " +
                            "the wrong scope. Use an 'OwningComponentBase<T>' " +
                            "component base class for the service 'T' you are " +
                            "trying to resolve.");
                    }

                    return originalResult;
                },
                original.Lifetime);

            return newDescriptor;
        }

        private ServiceDescriptor CreatePatchedDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) => {
                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            "transient disposable service " +
                            $"{original.ImplementationType?.Name} in the wrong " +
                            "scope. Use an 'OwningComponentBase<T>' component " +
                            "base class for the service 'T' you are trying to " +
                            "resolve.");
                    }

                    if (original.ImplementationType is null)
                    {
                        throw new InvalidOperationException(
                            "ImplementationType is null.");
                    }

                    return ActivatorUtilities.CreateInstance(sp, 
                        original.ImplementationType);
                },
                ServiceLifetime.Transient);
    
            return newDescriptor;
        }
    }

    internal class ThrowOnTransientDisposable
    {
        public bool ShouldThrow { get; set; }
    }
}

TransitiveTransientDisposableDependency.cs:

public class TransitiveTransientDisposableDependency 
    : ITransitiveTransientDisposableDependency, IDisposable
{
    public void Dispose() { }
}

public interface ITransitiveTransientDisposableDependency
{
}

public class TransientDependency
{
    private readonly ITransitiveTransientDisposableDependency 
        transitiveTransientDisposableDependency;

    public TransientDependency(ITransitiveTransientDisposableDependency 
        transitiveTransientDisposableDependency)
    {
        this.transitiveTransientDisposableDependency = 
            transitiveTransientDisposableDependency;
    }
}

Im folgenden Beispiel wird TransientDependency erkannt.

In Program.cs:

builder.DetectIncorrectUsageOfTransients();
builder.Services.AddTransient<TransientDependency>();
builder.Services.AddTransient<ITransitiveTransientDisposableDependency, 
    TransitiveTransientDisposableDependency>();

Die App kann vorübergehende verwerfbare Elemente registrieren, ohne eine Ausnahme auszulösen. Der Versuch, ein vorübergehendes verwerfbares Ergebnis aufzulösen, führt jedoch zu einer InvalidOperationException, wie im folgenden Beispiel gezeigt.

Pages/TransientExample.razor:

@page "/transient-example"
@inject TransientDependency TransientDependency

<h1>Transient Disposable Detection</h1>

Navigieren Sie zur TransientExample-Komponente unter /transient-example, und eine InvalidOperationException wird ausgelöst, wenn das Framework versucht, eine Instanz von TransientDependency zu erstellen:

System.InvalidOperationException: Es wurde versucht, den temporären verwerfbaren Dienst „TransientDependency“ im falschen Bereich aufzulösen. Verwenden Sie für den Dienst „T“, den Sie auflösen möchten, die Komponentenbasisklasse „OwningComponentBase<T>“.

Zusätzliche Ressourcen

Die Abhängigkeitsinjektion, die sogenannte Dependency Injection, ist ein Verfahren für den Zugriff auf Dienste, die an einer zentralen Stelle konfiguriert wurde:

  • Dienste, die beim Framework registriert wurden, können direkt in Komponenten von Blazor-Apps eingefügt werden.
  • Blazor-Apps definieren und registrieren benutzerdefinierte Dienste und machen sie über die Abhängigkeitsinjektion in der gesamten App verfügbar.

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.

Die Instanz von HttpClient in einer Blazor WebAssembly-App verwendet den Browser für die Behandlung des HTTP-Datenverkehrs im Hintergrund.

Blazor Server-Apps enthalten standardmäßig keinen HttpClient, der als Dienst konfiguriert ist. Stellen Sie einen HttpClient für eine Blazor Server-App bereit.

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

Blazor WebAssembly : Singleton

Blazor Server : Bereichsbezogen

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.
NavigationManager

Blazor WebAssembly : Singleton

Blazor Server : Bereichsbezogen

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

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 von Diensten zu einer Blazor WebAssembly-App

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

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        ...
        builder.Services.AddSingleton<IExampleDependency, ExampleDependency>();
        ...

        await builder.Build().RunAsync();
    }
}

Nachdem der Host erstellt wurde, kann auf die Dienste vom Stammbereich der Abhängigkeitsinjektion aus zugegriffen werden, bevor Komponenten bereitgestellt werden. Dies kann für die Ausführung der Initialisierungslogik vor dem Rendern von Inhalten nützlich sein:

public class Program
{
    public static async Task Main(string[] args)
    {
        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:

public class Program
{
    public static async Task Main(string[] args)
    {
        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 von Diensten zu einer Blazor Server-App

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 gemeinsamer Dienste in einer gehosteten Blazor WebAssembly-Lösung

Wenn mindestens ein gemeinsamer Dienst von den Projekten Server und Client einer gehosteten Blazor WebAssembly-Lösung benötigt wird, können Sie die gemeinsamen Dienstregistrierungen in einer Methode im Client-Projekt 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 beispielsweise eine ConfigureCommonServices-Methode im Client-Projekt:

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

Rufen Sie in der Datei Program.cs des Clientprojekts (Client) ConfigureCommonServices auf, um die gemeinsamen Dienste zu registrieren:

var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);

Rufen Sie in der ConfigureServices-Methode von Startup.cs des Server-Projekts ConfigureCommonServices auf, um die gemeinsamen Dienste für das Server-Projekt zu registrieren:

Client.Program.ConfigureCommonServices(services);

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

Lebensdauer von Diensten

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

Lebensdauer Beschreibung
Scoped

Blazor WebAssembly-Apps verfügen derzeit nicht über ein Konzept für Bereiche von Abhängigkeitsinjektionen. Scoped-registrierte Dienste verhalten sich wie Singleton-Dienste.

Das Blazor Server-Hostingmodell unterstützt die Scoped-Lebensdauer über HTTP-Anforderungen hinweg, nicht jedoch über SignalR-Verbindungs- bzw. Leitungsmeldungen zwischen Komponenten hinweg, die auf dem Client geladen sind. 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 eine Registerkarte 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 Blazor Server-Apps über bereichsbezogene Dienste hinweg finden Sie unter Blazor-Hostingmodell in ASP.NET Core.

Singleton Die Abhängigkeitsinjektion erstellt eine Einzelinstanz 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 Abhängigkeitsinjektionssystem basiert auf dem Abhängigkeitsinjektionssystem in ASP.NET Core. Weitere Informationen finden Sie unter Abhängigkeitsinjektion in ASP.NET Core.

Anfordern eines Diensts in einer Komponente

Nachdem die Dienste der Dienstsammlung hinzugefügt wurden, fügen Sie die 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.

Das folgende Beispiel veranschaulicht die Verwendung von @inject. Der Dienst, der Services.IDataAccess implementiert, wird in die Eigenschaft DataRepository der Komponente eingefügt. Beachten Sie, dass der Code nur die Abstraktion IDataAccess verwendet:

@page "/customer-list"
@inject IDataAccess DataRepository

@if (customers != null)
{
    <ul>
        @foreach (var customer in customers)
        {
            <li>@customer.FirstName @customer.LastName</li>
        }
    </ul>
}

@code {
    private IReadOnlyList<Customer> customers;

    protected override async Task OnInitializedAsync()
    {
        customers = await DataRepository.GetAllCustomersAsync();
    }

    private class Customer
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    private interface IDataAccess
    {
        public Task<IReadOnlyList<Customer>> GetAllCustomersAsync();
    }
}

Intern verwendet die generierte Eigenschaft (DataRepository) das Attribut [Inject]. 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 IDataAccess DataRepository { get; set; }

    ...
}

In Komponenten, die von der Basisklasse abgeleitet sind, ist die @inject-Direktive nicht erforderlich. Das InjectAttribute der Basisklasse ist ausreichend:

@page "/demo"
@inherits ComponentBase

<h1>Demo Component</h1>

Verwenden der Abhängigkeitsinjektion in Diensten

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 Constructor Injection verwendet werden. Erforderliche Dienste werden durch Hinzufügen von Parametern zum Konstruktor des Diensts hinzugefügt. Wenn die Abhängigkeitsinjektion den Dienst erstellt, erkennt sie die erforderlichen Dienste im Konstruktor und stellt sie entsprechend zur Verfügung. Im folgenden Beispiel empfängt der Konstruktor einen HttpClient über die Abhängigkeitsinjektion. HttpClient ist ein Standarddienst.

using System.Net.Http;

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

Voraussetzungen für die Constructor Injection:

  • Es muss einen Konstruktor geben, dessen Argumente alle von der Abhängigkeitsinjektion erfüllt werden können. Zusätzliche, nicht durch die Abhängigkeitsinjektion abgedeckte Parameter sind zulässig, wenn sie Standardwerte angeben.
  • Der anwendbare Konstruktor muss public sein.
  • Es muss ein anwendbarer Konstruktor vorhanden sein. Im Falle einer Mehrdeutigkeit löst die Abhängigkeitsinjektion eine Ausnahme aus.

Hilfsprogramm-Basiskomponentenklassen zur Verwaltung eines Bereichs für die Abhängigkeitsinjektion

In ASP.NET Core-Apps werden bereichsbezogene Dienste in der Regel der aktuellen Anforderung zugeordnet. Nachdem die Anforderung abgeschlossen ist, werden alle bereichsbezogenen oder vorübergehenden Dienste vom Abhängigkeitsinjektionssystem entsorgt. In Blazor Server-Apps besteht der Anforderungsbereich für die Dauer der Clientverbindung, was dazu führen kann, dass vorübergehende und bereichsbezogene Dienste viel länger als erwartet leben. In Blazor WebAssembly-Apps werden Dienste, die mit einer bereichsbezogenen Lebensdauer registriert sind, als Singletons behandelt, sodass sie länger leben als bereichsbezogene Dienste in typischen ASP.NET Core-Apps.

Hinweis

Informationen zum Erkennen vorübergehender verwerfbarer Dienste in einer App finden Sie in den folgenden Abschnitten:

Erkennen vorübergehend verwerfbarer Elemente in Blazor WebAssembly-AppsErkennen vorübergehend verwerfbarer Elemente in Blazor Server-Apps

Ein Ansatz, der die Lebensdauer eines Diensts in Blazor-Apps begrenzt, ist die Verwendung des Typs OwningComponentBase. OwningComponentBase ist ein abstrakter, von ComponentBase abgeleiteter Typ, der einen der Lebensdauer der Komponente entsprechenden Bereich für die Abhängigkeitsinjektion erstellt. Mit diesem Bereich ist es möglich, Abhängigkeitsinjektionsdienste mit einer bereichsbezogenen Lebensdauer zu nutzen und sie so lange wie die Komponente zu nutzen. Wenn die Komponente zerstört wird, werden auch die Dienste des bereichsbezogenen Dienstanbieters der Komponente entsorgt. Dies kann für Dienste nützlich sein, für die Folgendes gilt:

  • Sie sollten innerhalb einer Komponente wiederverwendet werden, da die vorübergehende Lebensdauer unangemessen ist.
  • Sie sollten nicht komponentenübergreifend freigegeben werden, da die Singleton-Lebensdauer unangemessen ist.

Zwei Versionen des Typs OwningComponentBase sind verfügbar:

  • OwningComponentBase ist ein abstraktes, verwerfbares, untergeordnetes Element vom Typ ComponentBase mit einer geschützten ScopedServices-Eigenschaft vom Typ IServiceProvider. Dieser 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 Bereich der Komponente zu verwenden, müssen die Dienste mit GetRequiredService oder GetService aufgelöst werden. Allen Diensten, die unter Verwendung des ScopedServices-Anbieters aufgelöst werden, werden ihre Abhängigkeiten aus demselben Bereich bereitgestellt.

    @page "/preferences"
    @using Microsoft.Extensions.DependencyInjection
    @inherits OwningComponentBase
    
    <h1>User (@UserService.Name)</h1>
    
    <ul>
        @foreach (var setting in SettingService.GetSettings())
        {
            <li>@setting.SettingName: @setting.SettingValue</li>
        }
    </ul>
    
    @code {
        private IUserService UserService { get; set; }
        private ISettingService SettingService { get; set; }
    
        protected override void OnInitialized()
        {
            UserService = ScopedServices.GetRequiredService<IUserService>();
            SettingService = ScopedServices.GetRequiredService<ISettingService>();
        }
    
        public class Setting
        {
            public string SettingName { get; set; }
            public string SettingValue { get; set; }
        }
    
        public interface IUserService
        {
            public string Name { get; set; }
        }
    
        public interface ISettingService
        {
            public IList<Setting> GetSettings();
        }
    }
    
  • OwningComponentBase<TService> ist abgeleitet von OwningComponentBase und fügt eine Eigenschaft Service hinzu, die eine Instanz von T vom bereichsbezogenen Abhängigkeitsinjektionsanbieter zurückgibt. Dieser Typ ist eine komfortable Methode für den Zugriff auf bereichsbezogene Dienste, ohne eine Instanz von IServiceProvider zu verwenden, wenn es einen primären Dienst gibt, den die Anwendung aus dem Abhängigkeitsinjektionscontainer unter Verwendung des Bereichs der Komponente benötigt. Die Eigenschaft ScopedServices ist verfügbar, sodass die App bei Bedarf auch Dienste mit anderen Typen erhalten 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>
    

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

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

Erkennen vorübergehender verwerfbarer Elemente in Blazor WebAssembly-Apps

Im folgenden Beispiel wird veranschaulicht, wie verwerfbare temporäre Dienste in einer App erkannt werden, die OwningComponentBase verwenden sollten. Weitere Informationen finden Sie im Abschnitt Hilfsprogramm-Basiskomponentenklassen zur Verwaltung eines Bereichs für die Abhängigkeitsinjektion.

DetectIncorrectUsagesOfTransientDisposables.cs:

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection
{
    using BlazorWebAssemblyTransientDisposable;
    using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

    public static class WebHostBuilderTransientDisposableExtensions
    {
        public static WebAssemblyHostBuilder DetectIncorrectUsageOfTransients(
            this WebAssemblyHostBuilder builder)
        {
            builder
                .ConfigureContainer(
                    new DetectIncorrectUsageOfTransientDisposablesServiceFactory());

            return builder;
        }

        public static WebAssemblyHost EnableTransientDisposableDetection(
            this WebAssemblyHost webAssemblyHost)
        {
            webAssemblyHost.Services
                .GetRequiredService<ThrowOnTransientDisposable>().ShouldThrow = true;

            return webAssemblyHost;
        }
    }
}

namespace BlazorWebAssemblyTransientDisposable
{
    public class DetectIncorrectUsageOfTransientDisposablesServiceFactory 
        : IServiceProviderFactory<IServiceCollection>
    {
        public IServiceCollection CreateBuilder(IServiceCollection services) => 
            services;

        public IServiceProvider CreateServiceProvider(
            IServiceCollection containerBuilder)
        {
            var collection = new ServiceCollection();

            foreach (var descriptor in containerBuilder)
            {
                if (descriptor.Lifetime == ServiceLifetime.Transient &&
                    descriptor.ImplementationType != null && 
                    typeof(IDisposable).IsAssignableFrom(
                        descriptor.ImplementationType))
                {
                    collection.Add(CreatePatchedDescriptor(descriptor));
                }
                else if (descriptor.Lifetime == ServiceLifetime.Transient &&
                         descriptor.ImplementationFactory != null)
                {
                    collection.Add(CreatePatchedFactoryDescriptor(descriptor));
                }
                else
                {
                    collection.Add(descriptor);
                }
            }

            collection.AddScoped<ThrowOnTransientDisposable>();

            return collection.BuildServiceProvider();
        }

        private ServiceDescriptor CreatePatchedFactoryDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) =>
                {
                    var originalFactory = original.ImplementationFactory;
                    var originalResult = originalFactory(sp);

                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow && 
                        originalResult is IDisposable d)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            $"transient disposable service {d.GetType().Name} in " +
                            "the wrong scope. Use an 'OwningComponentBase<T>' " +
                            "component base class for the service 'T' you are " +
                            "trying to resolve.");
                    }

                    return originalResult;
                },
                original.Lifetime);

            return newDescriptor;
        }

        private ServiceDescriptor CreatePatchedDescriptor(ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) => {
                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                        "transient disposable service " +
                        $"{original.ImplementationType.Name} in the wrong " +
                        "scope. Use an 'OwningComponentBase<T>' component base " +
                        "class for the service 'T' you are trying to resolve.");
                    }

                    return ActivatorUtilities.CreateInstance(sp, 
                        original.ImplementationType);
                },
                ServiceLifetime.Transient);
    
            return newDescriptor;
        }
    }

    internal class ThrowOnTransientDisposable
    {
        public bool ShouldThrow { get; set; }
    }
}

Im folgenden Beispiel wird TransientDisposable erkannt (Program.cs):

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.DetectIncorrectUsageOfTransients();
        builder.RootComponents.Add<App>("#app");

        builder.Services.AddTransient<TransientDisposable>();
        builder.Services.AddScoped(sp =>
            new HttpClient
            {
                BaseAddress = new(builder.HostEnvironment.BaseAddress)
            });

        var host = builder.Build();
        host.EnableTransientDisposableDetection();
        await host.RunAsync();
    }
}

public class TransientDisposable : IDisposable
{
    public void Dispose() => throw new NotImplementedException();
}

Die App kann vorübergehende verwerfbare Elemente registrieren, ohne eine Ausnahme auszulösen. Der Versuch, ein vorübergehendes verwerfbares Ergebnis aufzulösen, führt jedoch zu einer InvalidOperationException, wie im folgenden Beispiel gezeigt.

Pages/TransientExample.razor:

@page "/transient-example"
@inject TransientDisposable TransientDisposable

<h1>Transient Disposable Detection</h1>

Navigieren Sie zur TransientExample-Komponente unter /transient-example, und eine InvalidOperationException wird ausgelöst, wenn das Framework versucht, eine Instanz von TransientDisposable zu erstellen:

System.InvalidOperationException: Es wurde versucht, den vorübergehenden verwerfbaren Dienst TransientDisposable im falschen Bereich aufzulösen. Verwenden Sie für den Dienst „T“, den Sie auflösen möchten, die Komponentenbasisklasse „OwningComponentBase<T>“.

Erkennen vorübergehender verwerfbarer Elemente in Blazor Server-Apps

Im folgenden Beispiel wird veranschaulicht, wie verwerfbare temporäre Dienste in einer App erkannt werden, die OwningComponentBase verwenden sollten. Weitere Informationen finden Sie im Abschnitt Hilfsprogramm-Basiskomponentenklassen zur Verwaltung eines Bereichs für die Abhängigkeitsinjektion.

DetectIncorrectUsagesOfTransientDisposables.cs:

using System;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Microsoft.Extensions.DependencyInjection
{
    using BlazorServerTransientDisposable;

    public static class WebHostBuilderTransientDisposableExtensions
    {
        public static IHostBuilder DetectIncorrectUsageOfTransients(
            this IHostBuilder builder)
        {
            builder
                .UseServiceProviderFactory(
                    new DetectIncorrectUsageOfTransientDisposablesServiceFactory())
                .ConfigureServices(
                    s => s.TryAddEnumerable(ServiceDescriptor.Scoped<CircuitHandler,
                        ThrowOnTransientDisposableHandler>()));

            return builder;
        }
    }
}

namespace BlazorServerTransientDisposable
{
    internal class ThrowOnTransientDisposableHandler : CircuitHandler
    {
        public ThrowOnTransientDisposableHandler(
            ThrowOnTransientDisposable throwOnTransientDisposable)
        {
            throwOnTransientDisposable.ShouldThrow = true;
        }
    }

    public class DetectIncorrectUsageOfTransientDisposablesServiceFactory 
        : IServiceProviderFactory<IServiceCollection>
    {
        public IServiceCollection CreateBuilder(IServiceCollection services) => 
            services;

        public IServiceProvider CreateServiceProvider(
            IServiceCollection containerBuilder)
        {
            var collection = new ServiceCollection();

            foreach (var descriptor in containerBuilder)
            {
                if (descriptor.Lifetime == ServiceLifetime.Transient &&
                    descriptor.ImplementationType != null && 
                    typeof(IDisposable).IsAssignableFrom(
                        descriptor.ImplementationType))
                {
                    collection.Add(CreatePatchedDescriptor(descriptor));
                }
                else if (descriptor.Lifetime == ServiceLifetime.Transient &&
                         descriptor.ImplementationFactory != null)
                {
                    collection.Add(CreatePatchedFactoryDescriptor(descriptor));
                }
                else
                {
                    collection.Add(descriptor);
                }
            }

            collection.AddScoped<ThrowOnTransientDisposable>();

            return collection.BuildServiceProvider();
        }

        private ServiceDescriptor CreatePatchedFactoryDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) =>
                {
                    var originalFactory = original.ImplementationFactory;
                    var originalResult = originalFactory(sp);

                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow && 
                        originalResult is IDisposable d)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            $"transient disposable service {d.GetType().Name} in " +
                            "the wrong scope. Use an 'OwningComponentBase<T>' " +
                            "component base class for the service 'T' you are " +
                            "trying to resolve.");
                    }

                    return originalResult;
                },
                original.Lifetime);

            return newDescriptor;
        }

        private ServiceDescriptor CreatePatchedDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) => {
                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            "transient disposable service " +
                            $"{original.ImplementationType.Name} in the wrong " +
                            "scope. Use an 'OwningComponentBase<T>' component " +
                            "base class for the service 'T' you are trying to " +
                            "resolve.");
                    }

                    return ActivatorUtilities.CreateInstance(sp, 
                        original.ImplementationType);
                },
                ServiceLifetime.Transient);
    
            return newDescriptor;
        }
    }

    internal class ThrowOnTransientDisposable
    {
        public bool ShouldThrow { get; set; }
    }
}

Fügen Sie Program.cs den Namespace für Microsoft.Extensions.DependencyInjection hinzu:

using Microsoft.Extensions.DependencyInjection;

In Program.CreateHostBuilder von Program.cs:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .DetectIncorrectUsageOfTransients()
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

Im folgenden Beispiel wird TransientDependency erkannt (Startup.cs):

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddSingleton<WeatherForecastService>();
    services.AddTransient<TransientDependency>();
    services.AddTransient<ITransitiveTransientDisposableDependency, 
        TransitiveTransientDisposableDependency>();
}

public class TransitiveTransientDisposableDependency 
    : ITransitiveTransientDisposableDependency, IDisposable
{
    public void Dispose() { }
}

public interface ITransitiveTransientDisposableDependency
{
}

public class TransientDependency
{
    private readonly ITransitiveTransientDisposableDependency 
        _transitiveTransientDisposableDependency;

    public TransientDependency(ITransitiveTransientDisposableDependency 
        transitiveTransientDisposableDependency)
    {
        _transitiveTransientDisposableDependency = 
            transitiveTransientDisposableDependency;
    }
}

Die App kann vorübergehende verwerfbare Elemente registrieren, ohne eine Ausnahme auszulösen. Der Versuch, ein vorübergehendes verwerfbares Ergebnis aufzulösen, führt jedoch zu einer InvalidOperationException, wie im folgenden Beispiel gezeigt.

Pages/TransientExample.razor:

@page "/transient-example"
@inject TransientDependency TransientDependency

<h1>Transient Disposable Detection</h1>

Navigieren Sie zur TransientExample-Komponente unter /transient-example, und eine InvalidOperationException wird ausgelöst, wenn das Framework versucht, eine Instanz von TransientDependency zu erstellen:

System.InvalidOperationException: Es wurde versucht, den temporären verwerfbaren Dienst „TransientDependency“ im falschen Bereich aufzulösen. Verwenden Sie für den Dienst „T“, den Sie auflösen möchten, die Komponentenbasisklasse „OwningComponentBase<T>“.

Zusätzliche Ressourcen

Die Abhängigkeitsinjektion, die sogenannte Dependency Injection, ist ein Verfahren für den Zugriff auf Dienste, die an einer zentralen Stelle konfiguriert wurde:

  • Dienste, die beim Framework registriert wurden, können direkt in Komponenten von Blazor-Apps eingefügt werden.
  • Blazor-Apps definieren und registrieren benutzerdefinierte Dienste und machen sie über die Abhängigkeitsinjektion in der gesamten App verfügbar.

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.

Die Instanz von HttpClient in einer Blazor WebAssembly-App verwendet den Browser für die Behandlung des HTTP-Datenverkehrs im Hintergrund.

Blazor Server-Apps enthalten standardmäßig keinen HttpClient, der als Dienst konfiguriert ist. Stellen Sie einen HttpClient für eine Blazor Server-App bereit.

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

Blazor WebAssembly : Singleton

Blazor Server : Bereichsbezogen

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.
NavigationManager

Blazor WebAssembly : Singleton

Blazor Server : Bereichsbezogen

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

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 von Diensten zu einer Blazor WebAssembly-App

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

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        ...
        builder.Services.AddSingleton<IExampleDependency, ExampleDependency>();
        ...

        await builder.Build().RunAsync();
    }
}

Nachdem der Host erstellt wurde, kann auf die Dienste vom Stammbereich der Abhängigkeitsinjektion aus zugegriffen werden, bevor Komponenten bereitgestellt werden. Dies kann für die Ausführung der Initialisierungslogik vor dem Rendern von Inhalten nützlich sein:

public class Program
{
    public static async Task Main(string[] args)
    {
        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:

public class Program
{
    public static async Task Main(string[] args)
    {
        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 von Diensten zu einer Blazor Server-App

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 gemeinsamer Dienste in einer gehosteten Blazor WebAssembly-Lösung

Wenn mindestens ein gemeinsamer Dienst von den Projekten Server und Client einer gehosteten Blazor WebAssembly-Lösung benötigt wird, können Sie die gemeinsamen Dienstregistrierungen in einer Methode im Client-Projekt 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 beispielsweise eine ConfigureCommonServices-Methode im Client-Projekt:

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

Rufen Sie in der Datei Program.cs des Client-Projekts ConfigureCommonServices auf, um die gemeinsamen Dienste zu registrieren:

var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);

Rufen Sie in der ConfigureServices-Methode von Startup.cs des Server-Projekts ConfigureCommonServices auf, um die gemeinsamen Dienste für das Server-Projekt zu registrieren:

Client.Program.ConfigureCommonServices(services);

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

Lebensdauer von Diensten

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

Lebensdauer Beschreibung
Scoped

Blazor WebAssembly-Apps verfügen derzeit nicht über ein Konzept für Bereiche von Abhängigkeitsinjektionen. Scoped-registrierte Dienste verhalten sich wie Singleton-Dienste.

Das Blazor Server-Hostingmodell unterstützt die Scoped-Lebensdauer über HTTP-Anforderungen hinweg, nicht jedoch über SignalR-Verbindungs- bzw. Leitungsmeldungen zwischen Komponenten hinweg, die auf dem Client geladen sind. 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 eine Registerkarte 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 Blazor Server-Apps über bereichsbezogene Dienste hinweg finden Sie unter Blazor-Hostingmodell in ASP.NET Core.

Singleton Die Abhängigkeitsinjektion erstellt eine Einzelinstanz 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 Abhängigkeitsinjektionssystem basiert auf dem Abhängigkeitsinjektionssystem in ASP.NET Core. Weitere Informationen finden Sie unter Abhängigkeitsinjektion in ASP.NET Core.

Anfordern eines Diensts in einer Komponente

Nachdem die Dienste der Dienstsammlung hinzugefügt wurden, fügen Sie die 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.

Das folgende Beispiel veranschaulicht die Verwendung von @inject. Der Dienst, der Services.IDataAccess implementiert, wird in die Eigenschaft DataRepository der Komponente eingefügt. Beachten Sie, dass der Code nur die Abstraktion IDataAccess verwendet:

@page "/customer-list"
@inject IDataAccess DataRepository

@if (customers != null)
{
    <ul>
        @foreach (var customer in customers)
        {
            <li>@customer.FirstName @customer.LastName</li>
        }
    </ul>
}

@code {
    private IReadOnlyList<Customer> customers;

    protected override async Task OnInitializedAsync()
    {
        customers = await DataRepository.GetAllCustomersAsync();
    }

    private class Customer
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    private interface IDataAccess
    {
        public Task<IReadOnlyList<Customer>> GetAllCustomersAsync();
    }
}

Intern verwendet die generierte Eigenschaft (DataRepository) das Attribut [Inject]. 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 IDataAccess DataRepository { get; set; }

    ...
}

In Komponenten, die von der Basisklasse abgeleitet sind, ist die @inject-Direktive nicht erforderlich. Das InjectAttribute der Basisklasse ist ausreichend:

@page "/demo"
@inherits ComponentBase

<h1>Demo Component</h1>

Verwenden der Abhängigkeitsinjektion in Diensten

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 Constructor Injection verwendet werden. Erforderliche Dienste werden durch Hinzufügen von Parametern zum Konstruktor des Diensts hinzugefügt. Wenn die Abhängigkeitsinjektion den Dienst erstellt, erkennt sie die erforderlichen Dienste im Konstruktor und stellt sie entsprechend zur Verfügung. Im folgenden Beispiel empfängt der Konstruktor einen HttpClient über die Abhängigkeitsinjektion. HttpClient ist ein Standarddienst.

using System.Net.Http;

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

Voraussetzungen für die Constructor Injection:

  • Es muss einen Konstruktor geben, dessen Argumente alle von der Abhängigkeitsinjektion erfüllt werden können. Zusätzliche, nicht durch die Abhängigkeitsinjektion abgedeckte Parameter sind zulässig, wenn sie Standardwerte angeben.
  • Der anwendbare Konstruktor muss public sein.
  • Es muss ein anwendbarer Konstruktor vorhanden sein. Im Falle einer Mehrdeutigkeit löst die Abhängigkeitsinjektion eine Ausnahme aus.

Hilfsprogramm-Basiskomponentenklassen zur Verwaltung eines Bereichs für die Abhängigkeitsinjektion

In ASP.NET Core-Apps werden bereichsbezogene Dienste in der Regel der aktuellen Anforderung zugeordnet. Nachdem die Anforderung abgeschlossen ist, werden alle bereichsbezogenen oder vorübergehenden Dienste vom Abhängigkeitsinjektionssystem entsorgt. In Blazor Server-Apps besteht der Anforderungsbereich für die Dauer der Clientverbindung, was dazu führen kann, dass vorübergehende und bereichsbezogene Dienste viel länger als erwartet leben. In Blazor WebAssembly-Apps werden Dienste, die mit einer bereichsbezogenen Lebensdauer registriert sind, als Singletons behandelt, sodass sie länger leben als bereichsbezogene Dienste in typischen ASP.NET Core-Apps.

Hinweis

Informationen zum Erkennen vorübergehender verwerfbarer Dienste in einer App finden Sie in den folgenden Abschnitten:

Erkennen vorübergehend verwerfbarer Elemente in Blazor WebAssembly-AppsErkennen vorübergehend verwerfbarer Elemente in Blazor Server-Apps

Ein Ansatz, der die Lebensdauer eines Diensts in Blazor-Apps begrenzt, ist die Verwendung des Typs OwningComponentBase. OwningComponentBase ist ein abstrakter, von ComponentBase abgeleiteter Typ, der einen der Lebensdauer der Komponente entsprechenden Bereich für die Abhängigkeitsinjektion erstellt. Mit diesem Bereich ist es möglich, Abhängigkeitsinjektionsdienste mit einer bereichsbezogenen Lebensdauer zu nutzen und sie so lange wie die Komponente zu nutzen. Wenn die Komponente zerstört wird, werden auch die Dienste des bereichsbezogenen Dienstanbieters der Komponente entsorgt. Dies kann für Dienste nützlich sein, für die Folgendes gilt:

  • Sie sollten innerhalb einer Komponente wiederverwendet werden, da die vorübergehende Lebensdauer unangemessen ist.
  • Sie sollten nicht komponentenübergreifend freigegeben werden, da die Singleton-Lebensdauer unangemessen ist.

Zwei Versionen des Typs OwningComponentBase sind verfügbar:

  • OwningComponentBase ist ein abstraktes, verwerfbares, untergeordnetes Element vom Typ ComponentBase mit einer geschützten ScopedServices-Eigenschaft vom Typ IServiceProvider. Dieser 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 Bereich der Komponente zu verwenden, müssen die Dienste mit GetRequiredService oder GetService aufgelöst werden. Allen Diensten, die unter Verwendung des ScopedServices-Anbieters aufgelöst werden, werden ihre Abhängigkeiten aus demselben Bereich bereitgestellt.

    @page "/preferences"
    @using Microsoft.Extensions.DependencyInjection
    @inherits OwningComponentBase
    
    <h1>User (@UserService.Name)</h1>
    
    <ul>
        @foreach (var setting in SettingService.GetSettings())
        {
            <li>@setting.SettingName: @setting.SettingValue</li>
        }
    </ul>
    
    @code {
        private IUserService UserService { get; set; }
        private ISettingService SettingService { get; set; }
    
        protected override void OnInitialized()
        {
            UserService = ScopedServices.GetRequiredService<IUserService>();
            SettingService = ScopedServices.GetRequiredService<ISettingService>();
        }
    
        public class Setting
        {
            public string SettingName { get; set; }
            public string SettingValue { get; set; }
        }
    
        public interface IUserService
        {
            public string Name { get; set; }
        }
    
        public interface ISettingService
        {
            public IList<Setting> GetSettings();
        }
    }
    
  • OwningComponentBase<TService> ist abgeleitet von OwningComponentBase und fügt eine Eigenschaft Service hinzu, die eine Instanz von T vom bereichsbezogenen Abhängigkeitsinjektionsanbieter zurückgibt. Dieser Typ ist eine komfortable Methode für den Zugriff auf bereichsbezogene Dienste, ohne eine Instanz von IServiceProvider zu verwenden, wenn es einen primären Dienst gibt, den die Anwendung aus dem Abhängigkeitsinjektionscontainer unter Verwendung des Bereichs der Komponente benötigt. Die Eigenschaft ScopedServices ist verfügbar, sodass die App bei Bedarf auch Dienste mit anderen Typen erhalten 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>
    

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

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

Erkennen vorübergehender verwerfbarer Elemente in Blazor WebAssembly-Apps

Im folgenden Beispiel wird veranschaulicht, wie verwerfbare temporäre Dienste in einer App erkannt werden, die OwningComponentBase verwenden sollten. Weitere Informationen finden Sie im Abschnitt Hilfsprogramm-Basiskomponentenklassen zur Verwaltung eines Bereichs für die Abhängigkeitsinjektion.

DetectIncorrectUsagesOfTransientDisposables.cs:

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection
{
    using BlazorWebAssemblyTransientDisposable;
    using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

    public static class WebHostBuilderTransientDisposableExtensions
    {
        public static WebAssemblyHostBuilder DetectIncorrectUsageOfTransients(
            this WebAssemblyHostBuilder builder)
        {
            builder
                .ConfigureContainer(
                    new DetectIncorrectUsageOfTransientDisposablesServiceFactory());

            return builder;
        }

        public static WebAssemblyHost EnableTransientDisposableDetection(
            this WebAssemblyHost webAssemblyHost)
        {
            webAssemblyHost.Services
                .GetRequiredService<ThrowOnTransientDisposable>().ShouldThrow = true;

            return webAssemblyHost;
        }
    }
}

namespace BlazorWebAssemblyTransientDisposable
{
    public class DetectIncorrectUsageOfTransientDisposablesServiceFactory 
        : IServiceProviderFactory<IServiceCollection>
    {
        public IServiceCollection CreateBuilder(IServiceCollection services) => 
            services;

        public IServiceProvider CreateServiceProvider(
            IServiceCollection containerBuilder)
        {
            var collection = new ServiceCollection();

            foreach (var descriptor in containerBuilder)
            {
                if (descriptor.Lifetime == ServiceLifetime.Transient &&
                    descriptor.ImplementationType != null && 
                    typeof(IDisposable).IsAssignableFrom(
                        descriptor.ImplementationType))
                {
                    collection.Add(CreatePatchedDescriptor(descriptor));
                }
                else if (descriptor.Lifetime == ServiceLifetime.Transient &&
                         descriptor.ImplementationFactory != null)
                {
                    collection.Add(CreatePatchedFactoryDescriptor(descriptor));
                }
                else
                {
                    collection.Add(descriptor);
                }
            }

            collection.AddScoped<ThrowOnTransientDisposable>();

            return collection.BuildServiceProvider();
        }

        private ServiceDescriptor CreatePatchedFactoryDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) =>
                {
                    var originalFactory = original.ImplementationFactory;
                    var originalResult = originalFactory(sp);

                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow && 
                        originalResult is IDisposable d)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            $"transient disposable service {d.GetType().Name} in " +
                            "the wrong scope. Use an 'OwningComponentBase<T>' " +
                            "component base class for the service 'T' you are " +
                            "trying to resolve.");
                    }

                    return originalResult;
                },
                original.Lifetime);

            return newDescriptor;
        }

        private ServiceDescriptor CreatePatchedDescriptor(ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) => {
                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                        "transient disposable service " +
                        $"{original.ImplementationType.Name} in the wrong " +
                        "scope. Use an 'OwningComponentBase<T>' component base " +
                        "class for the service 'T' you are trying to resolve.");
                    }

                    return ActivatorUtilities.CreateInstance(sp, 
                        original.ImplementationType);
                },
                ServiceLifetime.Transient);
    
            return newDescriptor;
        }
    }

    internal class ThrowOnTransientDisposable
    {
        public bool ShouldThrow { get; set; }
    }
}

Im folgenden Beispiel wird TransientDisposable erkannt (Program.cs):

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.DetectIncorrectUsageOfTransients();
        builder.RootComponents.Add<App>("app");

        builder.Services.AddTransient<TransientDisposable>();
        builder.Services.AddScoped(sp =>
            new HttpClient
            {
                BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
            });

        var host = builder.Build();
        host.EnableTransientDisposableDetection();
        await host.RunAsync();
    }
}

public class TransientDisposable : IDisposable
{
    public void Dispose() => throw new NotImplementedException();
}

Die App kann vorübergehende verwerfbare Elemente registrieren, ohne eine Ausnahme auszulösen. Der Versuch, ein vorübergehendes verwerfbares Ergebnis aufzulösen, führt jedoch zu einer InvalidOperationException, wie im folgenden Beispiel gezeigt.

Pages/TransientExample.razor:

@page "/transient-example"
@inject TransientDisposable TransientDisposable

<h1>Transient Disposable Detection</h1>

Navigieren Sie zur TransientExample-Komponente unter /transient-example, und eine InvalidOperationException wird ausgelöst, wenn das Framework versucht, eine Instanz von TransientDisposable zu erstellen:

System.InvalidOperationException: Es wurde versucht, den vorübergehenden verwerfbaren Dienst TransientDisposable im falschen Bereich aufzulösen. Verwenden Sie für den Dienst „T“, den Sie auflösen möchten, die Komponentenbasisklasse „OwningComponentBase<T>“.

Erkennen vorübergehender verwerfbarer Elemente in Blazor Server-Apps

Im folgenden Beispiel wird veranschaulicht, wie verwerfbare temporäre Dienste in einer App erkannt werden, die OwningComponentBase verwenden sollten. Weitere Informationen finden Sie im Abschnitt Hilfsprogramm-Basiskomponentenklassen zur Verwaltung eines Bereichs für die Abhängigkeitsinjektion.

DetectIncorrectUsagesOfTransientDisposables.cs:

using System;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Microsoft.Extensions.DependencyInjection
{
    using BlazorServerTransientDisposable;

    public static class WebHostBuilderTransientDisposableExtensions
    {
        public static IHostBuilder DetectIncorrectUsageOfTransients(
            this IHostBuilder builder)
        {
            builder
                .UseServiceProviderFactory(
                    new DetectIncorrectUsageOfTransientDisposablesServiceFactory())
                .ConfigureServices(
                    s => s.TryAddEnumerable(ServiceDescriptor.Scoped<CircuitHandler,
                        ThrowOnTransientDisposableHandler>()));

            return builder;
        }
    }
}

namespace BlazorServerTransientDisposable
{
    internal class ThrowOnTransientDisposableHandler : CircuitHandler
    {
        public ThrowOnTransientDisposableHandler(
            ThrowOnTransientDisposable throwOnTransientDisposable)
        {
            throwOnTransientDisposable.ShouldThrow = true;
        }
    }

    public class DetectIncorrectUsageOfTransientDisposablesServiceFactory 
        : IServiceProviderFactory<IServiceCollection>
    {
        public IServiceCollection CreateBuilder(IServiceCollection services) => 
            services;

        public IServiceProvider CreateServiceProvider(
            IServiceCollection containerBuilder)
        {
            var collection = new ServiceCollection();

            foreach (var descriptor in containerBuilder)
            {
                if (descriptor.Lifetime == ServiceLifetime.Transient &&
                    descriptor.ImplementationType != null && 
                    typeof(IDisposable).IsAssignableFrom(
                        descriptor.ImplementationType))
                {
                    collection.Add(CreatePatchedDescriptor(descriptor));
                }
                else if (descriptor.Lifetime == ServiceLifetime.Transient &&
                         descriptor.ImplementationFactory != null)
                {
                    collection.Add(CreatePatchedFactoryDescriptor(descriptor));
                }
                else
                {
                    collection.Add(descriptor);
                }
            }

            collection.AddScoped<ThrowOnTransientDisposable>();

            return collection.BuildServiceProvider();
        }

        private ServiceDescriptor CreatePatchedFactoryDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) =>
                {
                    var originalFactory = original.ImplementationFactory;
                    var originalResult = originalFactory(sp);

                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow && 
                        originalResult is IDisposable d)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            $"transient disposable service {d.GetType().Name} in " +
                            "the wrong scope. Use an 'OwningComponentBase<T>' " +
                            "component base class for the service 'T' you are " +
                            "trying to resolve.");
                    }

                    return originalResult;
                },
                original.Lifetime);

            return newDescriptor;
        }

        private ServiceDescriptor CreatePatchedDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) => {
                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            "transient disposable service " +
                            $"{original.ImplementationType.Name} in the wrong " +
                            "scope. Use an 'OwningComponentBase<T>' component " +
                            "base class for the service 'T' you are trying to " +
                            "resolve.");
                    }

                    return ActivatorUtilities.CreateInstance(sp, 
                        original.ImplementationType);
                },
                ServiceLifetime.Transient);
    
            return newDescriptor;
        }
    }

    internal class ThrowOnTransientDisposable
    {
        public bool ShouldThrow { get; set; }
    }
}

Fügen Sie Program.cs den Namespace für Microsoft.Extensions.DependencyInjection hinzu:

using Microsoft.Extensions.DependencyInjection;

In Program.CreateHostBuilder von Program.cs:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .DetectIncorrectUsageOfTransients()
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

Im folgenden Beispiel wird TransientDependency erkannt (Startup.cs):

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddSingleton<WeatherForecastService>();
    services.AddTransient<TransientDependency>();
    services.AddTransient<ITransitiveTransientDisposableDependency, 
        TransitiveTransientDisposableDependency>();
}

public class TransitiveTransientDisposableDependency 
    : ITransitiveTransientDisposableDependency, IDisposable
{
    public void Dispose() { }
}

public interface ITransitiveTransientDisposableDependency
{
}

public class TransientDependency
{
    private readonly ITransitiveTransientDisposableDependency 
        _transitiveTransientDisposableDependency;

    public TransientDependency(ITransitiveTransientDisposableDependency 
        transitiveTransientDisposableDependency)
    {
        _transitiveTransientDisposableDependency = 
            transitiveTransientDisposableDependency;
    }
}

Die App kann vorübergehende verwerfbare Elemente registrieren, ohne eine Ausnahme auszulösen. Der Versuch, ein vorübergehendes verwerfbares Ergebnis aufzulösen, führt jedoch zu einer InvalidOperationException, wie im folgenden Beispiel gezeigt.

Pages/TransientExample.razor:

@page "/transient-example"
@inject TransientDependency TransientDependency

<h1>Transient Disposable Detection</h1>

Navigieren Sie zur TransientExample-Komponente unter /transient-example, und eine InvalidOperationException wird ausgelöst, wenn das Framework versucht, eine Instanz von TransientDependency zu erstellen:

System.InvalidOperationException: Es wurde versucht, den temporären verwerfbaren Dienst „TransientDependency“ im falschen Bereich aufzulösen. Verwenden Sie für den Dienst „T“, den Sie auflösen möchten, die Komponentenbasisklasse „OwningComponentBase<T>“.

Zusätzliche Ressourcen