Loss of configurability with AspNetCore implementation

ioc-container
aspnetcore
job-filters
Tags: #<Tag:0x00007f6a0577c060> #<Tag:0x00007f6a0577bf20> #<Tag:0x00007f6a0577bde0>

#1

I’m trying to execute logic that will entail manipulating database records using Hangfire. I manage the transaction for this explicitly using a very basic implementation of a IUnitOfWork class that I have registered with the default resolver for ASPNET Core (I’m using MVC Core). However the JobActivatorScope that is created internally to the CoreBackgroundJobPerformer is created and subsequently disposed before I can do anything with it, regardless if the job was successful or failed.

After some digging I found that the JobActivatorScope.Current will have an instance to this scope, but control of the execution before this scope is disposed, because of the using statement, never gets handed outside of CoreBackgroundJobPerformer.Perform(PerformContext context). Is there any way to try and execute code (to commit or rollback my transaction) before the JobActivatorScope is disposed?

I’m currently using this exact pattern in an MVC 5 site, but I had implemented it by storing my own IoC scope inside of a ThreadStatic variable in a custom JobActivator and a custom JobFilter. Now that the scope is being implicitly created and used by Hangfire, I have much less control over how it’s used.


#2

I think the easiest way is to make your own JobActivator / JobActivatorScope subclasses (based on AspNetCoreJobActivator / Scope source code), and implement your logic inside JobActivatorScope.DisposeScope() before actually disposing the underlying IServiceScope.

Being inside JobActivatorScope, you theoretically have the JobActivatorContext (by passing it from JobActivator.BeginScope() to your newly created JobActivatorScope instance), so you may know the identifier of the job (JobActivatorContext.BackgroundJob.Id) and its parameters (JobActivatorContext.GetJobParameter / SetJobParameter), and also have access to the entire IStorageConnection.

On the other hand, it would be nearly impossible to access ProcessContext from there, so if you’re using PerformContext.Items to pass data between job filters, you’re out of luck.


#3

The problem is that I need to have transaction handling logic for when the scope is no longer needed. That is to say, I don’t really care about when/how the scope is created ( can deal with that in the services setup of the ASPNET Core site), I need to conditionally test if any exceptions were called within hangfire and either call Commit or Rollback on the transaction. When I first did this (not in ASPNET Core), I could test for exceptions on a JobFilterAttribute.OnPerformed(), since it’s PerformedContext had the information I needed, and I had managed the scope myself, and had access to it on the JobActivator. But OnPerformed() this is not called until after the scope has been disposed of already, so it’s too late. Also, calling a commit DB changes from inside of a dispose method is a BAD practice, Ideally I don’t want dispose calling rollback either, but that’s one I’ve had to concede to out of necessity. Dispose should be doing nothing other than closing/releasing resources. It should be Idempotent. DisposeScope is not called by Hangfire directly from the looking at the code anyway, it’s actually called from inside of the Dispose() method. So It’s not something I can expect to have any context of the job that just ran (successful or otherwise).

I took another look, and the only way I can see being able to do what I need is to either try to ignore the ASPNET Core implementation and use my old logic of custom the JobFilter/JobActivator for managing my IoC scope (this feels very hacky, and I haven’t figured out how to add JobFilterAttributes using the new ASPNET Core implementation), or to try and reimplement the IBackgroundJobPerformer using the current code as a base and inject the necessary logic/events I need into my custom version of it (less hacky, but this seems overkill/shotgun approach), I was really hoping there was some sort of event/method called in a finally block of the JobPerformer to give the developer a chance to do some scope cleanup task(s) in the JobActivator, but it doesn’t appear to be the case. I guess I’m more trying to determine if this is an actual limitation/intentional behavior, and if my exposing a way for me to do this will cause problems. But I don’t think I have a choice in order to get the behavior I need.


#4

Crap, and I can’t even implement my own BackgroundJobPerformer, too many of the classes used are marked as internal. I’d end up duplicating a LOT of classes just for the sake of being able to essentially add a call to let me do uow.Commit(), which is ridiculous.
I don’t know of anyway to get this to work using the ASPNET Core implementation for Hangfire. I may have to set it up all manually and keep the logic from a different project to override the behavior of the IoC service resolver to be done in my custom JobFilter/Activator. Which is going to be ugly given that I believe it depended upon Owin, which ASPNET Core, doesn’t use. It’s either that or drop Hangfire in favor of one that allows me to control the scope of the service providers. :-/


#5

Changing my question title now that I’ve had larger problems with configuration

So I thought I had a solution to this, but when trying to implement it I realized that the ASPNET Core implementation for Hangfire is hardcoded. As soon as you call services.AddHangfire() inside of of your Startup.ConfigureServices(), you’re forced to use AspNetCoreJobActivator, It will ignore anything else you have setup to override which Activator to use.

Ex.

public void ConfigureServices(IServiceCollection services)
{
    //.... Other setup for services
    services.AddHangfire(x =>
    {
        x.UseActivator(new MyCustomJobActivator());
    });
    //.... Other setup for services
}

//OR

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    //...other middleware setup
    app.UseHangfireServer(new BackgroundJobServerOptions()
    {
        Activator = new MyCustomJobActivator()
    });
    //...other middleware setup
}

The above still seems to result in Hangfire using the AspNetCoreJobActivator type, instead of the one specified using either method.

I even tried calling the older method GlobalConfiguration.Configuration.UseActivator(new MyCustomJobActivator()) with no success. So at the moment, short of cloning the code from the Hangfire.AspNetCore namespace, I can find zero way to customize the way the AspNetCore implementation works.

It’s also worth noting that while previous version of Hangfire used the ActivateJob method on the JobActivator class, the AspNetCore implementation just uses the Job activator as a JobActivatorScope factory, and nothing more (uses the Resolve() method on the JobActivatorScope instead).

All in all, a lot of breaking changes to the way Hangfire runs tasks.


#6

You might be interested in https://github.com/HangfireIO/Hangfire/pull/800. It allows to replace almost all internal components with the custom ones from IoC container.


#7

That’s a great direction to go, but I would point out that in the current implementation in the Hangfire.AspNetCore/HangfireServiceCollectionExtensions.cs file, the default behavior for setting up the resolution of IGlobalConfiguration in the AddHangfire() extension method is hardcoded, and will ignore whatever activator or logger you specify anywhere else in your configuration.


#8

Added some comments there.

The current implementation might ignore the activator/logger indeed, but the proposed implementation will always resolve the activator from the IoC container, so the default values stored in JobActivator.Current during AddHangfire() method call won’t break anything.


#9

Hello @nickalbrecht,

I do have the same issue as you, can you tell me how you ended up doing it ?
And also could you open source it ?

Thanks,

Yann


#10

I ended up doing a Filter that create the ISessionScope OnPerforming. I store this session scope in a ThreadStatic. I take this session scope in the ActivatorJob and use it in a ActivatorJobScope. I dispose this scope in the filter. And also do the commit in the filter.


#11

Hi @kYann,

could you share your solution?

Kind regards,
Jordi