2

Hangfire looks to be pretty slick. However, I am having a challenge getting Hangfire to activate the correct concrete class.

Some background:

public interface IJob
{
    bool Execute(string payload);
}
public interface IJobPayload
{
    string UserId { get; set; }
    string JobName { get; set; }
    string JobQueueName { get; set; }
    int Id { get; set; }
    JobType JobType { get; set; }
    CronExpression Cron { get; set; }
}

Now, I have (potentially) hundreds of Jobs that all inherit from IJob and execute on a payload that inherits from IJobPayload. Without getting into depth on each of the Job's Execution code, I have something like:

[Queue("critical")] class Job1 : IJob {...}
[Queue("doors")] class Job2 : IJob {...}
[Queue("doors")] class Job3 : IJob {...}
[Queue("lights")] class Job4 : IJob {...}
[Queue("lights")] class Job5 : IJob {...}
[Queue("adhoc")] class Job6 : IJob {...}
...
[Queue("critical")] class JobN : IJob {...}

To provide a sample of a basic Job:

public class JobDoorStatusChanged : IJob
{
    [Queue("doors")]
    public bool Execute(string payload)
    {
        var command = JsonConvert.DeserializeObject<Payload>(payload);
        // handle execution here...
        return true/false;
    }

    public class Payload : IJobPayload
    {
        public string UserId { get; set; }
        public string JobName { get; set; }
        public string JobQueueName { get; set; }
        public int Id { get; set; }
        public JobType JobType { get; set; }
        public CronExpression Cron { get; set; }
    }
}

I have a Web API Post controller that is very simple:

[HttpPost]
public string Post()
{
    var payload = Request.Content.ReadAsStringAsync().Result;
    var queueHandler = new QueueHandler();
    return queueHandler.Load(payload);
}

And the next step is where I am meeting failure. He and I are getting to be best buds. Unfortunately!

Hangfire has 4 Enqueue methods (2 sync, 2 async):

public static string Enqueue([NotNull, InstantHandle] Expression<Action> methodCall)
public static string Enqueue([NotNull, InstantHandle] Expression<Func<Task>> methodCall)
public static string Enqueue<T>([NotNull, InstantHandle] Expression<Action<T>> methodCall)
public static string Enqueue<T>([NotNull, InstantHandle] Expression<Func<T, Task>> methodCall)

They either take a static class:

var id = BackgroundJob.Enqueue(() => MyStaticJob.Execute(payload));

Or they take a type that should be resolvable from Unity:

var id = BackgroundJob.Enqueue<ConcreteJobDefinition>(a => a.Execute(payload));

Since I have dozens, scores even way too many objects which are all based on IJob neither of these entry points are working for me.

I even went so far as to try a single class to wrap all my jobs and have Hangfire execute the single class and so far, no luck:

public interface IJobService
{
    bool Execute(string payload);
}
public class JobService : IJobService
{
    private readonly IUnityContainer _container = UnityConfig.GetConfiguredContainer();
    public bool Execute(string payload)
    {
        var command = JsonConvert.DeserializeObject<PayloadStub>(payload);
        var job = _container.Resolve<IJob>(command.JobName);
        return job.Execute(payload);
    }
    internal class PayloadStub : IJobPayload
    {
        public string UserId { get; set; }
        public string JobName { get; set; }
        public string JobQueueName { get; set; }
        public int Id { get; set; }
        public JobType JobType { get; set; }
        public CronExpression Cron { get; set; }
    }
}

Now I can execute a single concrete implementation this way (talk about obfuscation!):

var id = BackgroundJob.Enqueue<JobService>(a => a.Execute(payload));

Still nothing! And, you lose the Queue Attribute from every single job!

So, I went back to my startup.cs file:

public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        Framework.Initialize(); // internal framework here at work...
        AreaRegistration.RegisterAllAreas();
        GlobalConfiguration.Configure(WebApiConfig.Register);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        UnityConfig.RegisterUnity();           // <== UNITY ==
        ConfigureAuth(app);
        HangfireConfig.RegisterHangfire(app);  // <== HANGFIRE ==
    }
}

Class UnityConfig:

public class UnityConfig
{
    public static void RegisterUnity()
    {
        var container = Sol3.Web.WebApi.App_Start.UnityConfig.GetConfiguredContainer();

        // Register Auth & Exception handlers...
        container.RegisterType<IAccessDeniedResult, AccessDeniedResult>();
        container.RegisterType<IExceptionResult, ExceptionResult>();
        container.RegisterType<IJobService, JobService>();

        // Register all IJob concrete implementations found in this project...
        container.RegisterTypes(
            AllClasses.FromLoadedAssemblies().Where(type => typeof(IJob).IsAssignableFrom(type) && type.IsClass),
            WithMappings.FromAllInterfaces,
            t => t.IsNested ? t.DeclaringType.Name + "." + t.Name : t.Name,
            WithLifetime.Transient);
    }
}

Class HangfireConfig:

public class HangfireConfig
{
    public static void RegisterHangfire(IAppBuilder app)
    {
        GlobalConfiguration.Configuration.UseSqlServerStorage(Globals.DatabaseHangfire);
        GlobalJobFilters.Filters.Add(new LogAttribute());
        app.UseHangfireDashboard();
        var options = new BackgroundJobServerOptions
        {
            Queues = Globals.QueueNames,
            Activator = new UnityJobActivator(Sol3.Web.WebApi.App_Start.UnityConfig.GetConfiguredContainer()),
        };
        app.UseHangfireServer(options);

    }
}

I don't see anything wrong there. Unity has all the registrations. Since I have so many objects based on a single interface I know I am breaking how Unity is usually used for Dependency Injection.

Any thoughts? Any tweaks to code that you can see?

TIA

Keith Barrows
  • 24,802
  • 26
  • 88
  • 134

3 Answers3

1

It looks like the developer found a bug and has fixed it. I am testing it out now and so far it looks more pronising than not.

Hangfire Issue #656

I also found an error with my definition. I needed to change my Execute from a bool to a void return type.

Keith Barrows
  • 24,802
  • 26
  • 88
  • 134
1

I had a similar issue and worked around it using a generic wrapper called JobHandler<T> that enqueues the job. It works like this:

First, a base JobHandler:

public abstract class JobHandler
{
    public abstract void Enqueue(string payload);
}

Next, JobHandler<T> which will handle enqueuing with Hangfire as the correct concrete type:

public abstract class JobHandler<T> : JobHandler 
    where T: IJob
{
    public override void Enqueue(string payload)
    {
        BackgroundJob.Enqueue<T>(x => x.Execute(payload));
    }
}

JobHandler<T> doesn't necessarily need to be abstract, but in my case I have additional logic performed by the handler and provide a concrete implementation of it for each job type. The handler for each type is then registered with my application and I can simply do:

JobHandler handler = Activator.CreateInstance(GetHandler("MyJobType")) as JobHandler;

handler.Enqueue(payload)

Should be easy enough to do the same using Unity to resolve the JobHandler instead. That said, I'm not sure this provides much benefit over simply resolving the IJob and queuing the instance now that the bug that was causing your problem has been fixed.

Lykaios
  • 73
  • 2
  • 6
0

While the core concept is slightly different, and there might be a cleaner way to do it, you might try registering the Jobs as an IEnumerable then pick which one you need based on name. I had a related issue in which I wanted to process N handlers for an implementation based on criteria passed in. I did a write up on it in my blog.

There is a Unity example in this Stack Overflow answer

Community
  • 1
  • 1
Kevin R.
  • 3,571
  • 1
  • 19
  • 29