-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Description
Background and Motivation
Note: This was implemented in #46968
In Blazor Server apps, it's sometimes desirable to access Blazor services from a non-Blazor DI scope. Examples:
- Exception 'GetAuthenticationStateAsync was called before SetAuthenticationState.' thrown when calling AuthenticationStateProvider.GetAuthenticationStateAsync() method #28684
- Document how to configure HttpClient base address in Blazor Server using IHttpClientFactory #25758
- Blazor server DelegatingHandler for http client causing JavaScript error with prerendering disabled #40336
We currently document a solution here. It involves wrapping each asynchronous entry point into Blazor component code and capturing the Blazor context's IServiceProvider
in an AsyncLocal
. Blazor services can then be referenced via the async local if the async call stack originates from Blazor code.
However, this approach has its limitations:
- It requires copying and pasting parts of Blazor's
ComponentBase
implementation - It only works for components that extend a custom
ComponentBase
derivation implementing the wrapper methods
Separately, another common ask is to detect circuit idleness in support of custom circuit eviction logic.
Both these problems can be solved by allowing customers to inject a sort of "middleware" that runs for all incoming circuit activity that causes application code to run.
Proposed API
namespace Microsoft.AspNetCore.Server.Circuits;
+public sealed class CircuitInboundActivityContext
+{
+ public Circuit Circuit { get; }
+}
+public interface IHandleCircuitActivity
+{
+ Task HandleInboundActivityAsync(CircuitInboundActivityContext context, Func<CircuitInboundActivityContext, Task> next);
+}
Usage Examples
To utilize the feature, create a class that extends CircuitHandler
(required) and implements IHandleCircuitActivity
. Then, add the class as a service to the DI container.
Here's an example showing how to make Blazor services accessible to non-Blazor scopes:
BlazorServiceCircuitHandler.cs
public class BlazorServiceCircuitHandler : CircuitHandler, IHandleCircuitActivity
{
private readonly IServiceProvider _services;
private readonly BlazorServiceAccessor _bazorServiceAccessor;
public BlazorServiceCircuitHandler(IServiceProvider services, BlazorServiceAccessor blazorServiceAccessor)
{
_services = services;
_bazorServiceAccessor = blazorServiceAccessor;
}
public async Task HandleInboundActivityAsync(CircuitInboundActivityContext context, Func<CircuitInboundActivityContext, Task> next)
{
_bazorServiceAccessor.Services = _services;
await next(context);
_bazorServiceAccessor.Services = null;
}
}
BlazorServiceAccessor.cs
public class BlazorServiceAccessor
{
private static readonly AsyncLocal<IServiceProvider> s_currentServices = new();
public IServiceProvider? Services
{
get => s_currentServices.Value;
set => s_currentServices.Value = value;
}
}
Program.cs
// ...
services.AddScoped<CircuitHandler, BlazorServiceCircuitHandler>();
services.AddScoped<BlazorServiceAccessor>();
// ...
Alternative Designs
We considered adding a new method to the CircuitHandler
class, but that introduces unnecessary overhead when the HandleInboundActivityAsync()
is not overridden.
We also thought about including information about what "type" of event is being handled, but we decided against it because it ties our internal implementation to the public API, restricting us from making drastic changes to the area.
Risks
No known risks. This feature is additive and shouldn't change the behavior of existing apps.