Aspnetcore: [Blazor] OwningComponentBase should change Scope for @injected dependencies in child components

Created on 4 Mar 2020  路  4Comments  路  Source: dotnet/aspnetcore

(Alternative request: Create a new ScopedComponent that does this).

Desired

When a page descends from OwningComponentBase, all child controls within that component should have their @inject dependencies injected from that new scope.

Actual

Dependencies are injected from the main app's scope.

    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.Services.AddScoped<SomeService>();
            builder.RootComponents.Add<App>("app");

            await builder.Build().RunAsync();
        }
    }

    public class SomeService
    {
        private static volatile int NextId;
        public int Id { get; set; }

        public SomeService()
        {
            Id = Interlocked.Increment(ref NextId);
        }
    }

//========================Index.razor
@page "/"
@inherits OwningComponentBase<IServiceProvider>
<br />
<CascadingValue Value=@Service>
    <EmbeddedComponent/>
</CascadingValue>

//========================EmbeddedComponent.razor
@page "/counter"
@inject SomeService InjectedService

<p>
    Injected value = @InjectedService.Id
</p>
<p>
    Resolved value = @ResolvedService.Id
</p>

@code
{
    [CascadingParameter]
    public IServiceProvider ServiceProvider { get; set; }

    private SomeService ResolvedService;

    protected override void OnInitialized()
    {
        base.OnInitialized();
        ResolvedService = (SomeService)ServiceProvider.GetService(typeof(SomeService));
    }
}

Desired output:
Injected value = 1
Resolved value = 1

Actual output:
Injected value = 1
Resolved value = 2

Answered Resolved area-blazor

Most helpful comment

@javiercn This would make it the same as the way ASP MVC requests work for the pages that the developer wants to descend from OwningComponentBase. A per-page scope.

Currently, the lifetimes are confusing

  • Singleton (makes sense)

    • ASP.NET: Single-instance shared across all tabs and all users connected to the server.

    • Server-side: Same as ASP.NET

    • Client-side: Single-instance per tab, not shared across tabs (different, but sensible).

  • Scoped

    • ASP.NET: Built when the page is built, destroyed when it completes, lifetime=page.

    • Server-side: Same as a singleton, except unique per visitor + per tab, lifetime=app.

    • Client-side: Same as a singleton, except unique per visitor + per tab, lifetime = app.

  • Transient

    • ASP.NET: Built per build-up graph

    • Server-side: Built per injection

    • Client-side: Built per injection

Perhaps if we had a new ScopedComponent we could at least make Scoped behave the same way as one would expect coming from ASP.NET development.

Having to cascade services, or a service provider is too much work. Also, the owning page has to be aware of the dependencies of all of its inner contents no matter how deeply nested they are. It would be better to be able explicitly just state that the Scoped components in this page exist for the lifetime of this page only.

As for your recommendation to not proceed using any of the suggested approaches. It begs the question, why does OwningComponentBase<T> exist at all in that case? :)

All 4 comments

@mrpmorris thanks for contacting us.

That is not a feature that we have and it is not something we are likely to add as it would result in a very convoluted model where it would be really hard to reason about a component dependency as its scope would be based on what its parent is and might vary if a component is the child of different component types, which I don't think is a trait we want to have.

If you want to achieve this you can do so by creating and resolving the dependency on the parent component and exposing it as cascading parameter for the children, but that is also not something we recommend or alternatively implementing this yourself by:

  • Creating your own base class.
  • Forcing child components to inherit from it.
  • Creating your own attribute to perform injection.
  • Overriding OnInitializedAsync in your component class and resolving the dependencies there from the parent scope that you exposed as a cascading value (for example).

As i said, this is not something we plan to implement and we discourage you from going this way.

@javiercn This would make it the same as the way ASP MVC requests work for the pages that the developer wants to descend from OwningComponentBase. A per-page scope.

Currently, the lifetimes are confusing

  • Singleton (makes sense)

    • ASP.NET: Single-instance shared across all tabs and all users connected to the server.

    • Server-side: Same as ASP.NET

    • Client-side: Single-instance per tab, not shared across tabs (different, but sensible).

  • Scoped

    • ASP.NET: Built when the page is built, destroyed when it completes, lifetime=page.

    • Server-side: Same as a singleton, except unique per visitor + per tab, lifetime=app.

    • Client-side: Same as a singleton, except unique per visitor + per tab, lifetime = app.

  • Transient

    • ASP.NET: Built per build-up graph

    • Server-side: Built per injection

    • Client-side: Built per injection

Perhaps if we had a new ScopedComponent we could at least make Scoped behave the same way as one would expect coming from ASP.NET development.

Having to cascade services, or a service provider is too much work. Also, the owning page has to be aware of the dependencies of all of its inner contents no matter how deeply nested they are. It would be better to be able explicitly just state that the Scoped components in this page exist for the lifetime of this page only.

As for your recommendation to not proceed using any of the suggested approaches. It begs the question, why does OwningComponentBase<T> exist at all in that case? :)

@mrpmorris Frankly, OwningComponentBase is a mess. It is bad because:

  • It restricts you to a single scoped service, or you have to use the non generic class, which exposes the serviceprovider and converts your dependencies into runtime dependencies.
  • It makes it impossible to inherit from another component or another base class. Using inheritance for the sole purpose of changing the scope of a service is complete madness.

I never use it. I'll create a specific factory for the specific type(s) I need and pass the scoped service as a parameter to other controls if they need it. The whole DI in blazor is messed up (again, like any DI implementation MS does), and I mostly try to work around it.
Source is here: https://github.com/dotnet/aspnetcore/blob/3148acfb105a16aa6c535d00eb0e50ec03992f3f/src/Components/Components/src/OwningComponentBase.cs

I too would like to sidestep this issue by using a better DI container.

Was this page helpful?
0 / 5 - 0 ratings