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

Document how to access Blazor Services from another DI service scope. #27908

Closed
MackinnonBuck opened this issue Dec 14, 2022 · 2 comments · Fixed by #27909
Closed

Document how to access Blazor Services from another DI service scope. #27908

MackinnonBuck opened this issue Dec 14, 2022 · 2 comments · Fixed by #27909
Assignees
Labels
Blazor doc-enhancement Pri1 High priority, do before Pri2 and Pri3
Projects

Comments

@MackinnonBuck
Copy link
Member

This issue applies to Blazor Server specifically.

We've received some issues like dotnet/aspnetcore#25758 and dotnet/aspnetcore#40336 where customers try to get Blazor services from an IServiceProvider created from a different DI scope. In both the previous linked issues, an IHttpClientFactory was used to create an HttpClient configured to utilize a Blazor service in some way. This fails because these HttpClients will have their own DI service scopes, so they can't access Blazor services directly.

We should document how customers can access Blazor Server services from code where the Blazor IServiceProvider is not already accessible, specifically when that code is invoked from a Blazor component.

One approach to make this work is by using an AsyncLocal to capture the Blazor component's IServiceProvider so it can be retrieved later in the async call stack.

In the future, we hope to add a product feature that addresses this issue more fundamentally, making the code provided by this example unnecessary.

@MackinnonBuck
Copy link
Member Author

Here's some content for this issue:


(Blazor Server) Access Blazor Services from Another DI Scope

There may be times when a Blazor components invoke asynchronous methods that execute code in a different DI scope. Without some work, these DI scopes won't have access to Blazor's services, like IJSInterop and ProtectedBrowserStorage.

For example, HttpClient instances created using IHttpClientFactory have their own DI service scope. As a result, HttpMessageHandler instances configured on the HttpClient won't be able to directly inject Blazor services.

Until this is addressed as a product feature at a future time, there is a workaround to configure Blazor services to be accessible from code in other DI scopes when invoked from a Blazor component.

First, create a static class BlazorServiceAccessor:

internal static class BlazorServiceAccessor
{
    private static readonly AsyncLocal<IServiceProvider> s_blazorServices = new();

    public static IServiceProvider Services
    {
        get => s_blazorServices.Value ?? throw new InvalidOperationException(
            "Blazor services are not available in the current context.");
        set => s_blazorServices.Value = value;
    }
}

This class defines an AsyncLocal<IServiceProvider>, which will store the Blazor IServiceProvider for the current asynchronous context. Asynchronous code invoked from a Blazor component can use BlazorServiceAccessor.Services to access Blazor services. Note again that this is only necessary if the IServiceProvider available to the code being invoked was created in a different DI service scope.

Next, we need a way to set the value of BlazorServiceAccessor.Services automatically when an async component method gets invoked. We can do this by creating a custom base component that reimplements the three primary asynchronous entry points into Blazor component code:

  • IComponent.SetParametersAsync
  • IHandleEvent.HandleEventAsync
  • IHandleAfterRender.OnAfterRenderAsync

Following is the implementation for this base component, CustomComponentBase:

using Microsoft.AspNetCore.Components;

public class CustomComponentBase : ComponentBase, IHandleEvent, IHandleAfterRender
{
    private bool _hasCalledOnAfterRender;

    [Inject]
    private IServiceProvider Services { get; set; } = default!;

    public override Task SetParametersAsync(ParameterView parameters)
        => InvokeWithBlazorServiceContext(() => base.SetParametersAsync(parameters));

    Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
        => InvokeWithBlazorServiceContext(() =>
        {
            var task = callback.InvokeAsync(arg);
            var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
                task.Status != TaskStatus.Canceled;

            StateHasChanged();

            return shouldAwaitTask ?
                CallStateHasChangedOnAsyncCompletion(task) :
                Task.CompletedTask;
        });

    Task IHandleAfterRender.OnAfterRenderAsync()
        => InvokeWithBlazorServiceContext(() =>
        {
            var firstRender = !_hasCalledOnAfterRender;
            _hasCalledOnAfterRender |= true;

            OnAfterRender(firstRender);

            return OnAfterRenderAsync(firstRender);
        });

    private async Task CallStateHasChangedOnAsyncCompletion(Task task)
    {
        try
        {
            await task;
        }
        catch
        {
            if (task.IsCanceled)
            {
                return;
            }

            throw;
        }

        StateHasChanged();
    }

    private async Task InvokeWithBlazorServiceContext(Func<Task> func)
    {
        BlazorServiceAccessor.Services = Services;
        await func();
    }
}

Any components extending CustomComponentBase will automatically have BlazorServiceAccessor.Services set to the IServiceProvider in the current Blazor DI scope.


I'm not completely sure what the right place for this content is. Maybe a page one of the "Advanced" sections?

@guardrex guardrex added this to Triage in Blazor.Docs via automation Dec 14, 2022
@guardrex guardrex added Pri1 High priority, do before Pri2 and Pri3 doc-enhancement and removed ⌚ Not Triaged area-blazor labels Dec 14, 2022
@guardrex guardrex moved this from Triage to P0/P1 - High Priority in Blazor.Docs Dec 14, 2022
@guardrex guardrex moved this from P0/P1 - High Priority to In progress in Blazor.Docs Dec 14, 2022
@guardrex
Copy link
Collaborator

@MackinnonBuck ... Do you want to address Javier's ideas for the code updates on the PR? I can try to make those ideas work, but I suspect that I'm going to hit problems that I don't know how to solve and end up wasting Gaurav's 💰💰💰.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Blazor doc-enhancement Pri1 High priority, do before Pri2 and Pri3
Projects
Archived in project
Blazor.Docs
  
Done
Development

Successfully merging a pull request may close this issue.

3 participants