Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Injecting Simple Injector components into IHostedService with ASP.NET Core 2.0

In ASP.NET Core 2.0, there is a way to add background tasks by implementing the IHostedService interface (see https://learn.microsoft.com/en-us/aspnet/core/fundamentals/hosted-services?view=aspnetcore-2.0). By following this tutorial, the way I was able to get it working was by registering it in the ASP.NET Core container. My goal is to be reading messages from a queue and processing the jobs in the background; a message is posted to the queue (via controller action) and then processed in the background on a timed interval.

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

When I put this registration in the ASP.NET Core container, it kicks off the process automatically on application startup. However, when I register this in SimpleInjector, the service is not automatically started. I believe this is the case because we only register the SimpleInjector container with the MvcControllers and MvcViewComponents:

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

The problem I run into is when I want to start injecting components register from SimpleInjector (e.g. Repositories, generic handlers with decorators...) into an implementation of IHostedService as demonstrated below:

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

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

Since IHostedService is registered with ASP.NET Core and not Simple Injector, I receive the following error when running the timed background service:

Unhandled Exception: System.InvalidOperationException: Unable to resolve service for type 'Optimization.Core.Interfaces.IJobRepository' while attempting to activate 'Optimization.API.BackgroundServices.TimedService'.

So my question is, what is the best way to implement background tasks in Simple Injector? Does this require a separate integration package than the standard MVC integration? How am I able to inject my Simple Injector registrations into the IHostedService? If we could automatically start the service after being registered in Simple Injector, I think that would solve this problem.

Thank you for any pointers here and for any advice on this topic! I could be doing something wrong. I have really enjoyed using Simple Injector for the past year.

like image 871
chrisbuttacavoli Avatar asked May 17 '18 15:05

chrisbuttacavoli


2 Answers

There are multiple ways to approach this. The simplest way is probably to cross-wire the hosted service in such way that the built-in configuration system resolves the hosted service from 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>());

Do note that hosted services are resolved just once, and cached for ever, effectively making them Singletons. This is why you should register it in Simple Injector as Singleton.

Concequence of this, however, is that you won't be able to inject any Scoped or Transient dependencies into your hosted service. On top of that, it forces you to let your application component (TimedService) have a dependency on an ASP.NET Core abstraction (IHostedService). This is not ideal.

My preferred approach, therefore, is to instead create an adapter implementation that you register with the ASP.NET Core configuration system that forwards the calls to Simple Injector while using an application-specific abstraction to implement your service. So instead creating many IHostedService implementations, you define an abstraction that is specific and ideal to your application. Let's call this abstraction IMyJob.

The IHostedService adapter implementation might look like this:

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();
}

You can register it in ASP.NET core as follows:

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

This way the actual jobs that you run can stay oblivious to ASP.NET Core and can be defined as follows:

public class CoolJob : IMyJob
{
    private readonly IJobRepository repo;

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

    public void DoWork() => ...
}

And all jobs can be registered in Simple Injector as follows:

 // NOTE: Simple Injector v4.3 API
container.Collection.Register<IMyJob>(typeof(CoolJob).Assembly);
like image 135
Steven Avatar answered Nov 15 '22 04:11

Steven


I would hook onto the ConfigureContainer method of the HostBuilder and setup simpleinjectore there like this:

                   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();
        }

While you could use your IHostedService implementation indeed I think it may hide what is going on. I believe the infrastructure bootstrapping should be done in one place or orchestrated at least in one place. I consider the container to be infrastructure and would set it all up with the rest of the app via the HostBuilder methods.

An added advantage may also be that you do not entirely replace the ServiceCollection as it works well with doing other framework related things. An example of some stuff I would still do with the ServiceCollection:

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

This is in line with what is stated in the simpleinjector docs about setting the container up with ASP.NET Core:

The practice with Simple Injector is to use Simple Injector to build up object graphs of your application components and let the built-in container build framework and third-party components,The practice with Simple Injector is to use Simple Injector to build up object graphs of your application components and let the built-in container build framework and third-party components

The same should apply with just .net core and the generic HostBuilder.

like image 45
Thulani Chivandikwa Avatar answered Nov 15 '22 04:11

Thulani Chivandikwa