6

I'm refactoring my code to use Refit for my calls to the WebApi service. The interface is set up and I also created a delegating handler:

public class AuthHandler : DelegatingHandler
{
    private readonly TokenProvider tokenProvider;
    private readonly ISessionStorageService sessionStorage;

    public AuthHandler (
        TokenProvider tokenProvider, 
        ISessionStorageService sessionStorage)
    {
        this.tokenProvider = tokenProvider ?? throw new ArgumentNullException(nameof(tokenProvider));
        this.sessionStorage = sessionStorage ?? throw new ArgumentNullException(nameof(sessionStorage));
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
    {
        var someToken = await sessionStorage.GetItemAsync<string>("sometoken");

        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenProvider.AccessToken);
        request.Headers.Add("someToken", someToken);

        return await base.SendAsync(request, ct).ConfigureAwait(false);
    }
}

And in Startup.cs:

services.AddBlazoredSessionStorage();
services.AddScoped<TokenProvider>();
services.AddScoped<AuthHandler>();

services.AddRefitClient<IApiService>().ConfigureHttpClient(options =>
{
    options.BaseAddress = new Uri(Configuration["Server:Url"]);
    options.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}).AddHttpMessageHandler<AuthHandler>();

I have a razor component and I want to use the service above so I injected the services and did:

@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            var list = await myService.GetListAsync();
        }
    } 
}

Also, in _Host.cshtml:

<environment include="Staging,Production">
    <component render-mode="ServerPrerendered" type="typeof(App)" param-InitialState="tokens" />
</environment>
<environment include="Development">
    <component render-mode="Server" type="typeof(App)" param-InitialState="tokens"  />
</environment>

However I get the following exception:

System.InvalidOperationException: JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendered. When prerendering is enabled, JavaScript interop calls can only be performed during the OnAfterRenderAsync lifecycle method.

So, just to make sure if I have values or not I added the following before the call to the api:

var someToken = await sessionStorage.GetItemAsync<string>("sometoken");
var accessToken = tokenProvider.AccessToken;

And I DO have values in both variables.

So why can't I access the session storage from the delegating handler? And why is token provider instance instantiated but the properties all null (also in the handler)?

EDIT

I only need one place to keep my tokens. It doesn't matter if it's the token provider or the session storage, as long as it works in blazor pages/componenents and other services.

UPDATE 1

One can skip DI and create the service like this:

var service = RestService.For<IMyService>(new HttpClient(new AuthHandler(tokenProvider, sessionStorage))
   {
        BaseAddress = new Uri(myUrl)
   }
);

This will work as expected. However, it would be much more better to use DI. The problem might either be in Blazor or in Refit.

Ivan-Mark Debono
  • 15,500
  • 29
  • 132
  • 263
  • From "This is because the component is being statically rendered" I assume that you are using your AuthHandler too early thus you are trying to access sessionStorage when the app is being preloaded (server-side). If it's true, it just makes so much more sense to wait with any http requests till the app is loaded. – JanB Feb 13 '21 at 17:07
  • The exception says to place the code in OnAfterRenderAsync, which I'm doing. – Ivan-Mark Debono Feb 13 '21 at 17:58
  • So the only place you use IApiService is this GetListAsync call? – JanB Feb 13 '21 at 18:22
  • At thst stage yes. – Ivan-Mark Debono Feb 13 '21 at 18:43
  • Is this a duplicate of [this](https://stackoverflow.com/q/57299993)? Note the comment to the answer "My mistake was inject `IJSRuntime` one time in the MainLayout and stored it in a static variable to use it everywhere. Now when the `IJSRuntime` has been injected in each component, the problem solved." – JHBonarius Feb 14 '21 at 10:29
  • P.s. should your `OnAfterRenderAsync` call `base.OnAfterRenderAsync();`? And are you use the exception is being thrown in `OnAfterRenderAsync`? – JHBonarius Feb 14 '21 at 10:33
  • All snippets I found didnt call base. And yes, the exception is thrown there. I checked the call stack. Check my update. It works without DI – Ivan-Mark Debono Feb 14 '21 at 11:22
  • @Ivan-MarkDebono Interesting. I've been racking my brain for days with the exact same problem. Have you found a working solution? Also for me Refit only works if I do without Dependency Injection (like in your Update 1). – Saber Mar 27 '21 at 12:41
  • Having the same problem, also using a HttpClientFactory. I don't use refit. I suspect a Blazor bug. It makes absolutely no sense. Been, and still am, breaking my head on this as well. – Napoleon Jul 12 '21 at 15:44

1 Answers1

-1

I think I have found a working solution to the problem.

Instead of relying on the HttpClientFactory used by Refit, I created my own DI logic.

Previous:

Uri apiBaseUri = dataAccessLayerConfiguration.API.BaseUrl;
foreach (Type repositoryType in repositoryTypes)
{
    services
        .AddRefitClient(repositoryType)
        .ConfigureHttpClient((httpClient) => httpClient.BaseAddress = apiBaseUri)
        .AddHttpMessageHandler<RequestHeaderHandler>();
}

Current:

foreach (Type repositoryType in repositoryTypes)
{
    services.AddTransient(
        repositoryType,
        (provider) =>
        {
            return
                RepositoryFactory.Create(
                    repositoryType,
                    configuration,
                    provider.GetRequiredService<ITokenCacheProvider>(),
                    () => provider.GetRequiredService<ICurrentUser>());
        });
}

The RepositoryFactory I use creates the HttpClients for each API interface:

return
    RestService.For<TRepository>(
        new HttpClient(
            new RequestHeaderHandler(tokenCacheProvider, fetchCurrentUserDelegate)
            {
                InnerHandler = new HttpClientHandler()
            },
            true)
        {
            BaseAddress = configuration.DataAccessLayer.API.BaseUrl
        });

I currently get the current user in the code of my layout in the OnParametersSet() method. I'm not completely satisfied with it yet, but for now it's enough for me. However, it is important not to inject the user object directly when creating HttpClient, but only a delegate (or a Func<>) which then resolves the user object if needed.

This way I was able to work around the scope issue of Refit/HttpClientFactory, but still continue to work with Dependency Injection. It may not be a one hundred percent solution for everyone, but it could be enough to possibly find the right direction for your own project.

Saber
  • 501
  • 4
  • 8