Написание воркер-сервисов на .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/
Добавить комментарий