grinay
grinay

Reputation: 758

Hangfire run background job with user context

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.

Upvotes: 10

Views: 6200

Answers (2)

justaman
justaman

Reputation: 11

This instance contains state that cannot be serialized and deserialized on this platform.

I got this error when try to set user(ICurrentUser) into job parameters

Upvotes: 1

grinay
grinay

Reputation: 758

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));

public interface ICurrentUser
{
    string UserId { get; set; }
}


public class UserProvider : ICurrentUser
{
    private int? _userId;

    protected readonly IHttpContextAccessor HttpContextAccessor;


    public UserProvider(IHttpContextAccessor httpContextAccessor)
        {
            HttpContextAccessor = httpContextAccessor;
        }

    public virtual int? UserId =>
        HttpContextAccessor.HttpContext?.User?.Id() != null &&
        HttpContextAccessor.HttpContext?.User?.Id() != default(int)
            ? HttpContextAccessor.HttpContext?.User?.Id(): _userId;
} 

UserProvider is scoped service.

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.

Upvotes: 13

Related Questions