Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hangfire run background job with user context

Tags:

hangfire

I have an app, with multi-tenancy. I want to create background job under user context, but I can't find good way to implement that. I'll explain a bit my architecture. I'm using Interface ICurrentUser that contain UserID. In Startup class I register as scoped in IoC the class WebUser which implements ICurrentUser, this class getting HttpContext and extract user details from claims.

I'm executing background job and the ICurrentUser.UserID is null as expected because hangfire doesn't have any httpcontext.

I'm solving this problem by creating my background tasks with method which accept ICurrentUser as first argument, then inside method body, I set my "CurrentUser" for UnitOfWork (and AppServices) and start executing task, the problem with this approach that I have to repeat this code with every background task and pass CurrentUser into it.

My question how can achieve next thing. Or maybe you can suggest other solutions for it.

  1. How can I pass my CurrentUser into JobActivator, to order I can setup user context before all services is resolved.

For Example it may look like that:

BackgroundJob.Enqueue<MySvc>(UserContext, mysvc=>mysvc.Run());

I read sources and really didn't find any extension points to implement this.

Any help is greatly appreciated.

like image 541
grinay Avatar asked Sep 25 '19 15:09

grinay


1 Answers

Finally, I finished up with almost the same solution that @jbl suggested. I've created a filter which stores my current user into the job parameters.

public class BackgroundJobFilter : JobFilterAttribute, IClientFilter, IApplyStateFilter
{
    private readonly IServiceProvider _serviceProvider;

    public BackgroundJobFilter(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public void OnCreating(CreatingContext filterContext)
    {
        var currentUser = _serviceProvider.GetRequiredService<ICurrentUser>();
        filterContext.SetJobParameter(nameof(ICurrentUser), currentUser);
    }
}

Then add filter into Hangfire

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    GlobalConfiguration.Configuration.UseFilter(new BackgroundJobFilter(app.ApplicationServices));
}

Then I've replaced current job activator

    internal class ServiceJobActivatorScope : JobActivatorScope
    {
        private readonly IServiceScope _serviceScope;

        public ServiceJobActivatorScope([NotNull] IServiceScope serviceScope)
        {
            if (serviceScope == null)
                throw new ArgumentNullException(nameof(serviceScope));

            _serviceScope = serviceScope;
        }

        public override object Resolve(Type type)
        {
            return ActivatorUtilities.GetServiceOrCreateInstance(_serviceScope.ServiceProvider, type);
        }

        public override void DisposeScope()
        {
            _serviceScope.Dispose();
        }
    }

And finally, set current user details (which is null on the moment of running task)

  public class CustomJobActivator : JobActivator
    {
        private readonly IServiceScopeFactory _serviceScopeFactory;
        private readonly IMapper _objectMapper;


        public CustomJobActivator([NotNull] IServiceScopeFactory serviceScopeFactory, IMapper objectMapper)
        {
            if (serviceScopeFactory == null)
                throw new ArgumentNullException(nameof(serviceScopeFactory));

            _serviceScopeFactory = serviceScopeFactory;
            _objectMapper = objectMapper;
        }

        public override JobActivatorScope BeginScope(JobActivatorContext context)
        {
            var user = context.GetJobParameter<WebUser>(nameof(ICurrentUser));

            var serviceScope = _serviceScopeFactory.CreateScope();

            var currentUser = serviceScope.ServiceProvider.GetRequiredService<ICurrentUser>();
            //Copy value from user to currentUser
            _objectMapper.Map(user, currentUser);

            return new ServiceJobActivatorScope(serviceScope);
        }
    }

Then replace the existing JobActivator in container

services.Replace(new ServiceDescriptor(typeof(JobActivator), typeof(CustomJobActivator), ServiceLifetime.Scoped));

After that when services start resolving from this scope they will get user context and all filter in DbContext and other places when I use ICurrentUser works properly.

like image 86
grinay Avatar answered Sep 23 '22 20:09

grinay