1

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.

R4cOOn
  • 2,340
  • 2
  • 30
  • 41
  • This approach may be an option : https://stackoverflow.com/a/57396553/1236044 It would be a way to flow the TraceId to the Job execution – jbl Feb 15 '22 at 08:28
  • Thanks. I've got working fine with a custom JobActivator. The proposed solution stil has the issue of scoped vs. transient service (the JobId will be held in the scoped service but I need it to be passed to a transient service). – R4cOOn Feb 15 '22 at 10:43
  • 1
    Most of the time I address this with the Service wrapping an AsyncLocal scoped object, something like this https://stackoverflow.com/a/69971515/1236044 But maybe I don't understand your question – jbl Feb 15 '22 at 11:02
  • Thanks, that got me in the right direction. I assumed it wasn't working but that was only because ASP.NET was setting the correlation at the wrong time. – R4cOOn Feb 15 '22 at 13:17

1 Answers1

0

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.

R4cOOn
  • 2,340
  • 2
  • 30
  • 41