Как правильно и безопасно удалять одиночные экземпляры, зарегистрированные в контейнере, при завершении работы приложения ASP.NET Core

Я ищу руководство о том, как правильно и безопасно удалять зарегистрированные одноэлементные экземпляры, когда мое приложение ASP.NET Core 2.0 закрывается.

Согласно следующему документу, если я зарегистрирую одноэлементный экземпляр (через IServiceCollection), контейнер никогда не будет пытаться создать экземпляр (и не будет избавляться от экземпляра), поэтому мне остается самому избавиться от этих экземпляров, когда приложение закрывается .

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.0 (у 2.1 такие же рекомендации)

Я прилагаю некоторый псевдокод, иллюстрирующий то, чего я пытаюсь достичь.

Примечание. Мне приходится поддерживать ссылку на IServiceCollection, поскольку IServiceProvider, предоставляемый методу OnShutDown, является простым локатором службы и не дает мне возможности выполнять сложные запросы.

Когда приложение закрывается, мне нужен общий способ гарантировать удаление всех одноэлементных экземпляров. Я мог бы поддерживать ссылку на все эти одноэлементные экземпляры напрямую, но это плохо масштабируется.

Первоначально я использовал фабричный метод, который обеспечивал бы, чтобы DI управлял временем жизни моих объектов, однако выполнение фабричного метода происходило во время выполнения в конвейере обработки запроса, что означало, что если он выдавал исключение, ответ был 500 InternalServerError. и была зарегистрирована ошибка. Создавая объект напрямую, я стремлюсь к более быстрой обратной связи, чтобы ошибки при запуске приводили к автоматическому откату во время развертывания. Мне это не кажется неразумным, но в то же время я не злоупотребляю DI.

Есть ли у кого-нибудь предложения, как сделать это более элегантно?

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();
            }
        }
    }
}

person Dungimon    schedule 24.07.2018    source источник
comment
Задача DI — вызывать Dispose() для объектов, которые он создает. Он должен создавать любые синглтоны   -  person Panagiotis Kanavos    schedule 24.07.2018


Ответы (3)


Работа DI заключается в удалении любых объектов IDisposable, которые он создает, независимо от того, являются ли они временными, ограниченными или одноэлементными. Не регистрируйте существующие синглетоны, если только вы не собираетесь впоследствии их очищать.

В коде вопроса нет причин регистрировать экземпляр DisposableSingletonInstance. Он должен быть зарегистрирован в:

services.AddSingleton<IDisposableSingletonInstance,DisposableSingletonInstance>();

Когда IServiceCollection будет удален, он вызовет Dispose() для всех одноразовых объектов, созданных им. Для веб-приложений это происходит, когда заканчивается RunAsync();

То же самое относится и к службам с ограниченной областью действия. Однако в этом случае экземпляры будут удалены при выходе из области видимости, например, при завершении запроса.

ASP.NET создает область для каждого запроса. Если вы хотите, чтобы ваша служба была удалена после завершения этого запроса, вы должны зарегистрировать ее с помощью:

services.AddScoped<IDisposableSingletonInstance,DisposableSingletonInstance>();

Проверка

Для последнего редактирования:

Создавая объект напрямую, я стремлюсь к более быстрой обратной связи, чтобы ошибки при запуске приводили к автоматическому откату во время развертывания.

Это другая проблема. Ошибки развертывания часто вызваны неправильными значениями конфигурации, не отвечающими базами данных и т. д.

Проверка служб

Очень быстрый и грязный способ проверки — создать экземпляр синглтона после завершения всех шагов запуска:

services.GetRequiredService<IDisposableSingletonInstance>();

Проверка конфигурации

Проверка конфигурации более сложна, но не так сложна. Можно использовать атрибуты аннотации данных в классах конфигурации для простых правил и использовать Validator для их проверки.

Другой вариант — создать интерфейс IValidateable с методом Validate, который должен быть реализован каждым классом конфигурации. Это упрощает обнаружение с помощью отражения.

В этой статье показаны как интерфейс IValidator можно использовать в сочетании с IStartupFilter для проверки всех объектов конфигурации при первом запуске приложения

Из статьи:

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;
    }
}

Конструктор получает все экземпляры, реализующие IValidatable, от поставщика зависимостей и вызывает для них Validate().

person Panagiotis Kanavos    schedule 24.07.2018
comment
Спасибо за ответ. Мой пример кода был для краткости, экземпляры, которые я на самом деле создаю в коммерческом приложении, над которым я работаю, намного сложнее построить, поэтому я не хотел, чтобы кто-то увяз в этих деталях. Я отредактирую свой пост, чтобы он был немного ясным, ура. - person Dungimon; 24.07.2018
comment
@Dungimon, ты должен быть конкретным. Существует множество перегрузок, которые могут зарегистрировать службу, но ни одна из них не может проверить ее конфигурацию. Это не работа DI. Обычно это работа загрузчика конфигурации. Обеспечение возможности создания singleton может быть таким же простым, как вызов services.GetRequiredService<>() после завершения конфигурации DI. - person Panagiotis Kanavos; 24.07.2018

Это не точно. Синглтоны удаляются при закрытии приложения, хотя на самом деле это не так уж важно, потому что, когда процесс останавливается, все остается в любом случае.

Общее эмпирическое правило заключается в том, что при использовании DI вы должны использовать DI полностью вниз, что означает, что вы почти никогда не будете утилизировать самостоятельно, где бы то ни было. Все дело в собственности. Когда вы сами создаете новые вещи, вы также несете ответственность за их утилизацию. Однако при использовании DI контейнер — это то, что обновляет вещи, и, следовательно, контейнер и только контейнер должен затем удалять эти вещи.

person Chris Pratt    schedule 24.07.2018
comment
Спасибо за это, это имеет полный смысл. Я добавил несколько дополнительных примечаний над жирным шрифтом, мне было бы интересно услышать ваши мысли. - person Dungimon; 24.07.2018
comment
Да, поэтому контейнер DI не удаляет, потому что он их не создает. Если вы хотите обновить его самостоятельно, то вам также необходимо утилизировать. Однако, опять же, это на самом деле даже не имеет значения при завершении работы, поскольку сам процесс завершается (все в ОЗУ исчезает независимо от того, удалено оно или нет). Тем не менее, в любом случае вы полагаетесь на ошибку времени выполнения, чтобы определить жизнеспособность вашего развертывания. Гораздо лучше было бы написать интеграционные тесты, которые затем можно запускать как часть сборки. Тогда вы даже не доберетесь до развертывания с ошибкой в ​​любом случае. - person Chris Pratt; 24.07.2018
comment
К счастью, у нас есть модульные, внутренние и приемочные тесты, но, к сожалению, нам приходится выполнять развертывание в нескольких средах, и, поскольку наша система управления конфигурацией слишком сложна, искаженная конфигурация нередко проникает в приложение. Это для веб-сайта электронной коммерции с высоким трафиком, поэтому я просто надеюсь на более быструю обратную связь в конвейере CI / CD, не влияя на наших клиентов. Однако спасибо за ответы, я полностью рассмотрю DI, управляющий жизненным циклом, для меня это звучит как компромисс по сравнению с тем, чем мне приходится управлять. - person Dungimon; 24.07.2018
comment
@Dungimon проверка/проверка не является задачей контейнера DI. В большинстве случаев в проверке нуждается конфигурация. Ошибки развертывания обычно вызваны неправильными значениями конфигурации. - person Panagiotis Kanavos; 24.07.2018

Спасибо за ответы Panagiotis Kanavos и Крис Пратт и за помощь в разъяснении того, как лучше поступить в этом случае. Два выводных пункта таковы:

  • Всегда стремитесь позволить контейнеру управлять жизненным циклом ваших объектов, чтобы при завершении работы приложения контейнер автоматически удалял все объекты.
  • Проверяйте всю свою конфигурацию при запуске приложения, прежде чем она будет использована объектами, зарегистрированными в контейнере. Это позволяет вашему приложению быстро выходить из строя и защищает DI от создания исключений при создании новых объектов.
person Dungimon    schedule 24.07.2018