Search code examples
domain-driven-designhangfireclean-architecturecancellation-token

How to wrap a Hangfire cancellation token?


I'm building up a .net core web app that requires background tasks to be run. To avoid using an external cron triggers we've decided to go with Hangfire; which is a beautiful package to use, does exactly whats needed then gets out of the way ;-)

To keep things clean I'm trying to stick to Uncle Bob's Clean architecture principles and seperate my ApplicationCore from the Infrastructure as much as possible. As Hangfire is an implementation detail it should ideally sit in the Infrastructure project, alongside database access, message queues, etc. with an Interface in the ApplicationCore, that my domain can use.

For the basic client running recurring and background jobs this has been fairly simple to do, and I've ended up with this as my Interface

namespace ApplicationCore.Interfaces
{
    using System;
    using System.Linq.Expressions;
    using System.Threading.Tasks;

    public interface IBackgroundJobClient
    {
        void AddOrUpdate<T>(
            string recurringJobId,
            Expression<Func<T, Task>> methodCall,
            string cronExpression);

         void RemoveIfExists(string recurringJobId);
    }
}

Implementation is a simple wrapper for these methods which uses RecurringJob

namespace Infrastructure.BackgroundJobs
{
    using System;
    using System.Linq.Expressions;
    using System.Threading.Tasks;
    using Hangfire;
    using IBackgroundJobClient = ApplicationCore.Interfaces.IBackgroundJobClient;

    public class HangfireBackgroundJobClient : IBackgroundJobClient
    {
        public void AddOrUpdate<T>(
            string recurringJobId,
            Expression<Func<T, Task>> methodCall,
            string cronExpression)
        {
            RecurringJob.AddOrUpdate<T>(recurringJobId, methodCall, cronExpression);
        }

        public void RemoveIfExists(string recurringJobId)
        {
            RecurringJob.RemoveIfExists(recurringJobId);
        }
    }

The issue that I have is needing to setup a RecurringJob with a supplied cancellationToken. However, I can't see an easy way to do this without exposing the underlying IJobCancellationToken and JobCancellationToken objects in my ApplicationCore code...?

What I've got at present is a wrapper for the JobCancellationToken in my Infrastructure.

namespace Infrastructure.BackgroundJobs
{
    public class BackgroundJobCancellationToken : JobCancellationToken
    {
        public BackgroundJobCancellationToken(bool canceled): base(canceled)
        {
        }
    }
}

An Interface in my ApplicationCore, which replicates the Hangfire one.

namespace ApplicationCore.Interfaces
{
    using System.Threading;

    public interface IJobCancellationToken
    {
        CancellationToken ShutdownToken { get; }

        void ThrowIfCancellationRequested();
    }
}

This is then used by the Method I want to execute as job, making use of the cancellationToken.ShutdownToken to pass into other methods requiring a cancellationToken.

public async Task GenerateSubmission(Guid SetupGuidId, IJobCancellationToken cancellationToken)
        {
            try
            {
                // Sort out the entities that we'll need
                var setup = await this.SetupRepository.GetByGuidIdAsync(SetupGuidId);

                var forecast = await this.GetCurrentForecastForSetup(setup, DateTime.UtcNow, cancellationToken.ShutdownToken);

                // Other Code

                }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }

Which in turn is enabled elsewhere by calling

    public async Task<Setup> EnableSetup(Setup setup)
    {
        setup.Enable();

        this.jobClient
            .AddOrUpdate<IForecastService>(
                setup.GuidId.ToString(),
                f => f.GenerateSubmission(setup.GuidId, null),
                "45 */2 * * *");

        await this.setupRepository.UpdateAsync(setup);

        return setup;
    }

This should be done with DomainEvents and Handlers, but one step at a time :-)

Is there a cleaner, better, easier way of doing this without taken a direct dependency on Hangfire in my ApplicationCore?

If the above setup works, I'll leave a comment on this question.


Solution

  • Since Hangfire 1.7 you no longer have to rely on IJobCancellationToken. You can simply use the standard .NET CancellationToken instead.