Внедрение простых компонентов инжектора в IHostedService с помощью ASP.NET Core 2.0

В ASP.NET Core 2.0 есть способ добавить фоновые задачи путем реализации интерфейса IHostedService (см. Https://docs.microsoft.com/en-us/aspnet/core/fundamentals/hosted-services?view=aspnetcore- 2.0). Следуя этому руководству, я смог заставить его работать, зарегистрировав его в контейнере ASP.NET Core. Моя цель - читать сообщения из очереди и обрабатывать задания в фоновом режиме; сообщение отправляется в очередь (через действие контроллера), а затем обрабатывается в фоновом режиме через определенный интервал времени.

// Not registered in SimpleInjector
services.AddSingleton<IHostedService, MyTimedService>(); 

Когда я помещаю эту регистрацию в контейнер ASP.NET Core, она автоматически запускает процесс при запуске приложения. Однако, когда я регистрирую это в SimpleInjector, сервис не запускается автоматически. Я полагаю, что это так, потому что мы регистрируем контейнер SimpleInjector только с MvcControllers и MvcViewComponents:

// Wire up simple injector to the MVC components
container.RegisterMvcControllers(app);
container.RegisterMvcViewComponents(app);

Проблема, с которой я сталкиваюсь, заключается в том, что я хочу начать внедрение компонентов реестра из SimpleInjector (например, репозитории, универсальные обработчики с декораторами...) в реализацию IHostedService, как показано ниже:

public class TimedService : IHostedService, IDisposable
{
    private IJobRepository _repo;
    private Timer _timer;

    public TimedService(IJobRepository repo)
    {
        this._repo = repo;
    }
    ...
    ...
    ...
}

Так как IHostedService зарегистрирован в ASP.NET Core, а не в Simple Injector, я получаю следующую ошибку при запуске синхронизированной фоновой службы:

Необработанное исключение: System.InvalidOperationException: невозможно разрешить службу для типа "Optimization.Core.Interfaces.IJobRepository" при попытке активировать "Optimization.API.BackgroundServices.TimedService".

Итак, мой вопрос: как лучше всего реализовать фоновые задачи в Simple Injector? Требует ли это отдельного пакета интеграции, чем стандартная интеграция MVC? Как я могу ввести свои регистрации Simple Injector в IHostedService? Если бы мы могли автоматически запустить службу после регистрации в Simple Injector, я думаю, что это решило бы эту проблему.

Спасибо за любые ссылки здесь и за любые советы на эту тему! Я мог бы сделать что-то не так. Мне очень понравилось использовать Simple Injector в прошлом году.

Ответ 1

Есть несколько способов приблизиться к этому. Самый простой способ, вероятно, состоит в том, чтобы перекрестным образом подключить размещенный сервис таким образом, чтобы встроенная система конфигурации разрешала размещенный сервис из Simple Injector:

// Register in Simple Injector as Singleton
container.RegisterSingleton<THostedService>();

// Cross-wire TimedService in the built-in configuration system
services.AddSingleton<IHostedService>(
    c => container.GetInstance<TimedService>());

Обратите внимание, что размещенные сервисы разрешаются только один раз и кешируются навсегда, что делает их одиночными. Вот почему вы должны зарегистрировать его в Simple Injector как Singleton.

Следствием этого, однако, является то, что вы не сможете внедрить какие-либо зависимости Scoped или Transient в вашу размещенную службу. Кроме того, он заставляет ваш компонент приложения (TimedService) зависеть от абстракции ядра ASP.NET(IHostedService). Это не идеально.

Поэтому мой предпочтительный подход состоит в том, чтобы вместо этого создать реализацию адаптера, которую вы регистрируете в системе конфигурации ASP.NET Core, которая перенаправляет вызовы Simple Injector, используя абстракцию для конкретного приложения для реализации вашего сервиса. Таким образом, вместо создания многих реализаций IHostedService, вы определяете абстракцию, которая является специфической и идеальной для вашего приложения. Пусть эта абстракция IMyJob.

IHostedService адаптера IHostedService может выглядеть следующим образом:

public class SimpleInjectorJobProcessorHostedService : IHostedService, IDisposable
{
    private readonly Container container;
    private Timer timer;

    public SimpleInjectorJobProcessorHostedService(Container c) => this.container = c;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        this.timer = new Timer(this.DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        // Run operation in a scope
        using (AsyncScopedLifestyle.BeginScope(this.container))
        {
            // Resolve the collection of IMyJob implementations
            foreach (var service in this.container.GetAllInstances<IMyJob>())
            {
                service.DoWork();
            }
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        this.timer?.Change(Timeout.Infinite, 0);
        return Task.CompletedTask;
    }

    public void Dispose() => this.timer?.Dispose();
}

Вы можете зарегистрировать его в ядре ASP.NET следующим образом:

services.AddSingleton<IHostedService>(
    new SimpleInjectorJobProcessorHostedService(container)); 

Таким образом, фактические задания, которые вы запускаете, могут остаться незамеченными для ASP.NET Core и могут быть определены следующим образом:

public class CoolJob : IMyJob
{
    private readonly IJobRepository repo;

    public CoolJob(IJobRepository repo) => this.repo = repo;

    public void DoWork() => ...
}

И все задания могут быть зарегистрированы в Simple Injector следующим образом:

 // NOTE: Simple Injector v4.3 API
container.Collection.Register<IMyJob>(typeof(CoolJob).Assembly);

Ответ 2

Я бы подключился к методу ConfigureContainer HostBuilder и установил туда simpleinjectore следующим образом:

                   IHostBuilder()
                   .ConfigureContainer<ServiceCollection>((builder, services) =>
                   {
                       var container = new Container();

                       container.RegisterSingleton<IJobRepository, JobRepository>();
                       services.AddTransient<IHostedService, TimedService>();

                   })
                   .ConfigureServices((hostContext, services) =>
                   {
                       // Originally we would have done this
                       //services.AddHostedService<Service>();
                   })
                   .Build();

        using (host)
        {
            await host.StartAsync();
            await host.WaitForShutdownAsync();
        }

Хотя вы действительно можете использовать свою реализацию IHostedService, я думаю, что она может скрыть то, что происходит. Я считаю, что начальную загрузку инфраструктуры следует проводить в одном месте или организовывать хотя бы в одном месте. Я считаю, что контейнер является инфраструктурой и настроил бы все это вместе с остальной частью приложения с помощью методов HostBuilder.

Дополнительным преимуществом также может быть то, что вы не полностью заменяете ServiceCollection, поскольку она хорошо работает с другими вещами, связанными с платформой. Пример некоторых вещей, которые я все еще делал бы с ServiceCollection:

                   IHostBuilder()
                   .ConfigureServices((hostContext, services) =>
                   {
                       services.AddLogging();
                       services.AddOptions();
                   })

Это соответствует тому, что указано в документах simpleinjector о настройке контейнера с помощью ASP.NET Core:

Практика с Простым Инжектором состоит в том, чтобы использовать Простой Инжектор для создания графов объектов ваших компонентов приложения и позволить встроенной структуре построения контейнера и сторонним компонентам. Практика с Простым Инжектором состоит в том, чтобы использовать Простой Инжектор для построения графов объектов. компоненты вашего приложения и пусть встроенный контейнер строит каркас и сторонние компоненты

То же самое должно применяться только к ядру .net и универсальному HostBuilder.