Skip to content

[API Proposal] Ability to monitor circuit activity #47325

@MackinnonBuck

Description

@MackinnonBuck

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:

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-approvedAPI was approved in API review, it can be implementedarea-blazorIncludes: Blazor, Razor Components

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions