How to correctly and safely dispose of singletons instances registered in the container when an ASP.NET Core app shuts down

陌路散爱 提交于 2020-04-18 05:35:29

问题


I am looking for guidance on how to correctly and safely dispose of registered singleton instances when my ASP.NET Core 2.0 app is shutting down.

According to the following document, if I register a singleton instance (via IServiceCollection) the container will never attempt to create an instance (nor will it dispose of the instance), thus I am left to dispose of these instances myself when the app shuts down.

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.0 (2.1 has the same guidance)

I enclose some pseudo code that illustrates what I am trying to achieve.

Note I am having to maintain a reference to IServiceCollection since the IServiceProvider provided to the OnShutDown method is a simple service locator and doesn't give me the ability to execute complex queries.

When the app shuts down I want a generic way to ensure all singleton instances are disposed. I could maintain a reference to all these singleton instances directly but this doesn't scale well.

I originally used the factory method which would ensure the DI managed the lifetime of my objects, however, the execution of the factory method happened at runtime in the pipeline of handling a request, which meant that if it threw an exception the response was 500 InternalServerError and an error was logged. By creating the object directly I am striving for faster feedback so that errors on startup lead to a automatic rollback during the deployment. This doesn't seem unreasonable to me, but then at the same time I don't to misuse the DI.

Does anyone have any suggestions how I can achieve this more elegantly?

namespace MyApp
{
    public class Program
    {
        private static readonly CancellationTokenSource cts = new CancellationTokenSource();

        protected Program()
        {
        }

        public static int Main(string[] args)
        {
            Console.CancelKeyPress += OnExit;
            return RunHost(configuration).GetAwaiter().GetResult();
        }

        protected static void OnExit(object sender, ConsoleCancelEventArgs args)
        {
            cts.Cancel();
        }

        static async Task<int> RunHost()
        {
            await new WebHostBuilder()
                .UseStartup<Startup>()
                .Build()
                .RunAsync(cts.Token);
        }
    }

    public class Startup
    {
        public Startup()
        {
        }

        public void ConfigureServices(IServiceCollection services)
        {
            // This has been massively simplified, the actual objects I construct on the commercial app I work on are
            // lot more complicated to construct and span several lines of code.
            services.AddSingleton<IDisposableSingletonInstance>(new DisposableSingletonInstance());

            // See the OnShutdown method below
            this.serviceCollection = services;
        }

        public void Configure(IApplicationBuilder app)
        {
            var applicationLifetime = app.ApplicationServices.GetRequiredService<IApplicationLifetime>();
            applicationLifetime.ApplicationStopping.Register(this.OnShutdown, app.ApplicationServices);

            app.UseAuthentication();
            app.UseMvc();
        }

        private void OnShutdown(object state)
        {
            var serviceProvider = (IServiceProvider)state;

            var disposables = this.serviceCollection
                .Where(s => s.Lifetime == ServiceLifetime.Singleton &&
                            s.ImplementationInstance != null &&
                            s.ServiceType.GetInterfaces().Contains(typeof(IDisposable)))
                .Select(s => s.ImplementationInstance as IDisposable).ToList();

            foreach (var disposable in disposables)
            {
                disposable?.Dispose();
            }
        }
    }
}

回答1:


It's the DI's job to dispose of any IDisposable objects it creates, whether transient, scoped or singleton. Don't register existing singletons unless you intend to clean them up afterwards.

In the question's code there's no reason to register an instance of DisposableSingletonInstance. It should be registered with :

services.AddSingleton<IDisposableSingletonInstance,DisposableSingletonInstance>();

When the IServiceCollection gets disposed, it will call Dispose() on all the disposable entities created by it. For web applications, that happens when RunAsync() ends;

The same holds for scoped services. In this case though, the instances will be disposed when the scope exits, eg when a request ends.

ASP.NET creates a scope for each request. If you want your service to be disposed when that request ends, you should register it with :

services.AddScoped<IDisposableSingletonInstance,DisposableSingletonInstance>();

Validation

For the latest edit :

By creating the object directly I am striving for faster feedback so that errors on startup lead to a automatic rollback during the deployment.

That's a different problem. Deployment errors are often caused by bad configuration values, unresponsive databases etc.

Validating Services

A very quick & dirty way to check would be to instantiate the singleton once all startup steps are complete with :

services.GetRequiredService<IDisposableSingletonInstance>();

Validating Configuration

Validating the configuration is more involved but not that tricky. One could use Data Annotation attributes on the configuration classes for simple rules and use the Validator class to validate them.

Another option is to create an IValidateable interface with a Validate method that has to be implemented by each configuration class. This makes discovery easy using reflection.

This article shows how the IValidator interface can be used in conjunction with an IStartupFilter to validate all configuration objects when an application starts for the first time

From the article :

public class SettingValidationStartupFilter : IStartupFilter  
{
    readonly IEnumerable<IValidatable> _validatableObjects;
    public SettingValidationStartupFilter(IEnumerable<IValidatable> validatableObjects)
    {
        _validatableObjects = validatableObjects;
    }

    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        foreach (var validatableObject in _validatableObjects)
        {
            validatableObject.Validate();
        }

        //don't alter the configuration
        return next;
    }
}

The constructor gets all instances that implement IValidatable from the DI provider and calls Validate() on them




回答2:


That's not accurate. Singletons are disposed at app shutdown, though it's kind of not actually all that relevant because when the process stops, everything goes with it anyways.

The general rule of thumb is that when using DI, you should use DI all the way down, which then means you'll almost never be disposing on your own, anywhere. It's all about ownership. When you new stuff up yourself, you're also then responsible for disposing of it. However, when using DI, the container is what's newing things up, and therefore, the container and only the container should then dispose of those things.




回答3:


Thanks for the responses Panagiotis Kanavos and Chris Pratt and for helping to clarify how best to deal with this scenario. The two take away points are this:

  • Always strive to let the container manage the life cycle of your objects so when the app is shutdown the container will automatically dispose of all objects.
  • Validate all your configuration on app startup before it is consumed by objects registered in the container. This allows your app to fail fast and protects your DI from throwing exceptions when creating new objects.


来源:https://stackoverflow.com/questions/51501578/how-to-correctly-and-safely-dispose-of-singletons-instances-registered-in-the-co

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!