Skip to content

Commit fc0858c

Browse files
authored
Add application scope handler approach (#36579)
1 parent 36a7ce0 commit fc0858c

7 files changed

Lines changed: 179 additions & 10 deletions

File tree

aspnetcore/blazor/call-web-api.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,18 @@ The `BlazorWebAppCallWebApi` [sample app](#sample-apps) demonstrates calling a w
464464

465465
:::moniker-end
466466

467+
:::moniker range=">= aspnetcore-8.0"
468+
469+
## Accessing services outside of the `HttpClient`'s scope
470+
471+
<xref:System.Net.Http.IHttpClientFactory> creates <xref:System.Net.Http.DelegatingHandler> instances in a separate dependency injection (DI) scope from the app. If you inject a scoped service into a derived <xref:System.Net.Http.DelegatingHandler> type, the handler doesn't have access to the service from the Blazor circuit.
472+
473+
For an example of how to access a service in outgoing request middleware using either an application scope handler or a circuit activity handler, see <xref:blazor/security/additional-scenarios#access-authenticationstateprovider-in-outgoing-request-middleware>.
474+
475+
For more information on <xref:System.Net.Http.DelegatingHandler> instances, see <xref:fundamentals/http-requests#outgoing-request-middleware>.
476+
477+
:::moniker-end
478+
467479
## Disposal of `HttpRequestMessage`, `HttpResponseMessage`, and `HttpClient`
468480

469481
An <xref:System.Net.Http.HttpRequestMessage> without a body doesn't require explicit disposal. However, you can dispose of it with either of the following patterns:

aspnetcore/blazor/fundamentals/dependency-injection.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ For more information, see <xref:blazor/blazor-ef-core>.
586586

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

589-
[Circuit activity handlers](xref:blazor/fundamentals/signalr#monitor-server-side-circuit-activity) 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>.
589+
[Circuit activity handlers](xref:blazor/fundamentals/signalr#monitor-server-side-circuit-activity) provide an approach for accessing scoped Blazor services from other non-Blazor dependency injection (DI) scopes.
590590

591591
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:
592592

@@ -637,7 +637,7 @@ builder.Services.AddCircuitServicesAccessor();
637637

638638
Access the circuit-scoped services by injecting the `CircuitServicesAccessor` where it's needed.
639639

640-
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/additional-scenarios#access-authenticationstateprovider-in-outgoing-request-middleware>.
640+
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 the circuit activity handler approach in <xref:blazor/security/additional-scenarios#access-authenticationstateprovider-in-outgoing-request-middleware>.
641641

642642
:::moniker-end
643643

aspnetcore/blazor/security/additional-scenarios.md

Lines changed: 158 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ This article explains how to configure server-side Blazor for additional securit
2525

2626
If you merely want to use access tokens to make web API calls from a Blazor Web App with a [named HTTP client](xref:blazor/call-web-api#named-httpclient-with-ihttpclientfactory), see the [Use a token handler for web API calls](#use-a-token-handler-for-web-api-calls) section, which explains how to use a <xref:System.Net.Http.DelegatingHandler> implementation to attach a user's access token to outgoing requests. The following guidance in this section is for developers who need access tokens, refresh tokens, and other authentication properties server-side for other purposes.
2727

28+
> [!NOTE]
29+
> For more information on <xref:System.Net.Http.DelegatingHandler> instances, see <xref:fundamentals/http-requests#outgoing-request-middleware>.
30+
2831
To save tokens and other authentication properties for server-side use in Blazor Web Apps, we recommend using [`IHttpContextAccessor`/`HttpContext`](xref:blazor/components/httpcontext) (<xref:Microsoft.AspNetCore.Http.IHttpContextAccessor>, <xref:Microsoft.AspNetCore.Http.HttpContext>). Reading tokens from <xref:Microsoft.AspNetCore.Http.HttpContext>, including as a [cascading parameter](xref:Microsoft.AspNetCore.Components.CascadingParameterAttribute), using <xref:Microsoft.AspNetCore.Http.IHttpContextAccessor> is supported for obtaining tokens for use during interactive server rendering if the tokens are obtained during static server-side rendering (static SSR) or prerendering. However, tokens aren't updated if the user authenticates after the circuit is established, since the <xref:Microsoft.AspNetCore.Http.HttpContext> is captured at the start of the SignalR connection. Also, the use of <xref:System.Threading.AsyncLocal%601> by <xref:Microsoft.AspNetCore.Http.IHttpContextAccessor> means that you must be careful not to lose the execution context before reading the <xref:Microsoft.AspNetCore.Http.HttpContext>. For more information, see <xref:blazor/components/httpcontext>.
2932

3033
In a service class, obtain access to the members of the namespace <xref:Microsoft.AspNetCore.Authentication?displayProperty=fullName> to surface the <xref:Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.GetTokenAsync%2A> method on <xref:Microsoft.AspNetCore.Http.HttpContext>. An alternative approach, which is commented out in the following example, is to call <xref:Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.AuthenticateAsync%2A> on <xref:Microsoft.AspNetCore.Http.HttpContext>. For the returned <xref:Microsoft.AspNetCore.Authentication.AuthenticateResult.Properties%2A?displayProperty=nameWithType>, call <xref:Microsoft.AspNetCore.Authentication.AuthenticationTokenExtensions.GetTokenValue%2A>.
@@ -848,15 +851,167 @@ app.UseMiddleware<UserServiceMiddleware>();
848851

849852
## Access `AuthenticationStateProvider` in outgoing request middleware
850853

851-
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).
854+
<xref:System.Net.Http.IHttpClientFactory> creates <xref:System.Net.Http.DelegatingHandler> instances in a separate dependency injection (DI) scope from the app. If you inject <xref:Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider> into a derived <xref:System.Net.Http.DelegatingHandler> type, the handler doesn't have access to the current user's authentication state from the Blazor circuit.
855+
856+
Use either of the following approaches to address this scenario:
857+
858+
* [Application scope handler](#application-scope-handler-recommended) (*Recommended*)
859+
* [Circuit activity handler](#circuit-activity-handler)
852860

853861
> [!NOTE]
854-
> 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>:
862+
> For general guidance on defining delegating handlers for HTTP requests by <xref:System.Net.Http.HttpClient> instances created using <xref:System.Net.Http.IHttpClientFactory>, see the following sections of <xref:fundamentals/http-requests>:
855863
>
856864
> * [Outgoing request middleware](xref:fundamentals/http-requests#outgoing-request-middleware)
857865
> * [Use DI in outgoing request middleware](xref:fundamentals/http-requests#use-di-in-outgoing-request-middleware)
858866

859-
The following example uses <xref:Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider> to attach a custom user name header for authenticated users to outgoing requests.
867+
The examples in the following subsections attach a custom user name header for authenticated users to outgoing requests.
868+
869+
### Application scope handler (*Recommended*)
870+
871+
The approach in this section uses a [keyed service](xref:fundamentals/dependency-injection#keyed-services) to register a custom <xref:System.Net.Http.HttpClient> that wraps the base client with an application scope handler resolved from the current application scope to access <xref:Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider>.
872+
873+
Overview of the approach:
874+
875+
* Base client configuration: <xref:Microsoft.Extensions.DependencyInjection.HttpClientFactoryServiceCollectionExtensions.AddHttpClient%2A> is called to register a [named client](xref:blazor/call-web-api#named-httpclient-with-ihttpclientfactory) with <xref:System.Net.Http.IHttpClientFactory>.
876+
* Keyed registration: A custom `AddApplicationScopeHandler` extension method registers a keyed <xref:System.Net.Http.HttpClient> with the same client name.
877+
* Scope-aware handler: The application scope handler is resolved from the current scope, giving it access to <xref:Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider>.
878+
* Handler caching: The application scope handler uses <xref:System.Net.Http.IHttpMessageHandlerFactory> to get the cached <xref:System.Net.Http.HttpMessageHandler>, which preserves connection pooling.
879+
* Configuration reuse: The application scope handler applies the same <xref:Microsoft.Extensions.Http.HttpClientFactoryOptions> configuration to its <xref:System.Net.Http.HttpClient> as the base client.
880+
881+
Create the following methods and classes:
882+
883+
* `AddApplicationScopeHandler`: An extension method to add the application scope handler and a keyed <xref:System.Net.Http.HttpClient> service to the DI container.
884+
* `ApplicationScopeHandler`: The application scope handler class.
885+
* `AuthenticationStateHandler`: A <xref:System.Net.Http.DelegatingHandler> that attaches a custom user name header for authenticated users to outgoing requests.
886+
887+
`Services/ApplicationScopeHttpClientExtensions.cs`:
888+
889+
```csharp
890+
using System.Security.Claims;
891+
using Microsoft.AspNetCore.Components.Authorization;
892+
using Microsoft.Extensions.Http;
893+
using Microsoft.Extensions.Options;
894+
895+
namespace BlazorSample.Services;
896+
897+
public static class ApplicationScopeHttpClientExtensions
898+
{
899+
public static readonly HttpRequestOptionsKey<IServiceProvider> ScopeKey =
900+
new("ApplicationScope");
901+
902+
public static IHttpClientBuilder AddApplicationScopeHandler(
903+
this IHttpClientBuilder builder)
904+
{
905+
var name = builder.Name;
906+
907+
builder.Services.AddTransient<ApplicationScopeHandler>();
908+
909+
builder.Services.AddKeyedScoped<HttpClient>(name, (sp, key) =>
910+
{
911+
var handler = sp.GetRequiredService<ApplicationScopeHandler>();
912+
handler.InnerHandler =
913+
sp.GetRequiredService<IHttpMessageHandlerFactory>()
914+
.CreateHandler(name);
915+
916+
var client = new HttpClient(handler, disposeHandler: false);
917+
918+
var options =
919+
sp.GetRequiredService<IOptionsMonitor<HttpClientFactoryOptions>>()
920+
.Get(name);
921+
922+
foreach (var action in options.HttpClientActions)
923+
{
924+
action(client);
925+
}
926+
927+
return client;
928+
});
929+
930+
return builder;
931+
}
932+
}
933+
934+
public class ApplicationScopeHandler(IServiceProvider serviceProvider)
935+
: DelegatingHandler
936+
{
937+
protected override Task<HttpResponseMessage> SendAsync(
938+
HttpRequestMessage request,
939+
CancellationToken cancellationToken)
940+
{
941+
request.Options.Set(ApplicationScopeHttpClientExtensions.ScopeKey,
942+
serviceProvider);
943+
return base.SendAsync(request, cancellationToken);
944+
}
945+
}
946+
947+
public class AuthenticationStateHandler : DelegatingHandler
948+
{
949+
private ClaimsPrincipal? user;
950+
951+
protected override async Task<HttpResponseMessage> SendAsync(
952+
HttpRequestMessage request,
953+
CancellationToken cancellationToken)
954+
{
955+
if (user is null)
956+
{
957+
if (request.Options.TryGetValue(
958+
ApplicationScopeHttpClientExtensions.ScopeKey, out var sp))
959+
{
960+
var authStateProvider = sp.GetService<AuthenticationStateProvider>();
961+
962+
if (authStateProvider is not null)
963+
{
964+
user = (await authStateProvider.GetAuthenticationStateAsync())
965+
.User;
966+
}
967+
}
968+
}
969+
970+
if (user?.Identity?.IsAuthenticated)
971+
{
972+
request.Headers.TryAddWithoutValidation("X-USER-IDENTITY-NAME",
973+
user.Identity.Name);
974+
}
975+
976+
return await base.SendAsync(request, cancellationToken);
977+
}
978+
}
979+
```
980+
981+
The `AuthenticationStateHandler` in the preceding example caches the user for the lifetime of the <xref:System.Net.Http.DelegatingHandler>. To fetch the user's current authentication state for each request, remove the `null` conditional check on the user.
982+
983+
Register the named client in the `Program` file, calling `AddApplicationScopeHandler` to add the application scope handler:
984+
985+
```csharp
986+
builder.Services.AddHttpClient("ExternalApi", client =>
987+
{
988+
client.BaseAddress = new Uri("{REQUEST URI}");
989+
})
990+
.AddApplicationScopeHandler()
991+
.AddHttpMessageHandler<AuthenticationStateHandler>();
992+
```
993+
994+
The `{REQUEST URI}` placeholder in the preceding example is the request URI (localhost example: `http://localhost:5209`).
995+
996+
Inject the client into components using the keyed service:
997+
998+
```razor
999+
@using Microsoft.Extensions.DependencyInjection
1000+
1001+
@code {
1002+
[Inject(Key = "ExternalApi")]
1003+
public HttpClient Http { get; set; } = default!;
1004+
1005+
private async Task CallApiAsync()
1006+
{
1007+
var response = await Http.GetAsync("/api/endpoint");
1008+
}
1009+
}
1010+
```
1011+
1012+
### Circuit activity handler
1013+
1014+
The approach in this section uses a [circuit activity handler](xref:blazor/fundamentals/signalr#monitor-circuit-activity-blazor-server) to access the <xref:Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider>, which is an alternative to the recommended [application scope handler approach](#application-scope-handler-recommended) in the preceding section.
8601015

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

aspnetcore/blazor/security/blazor-web-app-with-oidc.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ Sample solution features:
416416

417417
* The app securely calls a web API for weather data:
418418

419-
* When rendering the `Weather` component on the server, the component uses the `ServerWeatherForecaster` on the server to obtain weather data from the web API in the `MinimalApiJwt` project using a <xref:System.Net.Http.DelegatingHandler> (`TokenHandler`) that attaches the access token from the <xref:Microsoft.AspNetCore.Http.HttpContext> to the request.
419+
* When rendering the `Weather` component on the server, the component uses the `ServerWeatherForecaster` on the server to obtain weather data from the web API in the `MinimalApiJwt` project using a <xref:System.Net.Http.DelegatingHandler> (`TokenHandler`) that attaches the access token from the <xref:Microsoft.AspNetCore.Http.HttpContext> to the request. For more information on <xref:System.Net.Http.DelegatingHandler> instances, see <xref:fundamentals/http-requests#outgoing-request-middleware>.
420420
* When the component is rendered on the client, the component uses the `ClientWeatherForecaster` service implementation, which uses a preconfigured <xref:System.Net.Http.HttpClient> (in the client project's `Program` file) to make the web API call from the server project's `ServerWeatherForecaster`.
421421

422422
:::moniker range=">= aspnetcore-9.0"

aspnetcore/blazor/security/webassembly/additional-scenarios.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ For convenience, the framework provides the <xref:Microsoft.AspNetCore.Component
2626
In the following example:
2727

2828
* <xref:Microsoft.Extensions.DependencyInjection.HttpClientFactoryServiceCollectionExtensions.AddHttpClient%2A> adds <xref:System.Net.Http.IHttpClientFactory> and related services to the service collection and configures a named <xref:System.Net.Http.HttpClient> (`WebAPI`). <xref:System.Net.Http.HttpClient.BaseAddress?displayProperty=nameWithType> is the base address of the resource URI when sending requests. <xref:System.Net.Http.IHttpClientFactory> is provided by the [`Microsoft.Extensions.Http`](https://www.nuget.org/packages/Microsoft.Extensions.Http) NuGet package.
29-
* <xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.BaseAddressAuthorizationMessageHandler> is the <xref:System.Net.Http.DelegatingHandler> used to process access tokens. Access tokens are only added when the request URI is within the app's base URI.
29+
* <xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.BaseAddressAuthorizationMessageHandler> is the <xref:System.Net.Http.DelegatingHandler> used to process access tokens. Access tokens are only added when the request URI is within the app's base URI. For more information on <xref:System.Net.Http.DelegatingHandler> instances, see <xref:fundamentals/http-requests#outgoing-request-middleware>.
30+
3031
* <xref:System.Net.Http.IHttpClientFactory.CreateClient%2A?displayProperty=nameWithType> creates and configures an <xref:System.Net.Http.HttpClient> instance for outgoing requests using the configuration that corresponds to the named <xref:System.Net.Http.HttpClient> (`WebAPI`).
3132

3233
In the following example, <xref:Microsoft.Extensions.DependencyInjection.HttpClientFactoryServiceCollectionExtensions.AddHttpClient%2A?displayProperty=nameWithType> is an extension in <xref:Microsoft.Extensions.Http?displayProperty=fullName>. Add the package to an app that doesn't already reference it.

aspnetcore/blazor/security/webassembly/graph-api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,7 @@ In the preceding example, the `GraphAuthorizationMessageHandler` <xref:System.Ne
938938

939939
* [Utility base component classes to manage a DI scope](xref:blazor/fundamentals/dependency-injection#utility-base-component-classes-to-manage-a-di-scope)
940940
* [Detect client-side transient disposables](xref:blazor/fundamentals/dependency-injection#detect-client-side-transient-disposables)
941+
* [`DelegatingHandler` instances](xref:fundamentals/http-requests#outgoing-request-middleware)
941942

942943
A trailing slash (`/`) on the base address is required. In the preceding code, the third argument to `string.Join` is `string.Empty` to ensure the trailing slash is present: `https://graph.microsoft.com/v1.0/`.
943944

aspnetcore/fundamentals/dependency-injection.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,13 +152,13 @@ The term *keyed services* refers to a mechanism for registering and retrieving D
152152

153153
:::code language="csharp" source="~/../AspNetCore.Docs.Samples/samples/KeyedServices9/Program.cs" highlight="6,7,12-14,39,47" id="snippet_1":::
154154

155-
#### Keyed services in Middleware
155+
#### Keyed services in middleware
156156

157-
Middleware supports Keyed services in both the constructor and the `Invoke`/`InvokeAsync` method:
157+
Middleware supports keyed services in both the constructor and the `Invoke`/`InvokeAsync` method:
158158

159159
:::code language="csharp" source="~/../AspNetCore.Docs.Samples/samples/KeyedServices9/Program.cs" id="snippet_2":::
160160

161-
For more information on creating Middleware, see <xref:fundamentals/middleware/write>
161+
For more information on creating middleware, see <xref:fundamentals/middleware/write>.
162162

163163
## Constructor injection behavior
164164

0 commit comments

Comments
 (0)