Как определить, в каком порядке IHostedServices вызываются при завершении работы?

Я поддерживаю пакет интеграции, который позволяет моим пользователям интегрировать мою библиотеку с ASP.NET Core. Этот пакет должен быть совместим со всеми версиями ASP.NET Core, начиная с 2.1. При завершении работы приложения мой пакет интеграции должен иметь возможность выполнять асинхронную очистку и, к сожалению, не может зависеть от IAsyncDisposable до Microsoft.Bcl.AsyncInterfaces (см. ниже).

Таким образом, единственный способ, которым это представляется возможным, — это зарегистрировать реализацию IHostedService. Его метод StopAsync вызывается при завершении работы:

public sealed class ShutdownHostedService : IHostedService
{
    public MyLibaryCleanupObject Obj;
    public Task StartAsync(CancellationToken token) => Task.CompletedTask;
    public Task StopAsync(CancellationToken token) => this.Obj.CleanupAsync();
}

services.AddSingleton<IHostedService>(new ShutdownHostedService { Obj = ... });

Однако разработчики приложений, конечно, могут добавлять свои собственные IHostedService реализации, которые могут взаимодействовать с моей библиотекой. Вот почему важно, чтобы моя собственная реализация IHostedService вызывалась последней. Но вот в чем проблема.

С введением ASP.NET Core 2.1 разработчики приложений могут выбирать между использованием нового Microsoft.Extensions.Hosting.Host и (теперь устаревшего) Microsoft.AspNetCore.WebHost. С WebHost при завершении работы IHostedService реализаций вызываются в порядке регистрации, тогда как с Host IHostedService реализации вызываются в противоположном порядке регистрации.

Это проблематично для меня, потому что мой размещенный сервис должен вызываться последним. Поскольку разработчики приложений могут использовать мой пакет интеграции в своем существующем приложении ASP.NET Core, они могут по-прежнему использовать WebHost, поэтому важно поддерживать этот сценарий.

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

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

Примечание к IAsyncDisposable:

Одно из решений, которое приходит на ум (как справедливо отмечает Ян в комментариях), состоит в том, чтобы добавить регистрацию IAsyncDisposable Singleton к ServiceCollection. Это позволило бы асинхронную очистку при завершении работы. К сожалению, из-за ограничений (объясненных здесь) мой пакет интеграции не может принять зависимость от Microsoft.Bcl.AsyncInterfaces и, следовательно, не от IAsyncDisposable. Это неприятная ситуация, которая, безусловно, усложняет дело. На самом деле, причина невозможности получить зависимость от IAsyncDisposable — это причина, по которой я ищу альтернативные способы реализации кода асинхронного завершения работы.


person Steven    schedule 08.12.2020    source источник
comment
Я сильно сомневаюсь, что фоновые службы предназначены для использования таким образом, и я уверен, что порядок, в котором они останавливаются, является деталью реализации, поэтому совершенно не гарантируется и может быть изменен в любое время, поэтому я определенно рекомендую изучить другие способы добиться этого. Я бы предложил IAsyncDisposable, который изначально поддерживается в Core 3.0 и выше и имеет прокладку для более старых версий через пакет Microsoft.Bcl.AsyncInterfaces NuGet. Кроме того, спросите непосредственно в репозитории ASP.NET Core GitHub.   -  person Ian Kemp    schedule 08.12.2020
comment
Привет @IanKemp, спасибо за ваш комментарий. К сожалению, использование IAsyncDisposable неуместно (я должен был более четко указать это в своем вопросе). IAsyncDisposable это как раз проблема, почему я в такой ситуации. Новая версия библиотеки, которую я разрабатываю, намеренно удаляет зависимость от Microsoft.Bcl.AsyncInterfaces, которая запрещает мне добавлять регистрацию IAsyncDisposable в файл IServiceCollection.   -  person Steven    schedule 08.12.2020
comment
В худшем случае, если автоматическое решение не найдено, создайте параметр конфигурации (флаг), который можно использовать для указания желаемого порядка. (просто у меня в голове)   -  person Nkosi    schedule 08.12.2020
comment
Мне кажется, что лучшим вариантом будет остановить разработку версий SimpleInjector до .NET Standard 2.1. Замечательно продолжать поддерживать людей, использующих более старые версии .NET, но необходимость удалять код для этого и использовать то, что по сути является очень хакерским обходным путем с IHostedService, наводит меня на мысль, что это действительно не стоит вашего времени.   -  person Ian Kemp    schedule 08.12.2020
comment
Другим вариантом может быть регистрация обработчика на IApplicationLifetime либо ApplicationStopping, либо ApplicaionStopped.   -  person Nkosi    schedule 08.12.2020
comment
Согласен с Нкоси, использование времени жизни приложения кажется лучшим выбором для такого рода задач, особенно если учесть, что вам нужен только шаг очистки, а не фактический запуск. Проблема в том, что события времени жизни не поддерживают асинхронную работу. Что может сработать, так это предоставить пользовательский IHostLifetime, но это также немного усложняет ситуацию, поскольку вам нужно поддерживать разные времена жизни…   -  person poke    schedule 08.12.2020
comment
@Nkosi, этот интерфейс проблематичен сам по себе, потому что он устарел и, возможно, даже уже удален из ASP.NET 5.0.   -  person Steven    schedule 08.12.2020
comment
Решение, которое я в конечном итоге использовал, - это "просто" вызвать CleanupAsync().GetAwaiter().GetResult() из метода Dispose. Смотрите мой собственный ответ.   -  person Steven    schedule 08.12.2020
comment
Правильный интерфейс для 3.0 и более поздних версий — IHostApplicationLifetime кстати.   -  person poke    schedule 09.12.2020
comment
@poke, это правильно. Однако этот интерфейс недоступен в версии 2.1.   -  person Steven    schedule 09.12.2020


Ответы (1)


Решение, которое я в конечном итоге использую, идентично решению, выбранному классом Microsoft.Extensions.Hosting.Internal.Host от Microsoft, и не требует использования экземпляров IHostedService. Класс Host содержит следующий метод Dispose:

public void Dispose()
{
    this.DisposeAsync().GetAwaiter().GetResult();
}

Это может вызвать недоумение и может рассматриваться как плохая практика, но не забывайте, что:

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

Вот почему класс Microsoft Host может использовать этот подход, и это означает, что это безопасный подход и для моей библиотеки.

person Steven    schedule 08.12.2020