Небольшой обзор стандартных средств запуска бэкграунд-задач в аспнет приложениях — что есть, чем отличается, как пользоваться. Встроенный механизм запуска таких задач строится вокруг интерфейса IHostedService и метода-расширения для IServiceCollection — AddHostedService. Но есть несколько способов реализовать фоновые задачи через этот механизм (и ещё несколько неочевидных моментов поведения этого механизма).
Запускаем фоновую задачу
С помощью механизмов aspnet core можно как решить задачу единократного запуска фоновой задачи при старте приложения, так и запускать какую-то задачу периодически. Для иллюстрации всех нюансов в качестве примера я буду вызывать каждые 5 секунд метод DoSomeWorkAsync стороннего сервиса ISomeBusinessLogicService, а ещё при остановке приложения делать очистку данных другим методом DoSomeCleanup.
Собственная реализация IHostedService
Интерфейс предоставляет 2 метода:
Что важно знать при реализации интерфейса? Все IHostedService запускаются последовательно, а вызов StartAsync блокирует запуск остальной части приложения. Поэтому в StartAsync не должно быть длинных блокирующих операций, если вы только действительно не хотите отложить запуск приложения до завершения этой операции (например, при миграции БД):
Именно это является особенностью и мотивацией реализовать фоновые операции через IHostedService — если вам нужен полный контроль за запуском и остановкой фонового сервиса. Если это не так важно (а часто это не так важно), то достаточно отнаследоваться от класса BackgroundService.
Ещё пара важных для реализации моментов:
- У CancellationToken в StopAsync есть 5 секунд для корректного завершения
- StopAsync может вообще не быть вызван при неожиданном завершенияя приложения
Общая реализация в этом случае может выглядеть примерно так:
Наследование от BackgroundService
BackgroundService — это абстрактный класс, котоырй реализует IHostedService, сам обрабатывает запуск и остановку, предоставляя 1 абстрактный метод ExecuteAsync:
StartAsync и StopAsync всё ещё можно перегрузить. Реализация фоновых задач через BackgroundService подходит для всех сценариев, где не нужно блокировать запуск приложения до завершения выполнения операции.
Общая реализация:
Когда и как запускается IHostedService
Занятный факт, правильный ответ — "зависит".
В .NET Core 2.x IHostedService запускались после конфигурирования и старта Kestrel, то есть после того, как приложение начинает слушать порты для приема запросов. Это значит, что, например, мы можем получить в фоновом сервисе объект IServer и IServerAddressesFeature и быть уверенными, что в момент запуска фонового сервиса список прослушиваемых адресов будет уже нсстроен. Ещё это значит, что на момент запуска IHostedService приложение уже может отвечать на запросы клиентов, поэтому нельзя гарантировать, что на момент обработки запроса какой-то из IHostedService уже запущен.
В .NET Core 3.0 с переходом на новую абстракцию IHost поведение изменилось — теперь Kestrel начал запускаться как отдельный IHostedService последним после всех остальных IHostedService. Фактически фоновые сервисы запускаются до метода Statup.Configure(). Теперь можно гарантировать, что на момент начала прослушивания портов и обработки запросов все другие фоновые сервисы запущены, а ещё можно не начинать обработку запросов до завершения запуска одного из фоновых сервисов с помощью переопределения StartAsync.
В .NET 6 всё снова немного поменялось. Появился Minimal hosting API, в котором нет дополнительных абстрацией в виде Startup.cs, а приложение конфигурируется явно с помощью нового класса WebApplication. Тут надо отметить, что новое апи включено по-умолчанию в шаблон аспнет приложения, поэтому для новых проектов из коробки будет использоваться именно оно. Все IHostedService в этом случае запускаются, когда вы вызываете WebApplication.Run(), то есть, уже после того, как вы настроили приложение и список прослушиваемых адресов. Подробнее об этом написано в issue на github.
Фактически это значит, что поведение и доступные в IHostedService параметры могут меняться в зависимости от версии .net и способа хостинга, и не может полагаться внутри сервиса на то, что Kestrel уже сконфигурирован и запущен. Поэтому, если фоновый сервис работает с конфигурацией Kestrel, то нужен способ дождаться его запуска внутри IHostedService.
Ожидание запуска Kestrel внутри IHostedService
В asp.net core начиная с версии 3.0 появился сервис, который позволяет получить уведомления о том, что приложение завершило запуск и начало обрабатывать запросы — это IHostApplicationLifetime.
CancellationToken даёт удобный механизм безопасного запуска колбеков при возникновении события:
Во-первых, благодаря этому мы можем дождаться запуска приложения. Но в ожидании запуска нам нужно обработать ситуацию, когда возникают проблемы с стартом — тогда приложение никогда не запустится, а метод, ожидающий старта не завершится. Чтобы это исправить достататочно ждать не только ApplicationStarted, но и обрабаывать событие для stoppingToken, приходящего в ExecuteAsync. Вот как будет выглядеть модифицированный пример фонового сервиса, который ожидает запуск приложения и корректно обрабатывает ошибки запуска:
Что кроме стандартных средств
Ещё одно популярное (57 миллионов скачиваний) решение для фоновых задач в дотнет — это Hangfire, библиотека для настройки, запуска и хранения фоновых задач с бесплатной версией для коммерческого использования, большим количеством настроек и отдельной админкой задач.