Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Monitor Blazor Server circuit activity #28983

Merged
merged 5 commits into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 80 additions & 4 deletions aspnetcore/blazor/fundamentals/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,70 @@ Navigate to the `TransientExample` component at `/transient-example` and an <xre

## Access Blazor services from a different DI scope

*This section only applies to Blazor Server apps.**
*This section only applies to Blazor Server apps.*

:::moniker range=">= aspnetcore-8.0"

[Circuit activity handlers](xref:blazor/fundamentals/signalr#monitor-circuit-activity-blazor-server) 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 8.0, 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:

```csharp
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>.

Expand Down Expand Up @@ -804,15 +867,28 @@ public class CustomComponentBase : ComponentBase, IHandleEvent, IHandleAfterRend

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 `Program.cs`, add the `BlazorServiceAccessor` as a scoped service:

```csharp
var builder = WebApplication.CreateBuilder(args);
// ...
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:

```csharp
services.AddScoped<BlazorServiceAccessor>();
```

:::moniker-end

## Additional resources

* <xref:fundamentals/dependency-injection>
Expand Down
82 changes: 82 additions & 0 deletions aspnetcore/blazor/fundamentals/signalr.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,88 @@ There was a problem with the connection! (Current reconnect attempt: 3 / 8)

By default, Blazor Server apps prerender the UI on the server before the client connection to the server is established. For more information, see <xref:mvc/views/tag-helpers/builtin-th/component-tag-helper>.

:::moniker-end

:::moniker range=">= aspnetcore-8.0"

## Monitor circuit activity (Blazor Server)

Monitor inbound circuit activity in Blazor Server apps using the `CreateInboundActivityHandler` method on <xref:Microsoft.AspNetCore.Components.Server.Circuits.CircuitHandler>. Inbound circuit activity is any activity sent from the browser to the server, such as UI events or JavaScript-to-.NET interop calls.

For example, you can use a circuit activity handler to detect if the client is idle:

```csharp
public sealed class IdleCircuitHandler : CircuitHandler, IDisposable
{
readonly Timer timer;
readonly ILogger logger;

public IdleCircuitHandler(IOptions<IdleCircuitOptions> options,
ILogger<IdleCircuitHandler> logger)
{
timer = new Timer();
timer.Interval = options.Value.IdleTimeout.TotalMilliseconds;
timer.AutoReset = false;
timer.Elapsed += CircuitIdle;
this.logger = logger;
}

private void CircuitIdle(object? sender, System.Timers.ElapsedEventArgs e)
{
logger.LogInformation("{Circuit} is idle", nameof(CircuitIdle));
}

public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
Func<CircuitInboundActivityContext, Task> next)
{
return context =>
{
timer.Stop();
timer.Start();
return next(context);
};
}

public void Dispose()
{
timer.Dispose();
}
}

public class IdleCircuitOptions
{
public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromMinutes(5);
}

public static class IdleCircuitHandlerServiceCollectionExtensions
{
public static IServiceCollection AddIdleCircuitHandler(
this IServiceCollection services,
Action<IdleCircuitOptions> configureOptions)
{
services.Configure(configureOptions);
services.AddIdleCircuitHandler();
return services;
}

public static IServiceCollection AddIdleCircuitHandler(
this IServiceCollection services)
{
services.AddScoped<CircuitHandler, IdleCircuitHandler>();
return services;
}
}
```

Circuit activity handlers also provide an approach for accessing scoped Blazor services from other non-Blazor dependency injection (DI) scopes. For more information and examples, see:

* <xref:blazor/fundamentals/dependency-injection#access-blazor-services-from-a-different-di-scope>
* <xref:blazor/security/server/additional-scenarios#access-authenticationstateprovider-in-outgoing-request-middleware>

:::moniker-end

:::moniker range=">= aspnetcore-7.0"

## Blazor startup

Configure the manual start of a Blazor app's SignalR circuit in the `Pages/_Host.cshtml` file (Blazor Server) or `wwwroot/index.html` (hosted Blazor WebAssembly with SignalR implemented):
Expand Down
62 changes: 62 additions & 0 deletions aspnetcore/blazor/security/server/additional-scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -697,3 +697,65 @@ Immediately before the call to `app.MapBlazorHub()` in `Startup.Configure` of `S
```csharp
app.UseMiddleware<UserServiceMiddleware>();
```

:::moniker range=">= aspnetcore-8.0"

## Access `AuthenticationStateProvider` in outgoing request middleware

The <xref:Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider> from a <xref:System.Net.Http.DelegatingHandler> for <xref:System.Net.Http.HttpClient> created with <xref:System.Net.Http.IHttpClientFactory> can be accessed in outgoing request middleware using a [circuit activity handler](xref:blazor/fundamentals/signalr#monitor-circuit-activity-blazor-server).

> [!NOTE]
> For general guidance on defining delegating handlers for HTTP requests by <xref:System.Net.Http.HttpClient> instances created using <xref:System.Net.Http.IHttpClientFactory> in ASP.NET Core apps, see the following sections of <xref:fundamentals/http-requests>:
>
> * [Outgoing request middleware](xref:fundamentals/http-requests#outgoing-request-middleware)
> * [Use DI in outgoing request middleware](xref:fundamentals/http-requests#use-di-in-outgoing-request-middleware)

The following example uses <xref:Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider> to attach a custom user name header for authenticated users to outgoing requests.

First, implement the `CircuitServicesAccessor` class in the following section of the Blazor dependency injection (DI) article:

[Access Blazor services from a different DI scope](xref:blazor/fundamentals/dependency-injection#access-blazor-services-from-a-different-di-scope)

Use the `CircuitServicesAccessor` to access the <xref:Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider> in the <xref:System.Net.Http.DelegatingHandler> implementation.

`AuthenticationStateHandler.cs`:

```csharp
public class AuthenticationStateHandler : DelegatingHandler
{
readonly CircuitServicesAccessor circuitServicesAccessor;

public AuthenticationStateHandler(
CircuitServicesAccessor circuitServicesAccessor)
{
this.circuitServicesAccessor = circuitServicesAccessor;
}

protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var authStateProvider = circuitServicesAccessor.Services
.GetRequiredService<AuthenticationStateProvider>();
var authState = await authStateProvider.GetAuthenticationStateAsync();
var user = authState.User;

if (user.Identity is not null && user.Identity.IsAuthenticated)
{
request.Headers.Add("X-USER-IDENTITY-NAME", user.Identity.Name);
}

return await base.SendAsync(request, cancellationToken);
}
}
```

In `Program.cs`, register the `AuthenticationStateHandler` and add the handler to the <xref:System.Net.Http.IHttpClientFactory> that creates <xref:System.Net.Http.HttpClient> instances:

```csharp
builder.Services.AddTransient<AuthenticationStateHandler>();

builder.Services.AddHttpClient("HttpMessageHandler")
.AddHttpMessageHandler<AuthenticationStateHandler>();
```

:::moniker-end