Worker Services в .NET

от автора

Написание воркер-сервисов на .NET часто сопряжено с написанием большого количества повторяющегося boilerplate-кода. Однажды мне это надоело и я попытался упростить этот процесс, перенеся часть бойлерплейта в отдельную библиотеку, которой и посвящена эта статья.

Знакомство с библиотекой

Библиотека называется dotWork, у нее есть репозиторий на GitHub, а сборки выкладываются на NuGet. По большей части библиотека представляет собой обертку над BackgroundService, встроенным в .NET решением для написания воркеров.

Что конкретно упрощает dotWork?

Если вкратце, dotWork упрощает следующие аспекты разработки воркеров:

  • регистрацию работ (works);

  • регистрацию соответствующих работам настроек;

  • написание кода для повторения итераций.

В моей практике именно эти три момента требуют практически шаблонного кода и тем не менее не имеют возможности стандартизации «из коробки».

Установка

Для установки достаточно поставить NuGet пакет:

dotnet add package dotWork

Использование

Для использования библиотеки нам нужно создать один или несколько классов-работ (works), описывающих, что должен делать работник (worker). В контексте .NET работником выступает само приложение, либо хост, если приложение содержит несколько хостов.

Создание класса-работы

Для создания работы добавим в приложение новый класс, унаследованный от WorkBase:

public class ExampleWork : WorkBase<DefaultWorkOptions> { }

Теперь добавим метод, описывающий, из чего состоит работа. Работник будет вызывать его с определенной периодичностью, поэтому этот метод можно назвать итерацией:

public class ExampleWork : WorkBase<DefaultWorkOptions> {     public async Task ExecuteIteration(CancellationToken ct)     {         await Task.Delay(TimeSpan.FromSeconds(1), ct); // симулируем работу         Console.WriteLine("Work iteration finished!");     } } 

Важно! Чтобы dotWork нашла метод итерации, необходимо, чтобы он соответствовал следующим правилам:

  • имел название ExecuteIteration;

  • был помечен модификатором public;

  • возвращал void или Task.

Наша работа уже готова к использованию, но пока не делает ничего полезного. Чтобы сделать ее более полезной, мы можем внедрить в нее сервисы, то есть зависимости.

Внедрение зависимостей

dotWork позволяет внедрять зависимости двумя способами. Выбор конкретного способа зависит от того, как зависимость зарегистрирована в контейнере.

Singleton-зависимости

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

public class ExampleWork : WorkBase<DefaultWorkOptions> {     readonly SingletonService _singletonService;      public ExampleWork(SingletonService singletonService)     {         _singletonService = singletonService;     }      ... } 

Scoped и Transient зависимости

Часто возникает необходимость внедрить не-singleton зависимости в работу. Типичный пример — база данных. Поскольку работа живет до остановки хоста, то внедрение в нее коннектора базы данных может привести к тому, что соединение с БД будет открыто с самого начала и до самого конца работы программы. Это может быть нежелательно, например, если работа короткая и выполняется раз в длительный промежуток времени.

В таком случае, зависимость можно внедрить напрямую в метод итерации:

public async Task ExecuteIteration(ScopedService scopedService) {     await Task.Delay(TimeSpan.FromSeconds(1), ct);     Console.WriteLine("Work iteration finished!"); } 

Сервисы, внедренные таким образом, будут удалены после окончания итерации. dotWork создает новый scope на каждую итерацию.

Помимо сервисов, в метод итерации можно также внедрить CancellationToken. Он сработает, когда хост начнет остановку

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

Регистрация работ

dotWork позволяет зарегистрировать все работы сразу, регистрировать каждую работу отдельно, либо комбинировать оба подхода.

Регистрация одной работы

Зарегистрировать одну отдельную работу можно, добавив следующий вызов в метод ConfigureServices HostBuilder-a:

.ConfigureServices(services => {     services.AddWork<ExampleWork, DefaultWorkOptions>(); // <- наш метод }) 

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

На данном этапе наша работа полностью готова к запуску. Однако, если запустить ее, обнаружится, что итерация выполняется только один раз. Это нормально — настройка работы по-умолчанию (DefaultWorkOptions) устанавливает бесконечную задержку между итерациями. Мы можем изменить это, переопределив стандартную настройку:

services.AddWork<ExampleWork, DefaultWorkOptions>(configure: opt => {     opt.DelayBetweenIterationsInSeconds = 10; }); 

Теперь следующая итерация работы начнется через 10 секунд после окончания предыдущей.

Автоматическая регистрация работ

Вместо того, чтобы регистрировать каждую работу по-отдельности, мы можем зарегистрировать все работы сразу. Очевидно, что при этом у нас нет возможности настроить каждую работу, поэтому этот способ регистрации требует обязательного указания раздела конфигурации с настроками работ. Этот раздел должен представлять из себя словарь, где ключами являются названия классов работ. Легче всего продемонстрировать это на примере. Предcтавим, что у нас есть приложение с двумя работами:

  • ExampleWork1

  • ExampleWork2

Файл appSettings.json, в таком случае, может выглядеть следующим образом:

{     "Works": {         "ExampleWork1": {             "IsEnabled": false,             "DelayBetweenIterationsInSeconds": 86400 // 1 day         },         "ExampleWork2": {             "DelayBetweenIterationsInSeconds": 3600 // 1 hour         }     } } 

И мы можем зарегистрировать все наши работы одной строчкой кода:

.ConfigureServices((ctx, services) => {     services.AddWorks(ctx.Configuration.GetSection("Works")); }); 

Мы также можем переопределить регистрацию любой отдельно взятой работы, вызвав метод явной регистрации после автоматической:

.ConfigureServices((ctx, services) => {     var cfg = ctx.Configuration;     services.AddWorks(cfg.GetSection("Works"));     services.AddWork<ExampleWork1, DefaultWorkOptions>(configure: opt =>     {         opt.DelayBetweenIterationsInSeconds = 86400 * 2; // 2 days     }); }) 

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

Изменение класса настроек

Работа необязательно должна использовать DefaultWorkOptions в качестве настроек. Можно создать свой класс, унаследовав его от DefaultWorkOptions или даже напрямую от интерфейса IWorkOptions:

public class MyWorkOptions : DefaultWorkOptions {     public string MyProp { get; set; } }  public class Work_With_DefaultWorkOptions2 : WorkBase<DefaultWorkOptions2> {     public async Task ExecuteIteration()     {         Console.WriteLine("MyProp value is: " + Options.MyProp);     } } 

Заключение

Надеюсь, dotWork поможет сэкономить время при написании приложений-воркеров. Если после начала использования библиотеки Вы обнаружите, что в ней отсутствует какая-то важная ожидаемая функциональность, пожалуйста, откройте issue в репозитории GitHub.


ссылка на оригинал статьи https://habr.com/ru/articles/587518/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *