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

Solution for accumulation of Disposable transient services #5496

Closed
rstropek opened this issue Apr 2, 2018 · 11 comments

Comments

@rstropek
Copy link

commented Apr 2, 2018

Given a service:

public class Repository : IRepository, IDisposable { ... }

It is injected as a transient:

static void Main(string[] args)
{
    var serviceProvider = new BrowserServiceProvider(configure =>
    {
        configure.Add(ServiceDescriptor.Transient<IRepository, Repository>());
    });

    new BrowserRenderer(serviceProvider).AddComponent<App>("app");
}

If this service is injected into a component, it is never disposed even when navigating away from the component:

@page "/"
@inject IRepository Repository
...

Is this a bug or is there a functionality available to dispose injected services implementing IDisposable?

@SteveSandersonMS

This comment has been minimized.

Copy link
Member

commented Apr 2, 2018

It's not a bug, as in general we don't know whether you want the service instance's lifetime to be scoped to that component. You might want to keep using that service instance over time.

In general your component needs to make its own decisions about what to dispose in its own Dispose method.

Closing as by design, but please let me know if you happen to know that the conventions are definitely different in some specific other case with ASP.NET.

@rstropek

This comment has been minimized.

Copy link
Author

commented Apr 2, 2018

The current behavior is clear for singletons. I am not sure if it makes sense for transients (scoped is currently not used, is it?). For transients, a new instance is created every time the service is requested. Therefore, it would be quite save to dispose the service once the component gets disposed.

One design problem with the current behavior is that the component receiving the service does not know whether it is transient or singleton. How should the component decide whether it should dispose the service instance?

The ASP.NET Core docs says: The container will call Dispose for IDisposable types it creates. As far as I know transient instances will be disposed at the end of the request in ASP.NET Core. So Blazor's behavior would be different.

@SteveSandersonMS

This comment has been minimized.

Copy link
Member

commented Apr 2, 2018

scoped is currently not used, is it?

Correct

One design problem with the current behavior is that the component receiving the service does not know whether it is transient or singleton

Isn't that the same for, say ASP.NET MVC controllers receiving services as constructor args?

The ASP.NET Core docs says: The container will call Dispose for IDisposable types it creates. As far as I know transient instances will be disposed at the end of the request in ASP.NET Core. So Blazor's behavior would be different.

For singletons, it doesn't make sense ever to dispose the instance (except if the IServiceContainer itself was disposed). For transients, I agree it would make sense to dispose them when the component itself is disposed.

What I'm not currently sure about (because I haven't yet investigated) is how that's even possible under the IServiceProvider contract. That interface only exposes object GetService(Type serviceType), which doesn't provide any mechanism to indicate that we want a service within a certain scope, or to be told whether the supplied service instance should later be disposed or not.

Clearly this requires a bit more research so I'll leave this issue open with a modified title.

@SteveSandersonMS SteveSandersonMS changed the title Service implementing IDisposable is not disposed by DI In DI, consider supporting component-scoped services, and auto-disposing transient services Apr 2, 2018

@rstropek

This comment has been minimized.

Copy link
Author

commented Apr 2, 2018

I did not check the ASP.NET Core source code but my understanding is that they use scopes for each request. Once the request is done, the scope is disposed (including all created services). Blazor would have to create a scope for each component. The router would have to dispose the scope similarly to disposing the component.

I cannot guarantee that this is correct. This is just my understanding of how it is done in ASP.NET Core.

@danroth27

This comment has been minimized.

Copy link
Member

commented Apr 5, 2018

@aspnet-hello aspnet-hello transferred this issue from aspnet/Blazor Dec 17, 2018

@mkArtakMSFT mkArtakMSFT added this to To do in Blazor via automation Jan 23, 2019

@danroth27 danroth27 moved this from To do to Design in Blazor Jan 23, 2019

@rynowak rynowak added this to the 3.0.0-preview6 milestone Mar 15, 2019

@mkArtakMSFT mkArtakMSFT modified the milestones: 3.0.0-preview6, Backlog Apr 19, 2019

@rynowak rynowak removed their assignment Apr 19, 2019

@danroth27 danroth27 modified the milestones: Backlog, 3.0.0-preview7 May 24, 2019

@danroth27

This comment has been minimized.

Copy link
Member

commented May 24, 2019

We're concerned about the potential for memory leaks here with transients. Circuit lifetime is arbitrarily long. We need to think about this.

@rynowak rynowak changed the title In DI, consider supporting component-scoped services, and auto-disposing transient services Investigate accumulation of Disposable transient services May 30, 2019

@danroth27 danroth27 referenced this issue May 30, 2019
11 of 28 tasks complete

@rynowak rynowak changed the title Investigate accumulation of Disposable transient services Solution for accumulation of Disposable transient services May 30, 2019

@rynowak

This comment has been minimized.

Copy link
Member

commented Jun 4, 2019

Summary

The solutions to this will require a new DI feature or will required going outside the DI system entirely.

The problem is that the DI system will track transient values. This means that anything you resolve as a transient is kept in memory until the DI scope is disposed.

There are a few approaches to this that we've outlined so far:

Self-serve unregistration

This is the proposal from DI: aspnet/Extensions#1785 - in this model each service that wants to support early disposal has to be aware of it.

public class SelfDisposer : IDisposable
{
    public SelfDisposer(IServiceCollectionNotifyDisposed notify)
    {
    }

    void IDisposable.Dispose() => _notify.NotifyDisposed(this);
}

Handles

This model allows requestors to resolve a Handle<> from DI. The handle matches the lifetime for the consumer's usage of the service. When you're done consuming the service you can dispose your handle:

  • If the service is transient it will be disposed and removed from memory
  • If the service is scoped, disposing the handle does nothing (disposed along with scope)
  • If the service is a singleton, disposing the handle does nothing (disposed along with container)

You can think of this as a non-owning handle - like std::shared_ptr<>.

public sealed class Handle<T> : IDisposable
{
    public Handle(T service, bool ownsLifetime)
    {
    }

    public void Dispose()
    {
        if (_ownsLifetime && service is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }
}

Owing handlers

This model allows requestors to resolve an OwningHandle<> from DI. All services are created as-if they are transients, and their lifetime is owned by the handle - they are disposed when the handle is disposed. There are cases where this won't work or won't work correctly. For instance if the service is registered an an instance then this is an error - or if the service is created by a factory function then there are no guarantees.

The reason why OwningHandle<> is required at all is to allow callers to specify whether they want to control the lifetime or not.

public sealed class OwningHandle<T> : IDisposable
{
    public Handle(T service)
    {
    }

    public void Dispose()
    {
        if (service is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }
}

@mkArtakMSFT mkArtakMSFT moved this from To do to Preview7 in Blazor Jun 5, 2019

@divega

This comment has been minimized.

Copy link
Member

commented Jun 7, 2019

@rynowak I am curious how the two handle-based approaches would work when an IDisposable service depends on other IDisposable service(es) (or a tree of them). At first glance, it seems to me that in order for the memory to be released, each IDisposable service would need to be changed to start only depending on other handles, and never on other IDisposable services directly. I.e. isn’t it kind of viral?

Also, isn’t it a bit strange that the the handles themselves are IDisposable but need to break the rules of how IDisposable services work in order to release any memory? I.e. to remove their owned service (and possible themselves) from the container’s disposable list, wouldn’t they need the feature you describe as self-serve unregistration?

In summary, assuming I am not missing anything:

  1. Self-serve unregistration could be the basic building block. Any IDisposable service that is aware of it can use it directly and continue to be consumed with no changes on consuming code to enjoy the benefit of early memory release.

  2. Handlers (any of the two variations) can be used to optimize existing IDisposable services without requiring modifications, but only if they don’t depend on other IDisposable services.

  3. If you have a tree of IDisposable services that you want to optimize, you will have to change the code of the service one way or the other. You face the choice of having to wrap every IDisposable into a handle, or you can make all of them self-unregistering, with the latter providing a nicer experience for consumers of the service.

@mkArtakMSFT mkArtakMSFT moved this from To Do to Preview8 in Blazor Jun 7, 2019

@dazinator

This comment has been minimized.

Copy link

commented Jun 10, 2019

Is there any reason why Blazor couldn't use an IServiceScope for page / component activation (as also mentioned by @rstropek ) - Dispose of that scope when the page / component is disposed.

This means that anything you resolve as a transient is kept in memory until the DI scope is disposed.

So if blazor used a scope and disposed of it appropriately then problem solved?

P.s a similar pattern can be used successfully in other UI stacks where dependencies are injected. For example in windows forms or wpf, or Xamarin forms apps, when navigating to a new UI you might have something conceptually like this:

E.g:

Public void NavigateTo<T>()
{
    using (var scope = sp.CreateScope())
    {
        var nextUi = scope.GetRequiredService<T>();
        nextUi.ShowDialog(); // I.e in windows forms this would block and control return once form was closed.
    // if not using a show method that blocks you'd adjust this so that the scope was kept around and only disposed once form was closed.
    }
}

@smartprogrammer93

This comment has been minimized.

Copy link

commented Jul 8, 2019

So for now. do i have to dispose of every transient IDisposable Service in the dispose function of the component?

I believe this should be a high priority issue. Thank you for your hard work

rynowak pushed a commit that referenced this issue Aug 1, 2019
Ryan Nowak
Add component for managing a DI scope
Fixes: #5496
Fixes: #10448

This change adds a *utility* base class that encourages you to do the
right thing when you need to interact with a disposable scoped or
transient service.

This solution ties the lifetime of a DI scope and a service to a
component instance. Note that this is not recursive - we expect users to
pass services like this around (or as cascading values) if the design
dictates it.
rynowak added a commit that referenced this issue Aug 1, 2019
Add component for managing a DI scope
Fixes: #5496
Fixes: #10448

This change adds a *utility* base class that encourages you to do the
right thing when you need to interact with a disposable scoped or
transient service.

This solution ties the lifetime of a DI scope and a service to a
component instance. Note that this is not recursive - we expect users to
pass services like this around (or as cascading values) if the design
dictates it.
rynowak added a commit that referenced this issue Aug 1, 2019
Add component for managing a DI scope
Fixes: #5496
Fixes: #10448

This change adds a *utility* base class that encourages you to do the
right thing when you need to interact with a disposable scoped or
transient service.

This solution ties the lifetime of a DI scope and a service to a
component instance. Note that this is not recursive - we expect users to
pass services like this around (or as cascading values) if the design
dictates it.
rynowak added a commit that referenced this issue Aug 1, 2019
Add component for managing a DI scope
Fixes: #5496
Fixes: #10448

This change adds a *utility* base class that encourages you to do the
right thing when you need to interact with a disposable scoped or
transient service.

This solution ties the lifetime of a DI scope and a service to a
component instance. Note that this is not recursive - we expect users to
pass services like this around (or as cascading values) if the design
dictates it.
rynowak added a commit that referenced this issue Aug 1, 2019
Add component for managing a DI scope
Fixes: #5496
Fixes: #10448

This change adds a *utility* base class that encourages you to do the
right thing when you need to interact with a disposable scoped or
transient service.

This solution ties the lifetime of a DI scope and a service to a
component instance. Note that this is not recursive - we expect users to
pass services like this around (or as cascading values) if the design
dictates it.

@rynowak rynowak added the Done label Aug 1, 2019

@rynowak

This comment has been minimized.

Copy link
Member

commented Aug 1, 2019

Added a utility component for this. Will updated docs as part of the release.

@rynowak rynowak closed this Aug 1, 2019

Blazor automation moved this from To do to Done Aug 1, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.