Keith Barrows Keith Barrows - 27 days ago 10
C# Question

How to inject the correct concrete class into a closed sytstem

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

Answer

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.