在 Blazor Server 的自定义委托处理程序中获取作用域服务

Obtaining a scoped service in a custom delegating handler in Blazor Server

我正在尝试将我们的 Blazor Server 应用程序设置为使用自定义委托处理程序,该处理程序会将不记名令牌附加到我们 API 的所有传出请求。委托处理程序使用令牌服务来处理令牌的检索以及令牌过期时的刷新过程。代码如下所示:

public class HttpAuthorizationHandler : DelegatingHandler
{
    private ITokenService _tokenService { get; set; }

    public HttpAuthorizationHandler(ITokenService tokenService)
    {
        _tokenService = tokenService;
    }

    protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await _tokenService.TryGetToken();

        request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", token);

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

我已将令牌服务注册为 Startup.cs 中的作用域服务,并理解该服务的同一实例将在请求的生命周期内在 DI 容器中保持活动状态。如果我理解正确,这意味着我可以将令牌值分配给我的 App.razor 页面中的服务,并在随后调用令牌服务时获取它们:

@code {
  [Parameter]
  public string AccessToken { get; set; }
  [Parameter]
  public string RefreshToken { get; set; }

  [Inject] private ITokenService _tokenService { get; set; }

  protected override void OnInitialized()
  {
    _tokenService.AccessToken = AccessToken.NullIfEmpty() ?? _tokenService.AccessToken;
    _tokenService.RefreshToken = RefreshToken.NullIfEmpty() ?? _tokenService.RefreshToken;
  }

这似乎对一切都很好 除了 委托处理程序 - 对于对令牌服务发出的任何其他请求,值在范围配置中显示得很好,但是当将令牌服务注入委托处理程序,令牌值始终为空。

不出所料,如果我将服务注册为单例,值会很好地传播到委托处理程序,但这显然不是解决方案。出于某种原因,处理程序的 DI 范围似乎与应用程序的其余部分不同,我不知道为什么。有任何想法吗?提前感谢您的帮助。

编辑

在 Simply Ged 和 Andrew Lock 的帮助下,我能够使用 IHttpContextAccessor 获取与请求相关联的 ITokenService 实例。当我第一次登录并看到它工作时,我欣喜若狂,但当我的兴奋消退后,我发现它会在短时间内停止正常工作。事实证明,此方法仅在初始请求时同步实例 - 在那之后,DI 笨拙的管道管理开始运行并为每个后续请求维护相同的服务实例,并且它们再次失去对齐。这是我测试的调试日志示例:

App.razor:         e12f6c80-5cee-44a0-a8a0-d6b783057339 ┐
AuthStateProvider: e12f6c80-5cee-44a0-a8a0-d6b783057339 ├ Initial request - all IDs match
AuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 ┘
AuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 ┐
AuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 │
AuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 │
AuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 ├ Clicking around a bunch
AuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 │
AuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 │
AuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 ┘
App.razor:         cab70e54-1907-462d-8918-dbd771fabe76 ┐
AuthStateProvider: cab70e54-1907-462d-8918-dbd771fabe76 ├ Load a new page - ah, crap...
AuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 ┘
AuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 ┐
AuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 │
App.razor:         3c55ff9c-5511-40a1-9fb8-cd00f9fc11c6 │
AuthStateProvider: 3c55ff9c-5511-40a1-9fb8-cd00f9fc11c6 ├ Dang it all to heck
AuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 │
AuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 │
AuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 ┘

安德鲁·洛克 excellent blog post on this topic goes into some very useful detail on why this kind of thing happens - apparently the dependency injection manages the lifetime of the http request pipelines separately from the lifetimes of the http clients themselves, so you can't rely on the pipeline being a part of the same context as the request. He proposes the use of the IHttpContextAccessor as a solution to this problem, but it does not appear to solve the problem in this particular case. I suspect his blog post refers to an earlier version of ASP.NET Core, the behavior of which is not applicable to Blazor apps. In fact, as Alex pointed out, Microsoft specifically advises against using the IHttpContextAccessor for shared state.

根据评论,Andrew Lock 的 excellent blog post 谈到了这一点。

TL;DR

您需要使用 IHttpContextAccessor 来解析作用域服务并获取您想要的实例。

例子

这个例子的所有功劳都应该归功于 Andrew Lock 和他的博客 post,但是对于这个问题的快速参考读者,你需要将 IHttpContextAccessor 插入你的 DelegatingHandler ] 并使用它来访问正确范围内的服务。

public class ScopedMessageHander: DelegatingHandler
{
    private readonly ILogger<ScopedMessageHander> _logger;
    private readonly IHttpContextAccessor _accessor;

    public ScopedMessageHander(ILogger<ScopedMessageHander> logger, IHttpContextAccessor accessor)
    {
        _logger = logger;
        _accessor = accessor;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // The HttpContext will be null if used outside a request context, so check in practice!
        var httpConext = _accessor.HttpContext;

        // retrieve the service from the Request DI Scope
        var service = _accessor.HttpContext.RequestServices.GetRequiredService<ScopedService>();

        ...
    }
}

经过几天的故障排除,我不得不承认委派处理程序根本不是一个可行的解决方案。因为依赖注入完全独立于 http 客户端本身来管理 http 请求管道范围,所以没有可靠的方法来确保管道具有与请求相同的范围,甚至破坏协议和使用 IHttpContextAccessor 也不能解决问题。

因此,我完全放弃了委托处理程序,而是为我的所有服务实现了一个基础 class 来继承,它在构造函数中处理令牌获取。这样我就可以将令牌服务直接注入构造函数并确保我获得正确的范围。

public class ServiceBase
{
    public readonly HttpClient _HttpClient;

    public ServiceBase(HttpClient httpClient, ITokenService tokenService)
    {
        _HttpClient = httpClient;

        var token = tokenService.TryGetToken();

        if (!string.IsNullOrEmpty(token))
        {
            _HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        }
        else
        {
            _HttpClient.DefaultRequestHeaders.Authorization = null;
        }
    }
}

它不像使用 DI 管道那样漂亮,但它至少一样干净和有效,到目前为止它工作得很好。正如一位智者曾经(有点)所说,“ 对抗 [依赖注入] 的替代方案。”

我在 Blazor Server 中遇到了类似的问题,我想在请求中添加一些 HTTP headers,headers 与发出请求的用户相关; DelegatingHandler 似乎是合适的。但是,我显然 运行 遇到了与 DI 范围相同的问题 - 由于范围不同,构造函数注入无法依赖,并且您不能使用 IHttpContextAccessor 通过 Blazor 访问 IServiceProvider。我通过传递包含 HttpRequestMessage 属性字典中必要细节的 object 解决了这个问题,例如

//where you make the request with HttpClient...
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url)
{
    Content = stringContent //stringContent contains body to post
};

request.Properties["MyProperty"] = userSpecificData;

HttpResponseMessage response = await httpClient.SendAsync(request);

然后在DelegatingHandler中,直接从Properties中取出数据:

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
    UserData userData = request.Properties["MyProperty"] as UserData;

    //do whatever you need etc
    
    return await base.SendAsync(request, cancellationToken);
}

这样您仍然可以通过 services.AddHttpClient(etc).AddHttpMessageHandler<MyHandler>() 在 Startup.cs 中注册您的 DelegatingHandler 并使用 IHttpClientFactory 获取您的 HttpClient,并且仍然将请求特定数据传递给 DelegatingHandler。