Search code examples
c#asp.netasp.net-coredependency-injectionhangfire

Using different sub-class services in dependency injection (in Hangfire task-scheduler)


I am setting up a TaskScheduler using the Hangfire library. The tasks are registered as a service now. There are many different jobs/tasks. In the service interface there is a reference to each Task-implemetation like this:

    using System.Threading.Tasks;
    namespace Domain.Interfaces
    {
        public interface IService
        {
            Task DoStuff1();
            Task DoStuff2();
            //etc..
        }
    }

This service is registered in the Startup class like:

    public void ConfigureServices(IServiceCollection services)
    {
         services.AddTransient<IService, Service>();
    }

Problem is now I have to have all of the code for each task in the service class. I would rather just have 1 reference in the service interface like:

    using System.Threading.Tasks;
    namespace Domain.Interfaces
    {
        public interface IService
        {
            Task RunTask();
        }
    }

RunTask() would be overridden with actual work to be done in each separate child class that inherits from Service (and thus implements IService) That way I can keep all of my task-code nice and separate but the call to start these tasks by a generic function call like

      _service.RunTask();

Registering all different tasks as separate services is also not a good option. I dont want:

    public void ConfigureServices(IServiceCollection services)
    {
         services.AddTransient<IService, Task1Service>();
         services.AddTransient<IService, Task2Service>();
         services.AddTransient<IService, Task3Service>();
         //etcetc..
    }

In Hangfire these are registered as recurring jobs like:

    RecurringJob.AddOrUpdate<Worker>(system, w => w.DoStuff1(),appSettings.AuthInterval, TimeZoneInfo.Local);
    RecurringJob.AddOrUpdate<Worker>(system, w => w.DoStuff2(),appSettings.AuthInterval, TimeZoneInfo.Local);

And the Worker class will execute the tasks using the injected IService like:

     public async Task DoStuff1()
    {
        await _service.DoStuff1();
        TextBuffer.WriteLine("DoStuff 1 was completed");
    }

I want to keep this structure in general, but I would want to specify which service would actually get injected so that Worker would only have to include:

     public async Task DoStuff()
    {
        await _service.RunTask(); //would run the correct task based on the service injected ??
    }

How should one best go about this? I am relatively new to the Dependecy Injection concept, but think I have a basic understanding. I mainly want to:

  1. limit the amount of services that will need to be registered in startup.
  2. split Tasks as separate classes for better project structure
  3. otherwise keep the general Dependency Injection structure already implemeted.

Thanks!


Solution

  • You can compose the classes for the individual services the same as if you weren't using Hangfire. If two services do different things then they should probably be separate classes.

    You can register them as recurring jobs according to their specific types:

    RecurringJob.AddOrUpdate<SomeService>(
        system, 
        s => s.DoWhateverThisServiceDoes(),
        appSettings.AuthInterval, 
        TimeZoneInfo.Local);
    
    RecurringJob.AddOrUpdate<OtherService>(
        system, 
        o => o.DoOtherThing(),
        appSettings.AuthInterval, 
        TimeZoneInfo.Local);
    

    What if you decide that you don't want these two services to run as separate tasks, but rather as part of a single task? You can do that too:

    public class CompositeService
    {
        private readonly SomeService _someService;
        private readonly OtherService _otherService;
    
        public CompositeService(SomeService someService, OtherService otherService)
        {
            _someService = someService;
            _otherService = otherService;
        }
    
        public async Task DoCompositeThing()
        {
            await Task.WhenAll(
                _someService.DoWhateverThisServiceDoes(),
                _otherService.DoOtherThing());
        }
    }
    
    RecurringJob.AddOrUpdate<CompositeService>(
        system, 
        s => s.DoCompositeThing(),
        appSettings.AuthInterval, 
        TimeZoneInfo.Local);
    

    A key point is that whether you decide to register them separately or create one task that executes both, you don't need to change how you write the individual classes. To decide whether to make them one class or separate classes you can apply the Single Responsibility Principle, and also consider what will make them easier to understand and unit test. Personally I'd lean toward writing smaller, separate classes.

    Another factor is that the individual classes might have their own dependencies, like this:

    public class SomeService
    {
        private readonly IDependOnThis _dependency;
    
        public SomeService(IDependOnThis dependency)
        {
            _dependency = dependency;
        }
    
        public async Task DoWhateverThisServiceDoes()
        {
            // uses _dependency for something
        }
    }
    

    This is more manageable if you keep classes separate. If they're all in one class and different methods depend on different things, you could end up with lots of unrelated dependencies and method crammed into one class. There's no need to do that. Even if we want all of the functionality "orchestrated" by one class, we can still write it as separate classes and then use another class to bring them together.

    That's one of the great things about the pattern of dependency injection. It limits the amount of code we need to read and understand at one time. One class might have a dependency or multiple dependencies. When you're looking at that class you don't have to think about whether those dependencies have dependencies of their own. There could be level upon level of nested dependencies. But when we're looking at one class all we have to think about is what it does and how it uses its dependencies. (That's also why it makes unit testing easier.)

    However many classes you create, you'll need to register them all with the IServiceCollection, along with their dependencies. You had mentioned that you don't want to register multiple services, but that's normal. If it gets large and out-of-control there are ways to break it up.