10

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://learn.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();
            }
        }
    }
}
abatishchev
  • 98,240
  • 88
  • 296
  • 433
Dungimon
  • 353
  • 2
  • 3
  • 10

3 Answers3

10

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

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Thanks for the response. My code sample was for brevity, the instances I am actually instantiating in the commercial app I'm working on are a lot more complicated to construct so I didn't want anyone to get bogged down in those details. I'll make edit my post so it's a little clear, cheers. – Dungimon Jul 24 '18 at 15:58
  • @Dungimon you have to be specific. There are many overloads that can register a service, none that can *validate its configuration*. That's not DI's job. That's typically the job of the configuration loader. Ensuring that a *singleton* can be constructed though can be as simple as a call to `services.GetRequiredService<>()` once DI config is complete – Panagiotis Kanavos Jul 24 '18 at 16:00
6

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.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • Thanks for that, it does make complete sense. I've added some additional notes above the bold, I'd be interested to hear your thoughts. – Dungimon Jul 24 '18 at 15:39
  • Yeah, so the DI container is not disposing because it's not creating them. If you want to new it up yourself, then you also then need to dispose. However, again, it actually doesn't even matter on shutdown, since the process itself is exiting (everything in RAM goes away regardless of whether it's disposed or not). That said, either way, you're relying on a runtime error to determine the viability of your deployment. It would be far better to write integration tests, which you can then run as part of your build. Then, you don't even get to deployment with a bug here, either way. – Chris Pratt Jul 24 '18 at 15:46
  • Fortunately we've got unit, int and acceptance tests but unfortunately we have deploy to several environments and because our configuration management system is overly complicated it's not uncommon for malformed configuration to creep it's way into the app. This is for a high traffic e-Commerce website so I am just hoping for faster feedback in the CI/CD pipeline without having to impact our customers. Thanks for the responses though, will look into DI managing the life cycle completely, it sounds like a trade off to me from what I have to manage. – Dungimon Jul 24 '18 at 15:55
  • @Dungimon *verification/validation* isn't the job of the DI container. In most cases it's *configuration* that needs validating. Deployment errors are typically caused by bad configuration values – Panagiotis Kanavos Jul 24 '18 at 15:56
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.
Dungimon
  • 353
  • 2
  • 3
  • 10