Search code examples
asp.net-corehangfire

Hangfire per-job correlationId/state


I run Hangfire on ASP.NET Core. For our other projects we have CorrelationIds that we pass when making API calls to be able to link the caller and callee. We use the IHttpContextAccessor's TraceIdentifier for this in ASP.NET Core.

Unfortunately it looks like the trick used by ASP.NET Core to get a scoped CorrelationId in the Transient IHttpContextAccessor doesn't work for Hangfire job execution. Using a Scoped state correlation object doesn't work because it must be Transient to be able to work with the rest of the system (logging etc.)

I used to be able to get away using the ServiceLocator anti-pattern and resolve a scoped state object in a transient service. In the latest ASP.NET Core that is no longer supported and an exception is thrown making the system too slow because of the huge number of exceptions thrown.

Is there something that Hangfire provides already that would give me a unique ID per job execution?

Cheers.


Solution

  • Thanks to jbl's comment I looked at what I was doing again and managed to get it working through a kludge.

    I've got the transient state holder (basically it's the HttpContextAccessor class renamed):

    public class StateHolder
    {
        private static AsyncLocal<ContextHolder> _contextCurrent = new AsyncLocal<ContextHolder>();
    
        public string State {
            get {
                return _contextCurrent.Value?.Context;
            }
            set {
                var holder = _contextCurrent.Value;
                if (holder != null)
                {
                    holder.Context = null;
                }
    
                if (value != null)
                {
                    _contextCurrent.Value = new ContextHolder { Context = value };
                }
            }
        }
    
        private class ContextHolder
        {
            public string Context;
        }
    }
    

    and then in Hangfire I hook it up to the activation with

    public class LoggingActivator : JobActivator
    {
        private readonly IServiceScopeFactory _serviceScopeFactory;
        private readonly ContextAccessor _contextAccessor;
    
        public LoggingActivator([NotNull] IServiceScopeFactory serviceScopeFactory, ContextAccessor contextAccessor)
        {
            _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
            _contextAccessor = contextAccessor;
        }
    
        public override JobActivatorScope BeginScope(JobActivatorContext context)
        {
            return new LoggingActivatorScope(_serviceScopeFactory.CreateScope(), _contextAccessor);
        }
    }
    

    and

    public class LoggingActivatorScope : JobActivatorScope
    {
        private readonly IServiceScope _serviceScope;
        private readonly ContextAccessor _contextAccessor;
    
        public LoggingActivatorScope(
            [NotNull] IServiceScope serviceScope,
            ContextAccessor contextAccessor)
        {
            _serviceScope = serviceScope ?? throw new ArgumentNullException(nameof(serviceScope));
            _contextAccessor = contextAccessor;
        }
    
        public override object Resolve(Type type)
        {
            _contextAccessor.Context = Guid.NewGuid().ToString();
    
            return ActivatorUtilities.GetServiceOrCreateInstance(_serviceScope.ServiceProvider, type);
        }
    
        public override void DisposeScope()
        {
            _serviceScope.Dispose();
        }
    }
    

    That seems to work fine.