Search code examples
c#dependency-injectionhangfire

Hangfire and Dependency Injection Scoped with Filters


I have a Hangfire job which uses a Service. This Service needs to connect at the Beginning and Close the Connection after the Job is done. I don't want to do it Manually inside each Job so i wrote a filter. The Problem is now that the DI of Hangfire or .net core two instances creates one for the Job and one for my Filter. My service is registered as scoped which is correct. I Also tried an Activator but nothing changed.

public class FileCleanerJob
{
    private IUnitofWork UnitOfWork { get; set; }

    public FileCleanerJob(IUnitofWork unitOfWork)
    {
        UnitOfWork = unitOfWork;
    }

    public void Run(PerformContext context)
    {
         var hash = UnitofWork.GetHashCode();
    }
}
public class HangfireSessionFilter : JobFilterAttribute, IServerFilter
{
    private readonly IUnitofWork UnitofWork;

    public HangfireSessionFilter(IUnitofWork unitofWork)
    {
        UnitofWork = unitofWork;
    }

    public void OnPerformed(PerformedContext filterContext)
    {
        UnitofWork.Commit();

    }

    public void OnPerforming(PerformingContext context)
    {
        var hash = UnitofWork.GetHashCode();
        UnitofWork.BeginTransaction();
    }

}
 public class ServiceJobActivator : JobActivator
{
    readonly IServiceScopeFactory _serviceScopeFactory;
    public ServiceJobActivator(IServiceScopeFactory serviceScopeFactory)
    {
        if (serviceScopeFactory == null)
        {
            throw new ArgumentNullException(nameof(serviceScopeFactory));
        }
        _serviceScopeFactory = serviceScopeFactory;
    }

    public override JobActivatorScope BeginScope(JobActivatorContext context)
    {
        return new ServiceJobActivatorScope(_serviceScopeFactory.CreateScope());
    }
}
public class ServiceJobActivatorScope : JobActivatorScope
{
    readonly IServiceScope _serviceScope;
    public ServiceJobActivatorScope(IServiceScope serviceScope)
    {
        if (serviceScope == null)
        {
            throw new ArgumentNullException(nameof(serviceScope));
        }
        _serviceScope = serviceScope;
    }
    public override object Resolve(Type type)
    {
        return ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, type);
        //return _serviceScope.ServiceProvider.GetService(type);
    }
}

Startup.cs

services.AddHangfire((provider, config) => {
    config.UseActivator(new ServiceJobActivator(provider.GetService<IServiceScopeFactory>()));
    config.UseFilter(new HangfireSessionAttribute(provider.GetService<IUnitofWork>()));
});

My guts says i inizalize HangfireSessionAttribute Wrong. But i couldn't find any other way of how to set it globally and still use scoped DI.


Solution

  • Let me try to turn on some lights here. In Hangfire the filters are instantiated by this class: https://github.com/HangfireIO/Hangfire/blob/7eb4b3fd56abe8eeed692265aff39e2d694e86df/src/Hangfire.Core/Common/ReflectedAttributeCache.cs#L52C114-L52C133

    They are created using the:

    memberInfo.GetCustomAttributes
    

    Which means there is no way, out of the box, to inject stuff into them!

    The closest thing I managed to do is the following:

    1. Create a custom JobActivator and register it
    2. Create a custom interface IScopeInitializationHook and register your "hooks"
    3. Once the scope is created, the custom JobActivator resolves the "hooks" and calls them one by one.
    public sealed class JobActivatorWithScopeHooks: AspNetCoreJobActivator {
    
      public JobActivatorWithScopeHooks(IServiceScopeFactory serviceScopeFactory): base(serviceScopeFactory) {
    
      }
    
      public override JobActivatorScope BeginScope(PerformContext context) {
        var scope = base.BeginScope(context);
    
        try {
          var hookTypes = GlobalScopeHooks.HookTypes;
          foreach(var hookType in hookTypes) {
            var hook = scope.Resolve(hookType) as IScopeInitializationHook;
    
            if (hook == null)
              throw new ArgumentException($"Hook of type {hookType.FullName} must implement {typeof(IScopeInitializationHook).FullName}");
    
            hook.OnScopeCreated(context, scope);
          }
        } catch {
          scope.Dispose();
          throw;
        }
    
        return scope;
      }
    }
    
    
    public static class GlobalScopeHooks {
      public static Type[] HookTypes {
        get;
        private set;
      } = new Type[0];
    
      public static void Set(params Type[] hookTypes) {
        ArgumentNullException.ThrowIfNull(hookTypes);
    
        if (HookTypes.Length > 0)
          throw new Exception("GlobalScopeHooks have already been initialized");
    
        foreach(var t in hookTypes)
        if (!typeof (IScopeInitializationHook).IsAssignableFrom(t))
          throw new ArgumentException($"Hook of type {t.FullName} must implement {typeof(IScopeInitializationHook).FullName}.");
    
        HookTypes = hookTypes;
      }
    }
    
    public interface IScopeInitializationHook {
      void OnScopeCreated(PerformContext context, JobActivatorScope scope);
    }
    
    public sealed class MySimpleHook: IScopeInitializationHook {
      MyDependencyClass ? _c;
    
      public InitJobExecutionContextHook(MyDependencyClass c) {
        _c = c;
      }
    
      public void OnScopeCreated(PerformContext context, JobActivatorScope scope) {
        ArgumentNullException.ThrowIfNull(context);
    
        //Do something here...
      }
    }
    

    Register your JobActivator and hooks:

    builder.Services.AddHangfire((serviceProvider, config) => {
      //Add your custom Hangfire configuration here...
    
      var scopeFactory = serviceProvider.GetService < IServiceScopeFactory > ();
      if (scopeFactory != null)
        config.UseActivator(
          new JobActivatorWithScopeHooks(
            scopeFactory
          )
        );
    });
    
    
    // Register your hooks
    GlobalScopeHooks.Set(
      typeof (MySimpleHook)
    );
    

    Remember to register your MySimpleHook class. Example:

    services.AddScoped<MySimpleHook>();