Комментарии 24
Почему бы не сделать пару виртуальных методов:
"Task ExecuteIterationAsync(CancellationToken ct)" и
"void ExecuteIteration(CancellationToken ct)",
чтобы исключить все эти ограничения на то, как должен быть определён метод???
PS: Всё понял... тогда DI в методе не сделать
Да, Вы правы, это ограничение вызвано именно необходимостью дать возможность инъекции scoped-зависимостей. При проектировке конкретно этого момента я опирался на то, как это сделано для middleware в ASP.NET Core.
Важно! Автоматическая регистрация работает только для работ, определенных в той же сборке, что и код, вызвавший метод регистрации.А если сделать указание assembly в имени типа? Тогда по идее можно подгрузить эту assembly по необходимости. Т.е. использовать assembly qualified name, на подобие:
System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35
Вручную, разумеется, Вы можете зарегистрировать работу из любой сборки. Однако, в процитированном Вами предложении речь идет про автоматическую регистрацию, при которой библиотека сама ищет все типы, реализующие абстрактный класс WorkBase
.
Если есть необходимость искать автоматически в других сборках, можно добавить новую перегрузку для метода AddWorks
с возможностью указания сборок, в которых нужно искать типы.
Т.е. мы решаем проблему decouple worker от приложения. На момент создания приложения, мы не знаем что нам понадобится и потом в процессе эксплуатации, добавляем или заменяем воркеры на новые, без необходимости перекомпиляции и передеплоймента приложения, через конфигурацию.
Можно поиспользовать Assembly.Load
И наверное было бы ещё полезно, не столько загружать по конкретному имени типа, а сказать — вот у нас есть интерфейс, загрузи с этой ассембли, воркер реализующий этот интерфейс.
Есть ли какие-то киллер фичи, чтобы выбрать для использования вашу библиотеку, а не hangfire?
Hangfire намного более продвинутая библиотека, скорее даже целый фреймворк. Однако, по моему мнению, там много своего бойлерплейта, если мы не берем в расчет несерьезные примеры вроде создания работы из лямбда-выражения. В dotWork я старался решить одну конкретную задачу - уменьшение бойлерплейта, а весь остальной функционал основывается на стандартном для .NET классе BackgroundService
. Кроме того, могу ошибаться, но в hangfire вроде бы нельзя использовать scoped и transient сервисы со стандартным DI контейнером.
Если мне не изменяет память, то можно делать вот так:
public class ExampleWork
{
private readonly ISomeDependency _someDep;
public ExampleWork(ISomeDependency someDep){_someDep = someDep}
public async Task ExecuteIteration(CancellationToken ct)
{
await _someDep.DoWorkAsync(TimeSpan.FromSeconds(1), ct); // симулируем работу
Console.WriteLine("Work iteration finished!");
}
}
Далее регистрируем джобу:
BackgroundJob.Enqueue<ExampleWork>(x => x.ExecuteIteration(CancellationToken.None));
И в итоге каждый раз будет создаваться экземпляр ExampleWork и зависимости с любым временем жизни подойдут.
Спасибо, не знал, выглядит очень лаконично. Здесь хочу подчернуть несколько моментов:
если мы регистрируем много работ, то нужно дублировать указанный Вами код регистрации, в то время как dotWork позволяет зарегистрировать "все сразу" вызовом
AddWorks
;если нам потребуется получить настройки (а это достаточно частая необходимость, по крайней мере, по моему опыту), нужно будет в каждую работу инжектить свой
IOptions<MyOptType>
(или дажеIOptionsMonitor<MyOptType>
, чтобы поддерживать горячую перезагрузку), а если один тип используется несколькими работами - пользоваться именованными настройками... В общем, Вы меня поняли :)
Если же стандартных возможностей .NET не хватает, то можно и нужно использовать hangfire.
Важно! Автоматическая регистрация работает только для работ, определенных в той же сборке, что и код, вызвавший метод регистрации. Работы из других сборок придется регистрировать вручную.
Возможно https://github.com/khellang/Scrutor помог бы зарегестрировать работы определенные и в других подключенных сборках. Если конечно сами сборки не грузить ручками.
Спасибо за идею! Я с осторожностью отношусь к автоматическому сканированию всех сборок, потому что это может привести к регистрации "лишних" работ, а также преждевременной подргузке тех библиотек, которые еще не загружены в домен приложения. Мне больше импонирует вариант, предложенный чуть выше - разрешить пользователю самому указывать сборки, которые нужно просканировать на предмет наличия работ.
Scrutor позволяет указать какие сборки сканировать. Но с другой стороны все это у нас не что иное как Back, и как показывает мой опыт, первые же клиенты подключившиеся к сервису заставят прогрузится все сборки, так что я не вижу особой разницы в моменте их загрузки, при первом вызове или при сканировании. Более того второй вариант мне кажется предпочтительней. Хотя конечно мой опыт не перекрывает все возможные сценарии.
Ну и по поводу автоматического сканирования. Мой текущий проект содержит порядка полторы сотни различных сервисов, простыня из AddSingleton/AddTransient значительно перегружает конфигурацию. Я использую пометку атрибутом [SingletonService]/[TransientService], и сканированием регистрирую именно их. Очень удобно, и не надо волноваться забыл ты зарегестрировать или нет.
Более того, если сборку под проект делает другой человек, а мы их подключаем через нугет-репозиторий, то ему достаточно пометить атрибутом и его сервисы также будут зарегистрированы. Таким образом, если я использую какую либо зависимость, а она имеет еще 20-30 своих зависимостей, то простыню моих 150 сервисов расширять еще 30 не нужно.
Единственный момент, о сканировании надо помнить, потому как это не явная регистрация зависимостей.
Спасибо, интересно было узнать про регистрацию в крупных приложениях. Хочу отметить, что благодаря простоте dotWork совместима с любым способом регистрации зависимостей. По желанию разработчик может использовать стороннюю библиотеку, упрощающую регистрацию, и с большой долей вероятности dotWork будет с ней совместим.
в DI .net core можно если в конструктор передается класс с дефолтным конструктором в мэпинге не указывать?
или нужно партянки вот такие писать?
services.AddSingleton<АProvider>();
services.AddSingleton<BProvider>();
services.AddSingleton<DProvider>();
services.AddSingleton<CProvider>();
Такой возможности нет, все зависимости нужно явно регистрировать в контейнере. Спасибо за идею, мне очень нравится. Займусь, как будет время.
Если я правильно понял вопрос, то может помочь класс ActivatorUtilities. В нем определены методы, позволяющие конструировать сервисы, не зарегистрированные в IServiceProvider, при этом зависимости конструктора резолвятся из него. Если коротко, то он просто делает provider.GetService() для каждого параметра конструктора
Вы проводили какой-нибудь R&D? Например, есть Quartz.NET либа, которая покрывает перечисленные Вами в статье требования и предлагает даже больше.
Вы проводили какой-нибудь R&D?
Да, разумеется. Все решения, которые я нашел, включая Quartz.NET, привносят много своих концепций в разрабатываемое приложение. dotWork, напротив, сводих их число к минимуму, по сути своей являясь оберткой над BackgroundService
.
Quartz.NET либа, которая покрывает перечисленные Вами в статье требования и предлагает даже больше
И это отлично! Если я буду разрабатывать приложение, в котором будут специфичные требования, сложнореализуемые через BackgroundWorker
, я выберу Quartz.NET, или Hangfire, или любой другой фреймворк, который мне подойдет. Если же я захочу реализовать воркер, которому достаточно возможностей "сырого" .NET, то для уменьшения бойлерплейта выберу dotWork. Заметьте - не чтобы закрыть потребность в функционале, а именно чтобы потратить меньше кода на функционал, который уже имеется в .NET.
Мы еще такую либу использовали в проекте. Норм работала: https://docs.coravel.net/Installation/
Спасибо автору, интересный проект. Недавно использовал такие сервисы, понадобилось запускать по крону. Может быть имеет смысл тоже добавить такую фичу вместе/вместо delay
Worker Services в .NET