title | author | description | monikerRange | ms.author | ms.custom | ms.date | uid |
---|---|---|---|---|---|---|---|
ASP.NET Core Blazor dependency injection |
guardrex |
Learn how Blazor apps can inject services into components. |
>= aspnetcore-3.1 |
riande |
mvc |
02/09/2024 |
blazor/fundamentals/dependency-injection |
By Rainer Stropek and Mike Rousos
This article explains how Blazor apps can inject services into components.
Dependency injection (DI) is a technique for accessing services configured in a central location:
- Framework-registered services can be injected directly into Razor components.
- Blazor apps define and register custom services and make them available throughout the app via DI.
Note
We recommend reading xref:fundamentals/dependency-injection before reading this topic.
The services shown in the following table are commonly used in Blazor apps.
Service | Lifetime | Description |
---|---|---|
xref:System.Net.Http.HttpClient | Scoped | Provides methods for sending HTTP requests and receiving HTTP responses from a resource identified by a URI. Client-side, an instance of xref:System.Net.Http.HttpClient is registered by the app in the Server-side, an xref:System.Net.Http.HttpClient isn't configured as a service by default. In server-side code, provide an xref:System.Net.Http.HttpClient. For more information, see xref:blazor/call-web-api. An xref:System.Net.Http.HttpClient is registered as a scoped service, not singleton. For more information, see the Service lifetime section. |
xref:Microsoft.JSInterop.IJSRuntime | Client-side: Singleton Server-side: Scoped The Blazor framework registers xref:Microsoft.JSInterop.IJSRuntime in the app's service container. |
Represents an instance of a JavaScript runtime where JavaScript calls are dispatched. For more information, see xref:blazor/js-interop/call-javascript-from-dotnet. When seeking to inject the service into a singleton service on the server, take either of the following approaches:
|
xref:Microsoft.AspNetCore.Components.NavigationManager | Client-side: Singleton Server-side: Scoped The Blazor framework registers xref:Microsoft.AspNetCore.Components.NavigationManager in the app's service container. |
Contains helpers for working with URIs and navigation state. For more information, see URI and navigation state helpers. |
Additional services registered by the Blazor framework are described in the documentation where they're used to describe Blazor features, such as configuration and logging.
A custom service provider doesn't automatically provide the default services listed in the table. If you use a custom service provider and require any of the services shown in the table, add the required services to the new service provider.
Configure services for the app's service collection in the Program
file. In the following example, the ExampleDependency
implementation is registered for IExampleDependency
:
var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<IExampleDependency, ExampleDependency>();
...
await builder.Build().RunAsync();
After the host is built, services are available from the root DI scope before any components are rendered. This can be useful for running initialization logic before rendering content:
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();
The host provides a central configuration instance for the app. Building on the preceding example, the weather service's URL is passed from a default configuration source (for example, appsettings.json
) to InitializeWeatherAsync
:
var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<WeatherService>();
...
var host = builder.Build();
var weatherService = host.Services.GetRequiredService<WeatherService>();
await weatherService.InitializeWeatherAsync(
host.Configuration["WeatherServiceUrl"]);
await host.RunAsync();
After creating a new app, examine part of the Program
file:
:::moniker range=">= aspnetcore-8.0"
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
:::moniker-end
:::moniker range="< aspnetcore-8.0"
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
:::moniker-end
The builder
variable represents a xref:Microsoft.AspNetCore.Builder.WebApplicationBuilder with an xref:Microsoft.Extensions.DependencyInjection.IServiceCollection, which is a list of service descriptor objects. Services are added by providing service descriptors to the service collection. The following example demonstrates the concept with the IDataAccess
interface and its concrete implementation DataAccess
:
builder.Services.AddSingleton<IDataAccess, DataAccess>();
:::moniker range="< aspnetcore-6.0"
After creating a new app, examine the Startup.ConfigureServices
method in Startup.cs
:
using Microsoft.Extensions.DependencyInjection;
...
public void ConfigureServices(IServiceCollection services)
{
...
}
The xref:Microsoft.Extensions.Hosting.IHostBuilder.ConfigureServices%2A method is passed an xref:Microsoft.Extensions.DependencyInjection.IServiceCollection, which is a list of service descriptor objects. Services are added in the ConfigureServices
method by providing service descriptors to the service collection. The following example demonstrates the concept with the IDataAccess
interface and its concrete implementation DataAccess
:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IDataAccess, DataAccess>();
}
:::moniker-end
If one or more common services are required client- and server-side, you can place the common service registrations in a method client-side and call the method to register the services in both projects.
First, factor common service registrations into a separate method. For example, create a ConfigureCommonServices
method client-side:
public static void ConfigureCommonServices(IServiceCollection services)
{
services.Add...;
}
For the client-side Program
file, call ConfigureCommonServices
to register the common services:
var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
ConfigureCommonServices(builder.Services);
In the server-side Program
file, call ConfigureCommonServices
to register the common services:
var builder = WebApplication.CreateBuilder(args);
...
Client.Program.ConfigureCommonServices(builder.Services);
For an example of this approach, see xref:blazor/security/webassembly/additional-scenarios#prerendering-with-authentication.
:::moniker range=">= aspnetcore-8.0"
This section only applies to WebAssembly components in Blazor Web Apps.
Blazor Web Apps normally prerender client-side WebAssembly components. If an app is run with a required service only registered in the .Client
project, executing the app results in a runtime error similar to the following when a component attempts to use the required service during prerendering:
:::no-loc text="InvalidOperationException: Cannot provide a value for {PROPERTY} on type '{ASSEMBLY}}.Client.Pages.{COMPONENT NAME}'. There is no registered service of type '{SERVICE}'.":::
Use either of the following approaches to resolve this problem:
- Register the service in the main project to make it available during component prerendering.
- If prerendering isn't required for the component, disable prerendering by following the guidance in xref:blazor/components/render-modes#prerendering. If you adopt this approach, you don't need to register the service in the main project.
For more information, see Client-side services fail to resolve during prerendering.
:::moniker-end
Services can be configured with the lifetimes shown in the following table.
Lifetime | Description |
---|---|
xref:Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Scoped%2A | Client-side doesn't currently have a concept of DI scopes. Server-side development supports the
For more information on preserving user state in server-side apps, see xref:blazor/state-management. |
xref:Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Singleton%2A | DI creates a single instance of the service. All components requiring a Singleton service receive the same instance of the service. |
xref:Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Transient%2A | Whenever a component obtains an instance of a Transient service from the service container, it receives a new instance of the service. |
The DI system is based on the DI system in ASP.NET Core. For more information, see xref:fundamentals/dependency-injection.
:::moniker range=">= aspnetcore-9.0"
For injecting services into components, Blazor supports constructor injection and property injection.
After services are added to the service collection, inject one or more services into components with constructor injection. The following example injects the NavigationManager
service.
ConstructorInjection.razor
:
@page "/constructor-injection"
<button @onclick="@(() => Navigation.NavigateTo("/counter"))">
Take me to the Counter component
</button>
ConstructorInjection.razor.cs
:
using Microsoft.AspNetCore.Components;
public partial class ConstructorInjection(NavigationManager navigation)
{
protected NavigationManager Navigation { get; } = navigation;
}
:::moniker-end
After services are added to the service collection, inject one or more services into components with the @inject
Razor directive, which has two parameters:
- Type: The type of the service to inject.
- Property: The name of the property receiving the injected app service. The property doesn't require manual creation. The compiler creates the property.
For more information, see xref:mvc/views/dependency-injection.
Use multiple @inject
statements to inject different services.
The following example demonstrates shows how to use the @inject
directive. The service implementing Services.NavigationManager
is injected into the component's property Navigation
. Note how the code is only using the NavigationManager
abstraction.
PropertyInjection.razor
:
@page "/property-injection"
@inject NavigationManager Navigation
<button @onclick="@(() => Navigation.NavigateTo("/counter"))">
Take me to the Counter component
</button>
Internally, the generated property (Navigation
) uses the [Inject]
attribute. Typically, this attribute isn't used directly. If a base class is required for components and injected properties are also required for the base class, manually add the [Inject]
attribute:
using Microsoft.AspNetCore.Components;
public class ComponentBase : IComponent
{
[Inject]
protected NavigationManager Navigation { get; set; } = default!;
...
}
Note
Since injected services are expected to be available, the default literal with the null-forgiving operator (default!
) is assigned in .NET 6 or later. For more information, see Nullable reference types (NRTs) and .NET compiler null-state static analysis.
In components derived from a base class, the @inject
directive isn't required. The xref:Microsoft.AspNetCore.Components.InjectAttribute of the base class is sufficient. The component only requires the @inherits
directive. In the following example, any injected services of CustomComponentBase
are available to the Demo
component:
@page "/demo"
@inherits CustomComponentBase
Complex services might require additional services. In the following example, DataAccess
requires the xref:System.Net.Http.HttpClient default service. @inject
(or the [Inject]
attribute) isn't available for use in services. Constructor injection must be used instead. Required services are added by adding parameters to the service's constructor. When DI creates the service, it recognizes the services it requires in the constructor and provides them accordingly. In the following example, the constructor receives an xref:System.Net.Http.HttpClient via DI. xref:System.Net.Http.HttpClient is a default service.
using System.Net.Http;
public class DataAccess : IDataAccess
{
public DataAccess(HttpClient http)
{
...
}
}
Prerequisites for constructor injection:
- One constructor must exist whose arguments can all be fulfilled by DI. Additional parameters not covered by DI are allowed if they specify default values.
- The applicable constructor must be
public
. - One applicable constructor must exist. In case of an ambiguity, DI throws an exception.
:::moniker range=">= aspnetcore-8.0"
Blazor supports injecting keyed services using the [Inject]
attribute. Keys allow for scoping of registration and consumption of services when using dependency injection. Use the xref:Microsoft.AspNetCore.Components.InjectAttribute.Key?displayProperty=nameWithType property to specify the key for the service to inject:
[Inject(Key = "my-service")]
public IMyService MyService { get; set; }
:::moniker-end
In non-Blazor ASP.NET Core apps, scoped and transient services are typically scoped to the current request. After the request completes, scoped and transient services are disposed by the DI system.
In interactive server-side Blazor apps, the DI scope lasts for the duration of the circuit (the SignalR connection between the client and server), which can result in scoped and disposable transient services living much longer than the lifetime of a single component. Therefore, don't directly inject a scoped service into a component if you intend the service lifetime to match the lifetime of the component. Transient services injected into a component that don't implement xref:System.IDisposable are garbage collected when the component is disposed. However, injected transient services that implement xref:System.IDisposable are maintained by the DI container for the lifetime of the circuit, which prevents service garbage collection when the component is disposed and results in a memory leak. An alternative approach for scoped services based on the xref:Microsoft.AspNetCore.Components.OwningComponentBase type is described later in this section, and disposable transient services shouldn't be used at all. For more information, see Design for solving transient disposables on Blazor Server (dotnet/aspnetcore
#26676).
Even in client-side Blazor apps that don't operate over a circuit, services registered with a scoped lifetime are treated as singletons, so they live longer than scoped services in typical ASP.NET Core apps. Client-side disposable transient services also live longer than the components where they're injected because the DI container, which holds references to disposable services, persists for the lifetime of the app, preventing garbage collection on the services. Although long-lived disposable transient services are of greater concern on the server, they should be avoided as client service registrations as well. Use of the xref:Microsoft.AspNetCore.Components.OwningComponentBase type is also recommended for client-side scoped services to control service lifetime, and disposable transient services shouldn't be used at all.
An approach that limits a service lifetime is use of the xref:Microsoft.AspNetCore.Components.OwningComponentBase type. xref:Microsoft.AspNetCore.Components.OwningComponentBase is an abstract type derived from xref:Microsoft.AspNetCore.Components.ComponentBase that creates a DI scope corresponding to the lifetime of the component. Using this scope, a component can inject services with a scoped lifetime and have them live as long as the component. When the component is destroyed, services from the component's scoped service provider are disposed as well. This can be useful for services reused within a component but not shared across components.
Two versions of xref:Microsoft.AspNetCore.Components.OwningComponentBase type are available and described in the next two sections:
xref:Microsoft.AspNetCore.Components.OwningComponentBase is an abstract, disposable child of the xref:Microsoft.AspNetCore.Components.ComponentBase type with a protected xref:Microsoft.AspNetCore.Components.OwningComponentBase.ScopedServices property of type xref:System.IServiceProvider. The provider can be used to resolve services that are scoped to the lifetime of the component.
DI services injected into the component using @inject
or the [Inject]
attribute aren't created in the component's scope. To use the component's scope, services must be resolved using xref:Microsoft.AspNetCore.Components.OwningComponentBase.ScopedServices with either xref:Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService%2A or xref:System.IServiceProvider.GetService%2A. Any services resolved using the xref:Microsoft.AspNetCore.Components.OwningComponentBase.ScopedServices provider have their dependencies provided in the component's scope.
The following example demonstrates the difference between injecting a scoped service directly and resolving a service using xref:Microsoft.AspNetCore.Components.OwningComponentBase.ScopedServices on the server. The following interface and implementation for a time travel class include a DT
property to hold a xref:System.DateTime value. The implementation calls xref:System.DateTime.Now?displayProperty=nameWithType to set DT
when the TimeTravel
class is instantiated.
ITimeTravel.cs
:
public interface ITimeTravel
{
public DateTime DT { get; set; }
}
TimeTravel.cs
:
public class TimeTravel : ITimeTravel
{
public DateTime DT { get; set; } = DateTime.Now;
}
The service is registered as scoped in the server-side Program
file. Server-side, scoped services have a lifetime equal to the duration of the circuit.
In the Program
file:
builder.Services.AddScoped<ITimeTravel, TimeTravel>();
In the following TimeTravel
component:
- The time travel service is directly injected with
@inject
asTimeTravel1
. - The service is also resolved separately with xref:Microsoft.AspNetCore.Components.OwningComponentBase.ScopedServices and xref:Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService%2A as
TimeTravel2
.
TimeTravel.razor
:
:::moniker range=">= aspnetcore-8.0"
@page "/time-travel"
@inject ITimeTravel TimeTravel1
@inherits OwningComponentBase
<h1><code>OwningComponentBase</code> Example</h1>
<ul>
<li>TimeTravel1.DT: @TimeTravel1?.DT</li>
<li>TimeTravel2.DT: @TimeTravel2?.DT</li>
</ul>
@code {
private ITimeTravel TimeTravel2 { get; set; } = default!;
protected override void OnInitialized()
{
TimeTravel2 = ScopedServices.GetRequiredService<ITimeTravel>();
}
}
:::moniker-end
:::moniker range="< aspnetcore-8.0"
@page "/time-travel"
@inject ITimeTravel TimeTravel1
@inherits OwningComponentBase
<h1><code>OwningComponentBase</code> Example</h1>
<ul>
<li>TimeTravel1.DT: @TimeTravel1?.DT</li>
<li>TimeTravel2.DT: @TimeTravel2?.DT</li>
</ul>
@code {
private ITimeTravel TimeTravel2 { get; set; } = default!;
protected override void OnInitialized()
{
TimeTravel2 = ScopedServices.GetRequiredService<ITimeTravel>();
}
}
:::moniker-end
Initially navigating to the TimeTravel
component, the time travel service is instantiated twice when the component loads, and TimeTravel1
and TimeTravel2
have the same initial value:
:::no-loc text="TimeTravel1.DT: 8/31/2022 2:54:45 PM":::
:::no-loc text="TimeTravel2.DT: 8/31/2022 2:54:45 PM":::
When navigating away from the TimeTravel
component to another component and back to the TimeTravel
component:
TimeTravel1
is provided the same service instance that was created when the component first loaded, so the value ofDT
remains the same.TimeTravel2
obtains a newITimeTravel
service instance inTimeTravel2
with a new DT value.
:::no-loc text="TimeTravel1.DT: 8/31/2022 2:54:45 PM":::
:::no-loc text="TimeTravel2.DT: 8/31/2022 2:54:48 PM":::
TimeTravel1
is tied to the user's circuit, which remains intact and isn't disposed until the underlying circuit is deconstructed. For example, the service is disposed if the circuit is disconnected for the disconnected circuit retention period.
In spite of the scoped service registration in the Program
file and the longevity of the user's circuit, TimeTravel2
receives a new ITimeTravel
service instance each time the component is initialized.
xref:Microsoft.AspNetCore.Components.OwningComponentBase%601 derives from xref:Microsoft.AspNetCore.Components.OwningComponentBase and adds a xref:Microsoft.AspNetCore.Components.OwningComponentBase%601.Service%2A property that returns an instance of T
from the scoped DI provider. This type is a convenient way to access scoped services without using an instance of xref:System.IServiceProvider when there's one primary service the app requires from the DI container using the component's scope. The xref:Microsoft.AspNetCore.Components.OwningComponentBase.ScopedServices property is available, so the app can get services of other types, if necessary.
@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>
:::moniker range=">= aspnetcore-6.0"
Custom code can be added to a client-side Blazor app to detect disposable transient services in an app that should use xref:Microsoft.AspNetCore.Components.OwningComponentBase. This approach is useful if you're concerned that code added to the app in the future consumes one or more transient disposable services, including services added by libraries. Demonstration code is available in the Blazor samples GitHub repository.
Inspect the following in .NET 6 or later versions of the BlazorSample_WebAssembly
sample:
DetectIncorrectUsagesOfTransientDisposables.cs
Services/TransientDisposableService.cs
- In
Program.cs
:- The app's
Services
namespace is provided at the top of the file (using BlazorSample.Services;
). DetectIncorrectUsageOfTransients
is called immediately after thebuilder
is assigned from xref:Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder.CreateDefault%2A?displayProperty=nameWithType.- The
TransientDisposableService
is registered (builder.Services.AddTransient<TransientDisposableService>();
). EnableTransientDisposableDetection
is called on the built host in the processing pipeline of the app (host.EnableTransientDisposableDetection();
).
- The app's
- The app registers the
TransientDisposableService
service without throwing an exception. However, attempting to resolve the service inTransientService.razor
throws an xref:System.InvalidOperationException when the framework attempts to construct an instance ofTransientDisposableService
.
Custom code can be added to a server-side Blazor app to detect server-side disposable transient services in an app that should use xref:Microsoft.AspNetCore.Components.OwningComponentBase. This approach is useful if you're concerned that code added to the app in the future consumes one or more transient disposable services, including services added by libraries. Demonstration code is available in the Blazor samples GitHub repository.
:::moniker-end
:::moniker range=">= aspnetcore-8.0"
Inspect the following in .NET 8 or later versions of the BlazorSample_BlazorWebApp
sample:
:::moniker-end
:::moniker range=">= aspnetcore-6.0 < aspnetcore-8.0"
Inspect the following in .NET 6 or .NET 7 versions of the BlazorSample_Server
sample:
:::moniker-end
:::moniker range=">= aspnetcore-6.0"
DetectIncorrectUsagesOfTransientDisposables.cs
Services/TransitiveTransientDisposableDependency.cs
:- In
Program.cs
:- The app's
Services
namespace is provided at the top of the file (using BlazorSample.Services;
). DetectIncorrectUsageOfTransients
is called on the host builder (builder.DetectIncorrectUsageOfTransients();
).- The
TransientDependency
service is registered (builder.Services.AddTransient<TransientDependency>();
). - The
TransitiveTransientDisposableDependency
is registered forITransitiveTransientDisposableDependency
(builder.Services.AddTransient<ITransitiveTransientDisposableDependency, TransitiveTransientDisposableDependency>();
).
- The app's
- The app registers the
TransientDependency
service without throwing an exception. However, attempting to resolve the service inTransientService.razor
throws an xref:System.InvalidOperationException when the framework attempts to construct an instance ofTransientDependency
.
Transient service registrations for xref:System.Net.Http.IHttpClientFactory/xref:System.Net.Http.HttpClient handlers are recommended. If the app contains xref:System.Net.Http.IHttpClientFactory/xref:System.Net.Http.HttpClient handlers and uses the xref:Microsoft.Extensions.DependencyInjection.IRemoteAuthenticationBuilder%602 to add support for authentication, the following transient disposables for client-side authentication are also discovered, which is expected and can be ignored:
- xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.BaseAddressAuthorizationMessageHandler
- xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.AuthorizationMessageHandler
Other instances of xref:System.Net.Http.IHttpClientFactory/xref:System.Net.Http.HttpClient are also discovered. These instances can also be ignored.
The Blazor sample apps in the Blazor samples GitHub repository demonstrate the code to detect transient disposables. However, the code is deactivated because the sample apps include xref:System.Net.Http.IHttpClientFactory/xref:System.Net.Http.HttpClient handlers.
To activate the demonstration code and witness its operation:
-
Uncomment the transient disposable lines in
Program.cs
. -
Remove the conditional check in
NavLink.razor
that prevents theTransientService
component from displaying in the app's navigation sidebar:- else if (name != "TransientService") + else
-
Run the sample app and navigate to to the
TransientService
component at/transient-service
.
:::moniker-end
For more information, see xref:blazor/blazor-ef-core.
:::moniker range=">= aspnetcore-8.0"
Circuit activity handlers provide an approach for accessing scoped Blazor services from other non-Blazor dependency injection (DI) scopes, such as scopes created using xref:System.Net.Http.IHttpClientFactory.
Prior to the release of ASP.NET Core in .NET 8, accessing circuit-scoped services from other dependency injection scopes required using a custom base component type. With circuit activity handlers, a custom base component type isn't required, as the following example demonstrates:
public class CircuitServicesAccessor
{
static readonly AsyncLocal<IServiceProvider> blazorServices = new();
public IServiceProvider? Services
{
get => blazorServices.Value;
set => blazorServices.Value = value;
}
}
public class ServicesAccessorCircuitHandler : CircuitHandler
{
readonly IServiceProvider services;
readonly CircuitServicesAccessor circuitServicesAccessor;
public ServicesAccessorCircuitHandler(IServiceProvider services,
CircuitServicesAccessor servicesAccessor)
{
this.services = services;
this.circuitServicesAccessor = servicesAccessor;
}
public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
Func<CircuitInboundActivityContext, Task> next)
{
return async context =>
{
circuitServicesAccessor.Services = services;
await next(context);
circuitServicesAccessor.Services = null;
};
}
}
public static class CircuitServicesServiceCollectionExtensions
{
public static IServiceCollection AddCircuitServicesAccessor(
this IServiceCollection services)
{
services.AddScoped<CircuitServicesAccessor>();
services.AddScoped<CircuitHandler, ServicesAccessorCircuitHandler>();
return services;
}
}
Access the circuit-scoped services by injecting the CircuitServicesAccessor
where it's needed.
For an example that shows how to access the xref:Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider from a xref:System.Net.Http.DelegatingHandler set up using xref:System.Net.Http.IHttpClientFactory, see xref:blazor/security/server/additional-scenarios#access-authenticationstateprovider-in-outgoing-request-middleware.
:::moniker-end
:::moniker range="< aspnetcore-8.0"
There may be times when a Razor component invokes asynchronous methods that execute code in a different DI scope. Without the correct approach, these DI scopes don't have access to Blazor's services, such as xref:Microsoft.JSInterop.IJSRuntime and xref:Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.
For example, xref:System.Net.Http.HttpClient instances created using xref:System.Net.Http.IHttpClientFactory have their own DI service scope. As a result, xref:System.Net.Http.HttpMessageHandler instances configured on the xref:System.Net.Http.HttpClient aren't able to directly inject Blazor services.
Create a class BlazorServiceAccessor
that defines an AsyncLocal
, which stores the Blazor xref:System.IServiceProvider for the current asynchronous context. A BlazorServiceAcccessor
instance can be acquired from within a different DI service scope to access Blazor services.
BlazorServiceAccessor.cs
:
internal sealed class BlazorServiceAccessor
{
private static readonly AsyncLocal<BlazorServiceHolder> s_currentServiceHolder = new();
public IServiceProvider? Services
{
get => s_currentServiceHolder.Value?.Services;
set
{
if (s_currentServiceHolder.Value is { } holder)
{
// Clear the current IServiceProvider trapped in the AsyncLocal.
holder.Services = null;
}
if (value is not null)
{
// Use object indirection to hold the IServiceProvider in an AsyncLocal
// so it can be cleared in all ExecutionContexts when it's cleared.
s_currentServiceHolder.Value = new() { Services = value };
}
}
}
private sealed class BlazorServiceHolder
{
public IServiceProvider? Services { get; set; }
}
}
To set the value of BlazorServiceAccessor.Services
automatically when an async
component method is invoked, create a custom base component that re-implements the three primary asynchronous entry points into Razor component code:
- xref:Microsoft.AspNetCore.Components.IComponent.SetParametersAsync%2A?displayProperty=nameWithType
- xref:Microsoft.AspNetCore.Components.IHandleEvent.HandleEventAsync%2A?displayProperty=nameWithType
- xref:Microsoft.AspNetCore.Components.IHandleAfterRender.OnAfterRenderAsync%2A?displayProperty=nameWithType
The following class demonstrates the implementation for the base component.
CustomComponentBase.cs
:
using Microsoft.AspNetCore.Components;
public class CustomComponentBase : ComponentBase, IHandleEvent, IHandleAfterRender
{
private bool hasCalledOnAfterRender;
[Inject]
private IServiceProvider Services { get; set; } = default!;
[Inject]
private BlazorServiceAccessor BlazorServiceAccessor { get; set; } = default!;
public override Task SetParametersAsync(ParameterView parameters)
=> InvokeWithBlazorServiceContext(() => base.SetParametersAsync(parameters));
Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
=> InvokeWithBlazorServiceContext(() =>
{
var task = callback.InvokeAsync(arg);
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled;
StateHasChanged();
return shouldAwaitTask ?
CallStateHasChangedOnAsyncCompletion(task) :
Task.CompletedTask;
});
Task IHandleAfterRender.OnAfterRenderAsync()
=> InvokeWithBlazorServiceContext(() =>
{
var firstRender = !hasCalledOnAfterRender;
hasCalledOnAfterRender |= true;
OnAfterRender(firstRender);
return OnAfterRenderAsync(firstRender);
});
private async Task CallStateHasChangedOnAsyncCompletion(Task task)
{
try
{
await task;
}
catch
{
if (task.IsCanceled)
{
return;
}
throw;
}
StateHasChanged();
}
private async Task InvokeWithBlazorServiceContext(Func<Task> func)
{
try
{
BlazorServiceAccessor.Services = Services;
await func();
}
finally
{
BlazorServiceAccessor.Services = null;
}
}
}
Any components extending CustomComponentBase
automatically have BlazorServiceAccessor.Services
set to the xref:System.IServiceProvider in the current Blazor DI scope.
:::moniker-end
:::moniker range=">= aspnetcore-6.0 < aspnetcore-8.0"
Finally, in the Program
file, add the BlazorServiceAccessor
as a scoped service:
builder.Services.AddScoped<BlazorServiceAccessor>();
:::moniker-end
:::moniker range="< aspnetcore-6.0"
Finally, in Startup.ConfigureServices
of Startup.cs
, add the BlazorServiceAccessor
as a scoped service:
services.AddScoped<BlazorServiceAccessor>();
:::moniker-end
:::moniker range=">= aspnetcore-8.0"
- Service injection via a top-level imports file (
_Imports.razor
) in Blazor Web Apps - xref:fundamentals/dependency-injection
IDisposable
guidance for Transient and shared instances- xref:mvc/views/dependency-injection
:::moniker-end
:::moniker range="< aspnetcore-8.0"
- xref:fundamentals/dependency-injection
IDisposable
guidance for Transient and shared instances- xref:mvc/views/dependency-injection
:::moniker-end