Search code examples
c#dependency-injection.net-5blazor-server-side

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


I'm attempting to set up our Blazor Server application to use a custom delegating handler that will attach a bearer token to all outgoing requests to our APIs. The delegating handler uses a token service that handles the retrieval of the token as well as the process for refreshing it if it's expired. The code looks like this:

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);
    }
}

I've registered the token service as a scoped service in Startup.cs, with the understanding that the same instance of the service will stay alive in the DI container for the lifetime of the request. If I'm understanding that correctly, this means that I can assign the token values to the service in my App.razor page thusly and pick them up with any subsequent call to the token service:

@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;
  }

This seems to work just fine for everything except the delegating handler - the values turn up just fine in a scoped configuration for any other request made to the token service, but when injecting the token service into the delegating handler the token value always comes up null.

Access token in authorization handler comes up null

Unsurprisingly, if I register the service as a singleton, the values propagate to the delegating handler just fine, but that's obviously not a solution. For some reason the DI scope for the handler appears to be different from that of the rest of the application, and I have no idea why. Any ideas? Appreciate the help in advance.

EDIT

With some help from Simply Ged and Andrew Lock, I was able to get quite a bit closer using the IHttpContextAccessor to grab the instance of the ITokenService associated with the request. I was ecstatic when I first logged in and saw it working, but once my excitement wore down I noticed it would stop working correctly a short time later. It turns out that this method only synchronizes the instances on the initial request - after that time, the DI's awkward pipeline management kicks in and maintains the same service instance for every subsequent request, and they all fall out of alignment again. Here's an example of the debug logs from my testing:

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 ┘

Andrew Lock's 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.


Solution

  • After several days of troubleshooting, I'm forced to concede that the delegating handler simply isn't a workable solution for this. Because the dependency injection manages the http request pipeline scopes completely separately from the http clients themselves, there's no reliable way to ensure that the pipeline has the same scope as the request, and even breaking protocol and using the IHttpContextAccessor doesn't solve the problem.

    As a result I've ditched the delegating handler entirely and instead implemented a base class for all my services to inherit that handles the token acquisition in the constructor. That way I can inject the token service directly into the constructor and ensure that I'm getting the right scope.

    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;
            }
        }
    }
    

    It's not as spiffy as using the DI pipeline would be, but it's at least as clean and effective and so far it's working very well. As a wise man once (sort of) said, "There are alternatives to fighting [with dependency injection]."