8

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.

grinay
  • 709
  • 1
  • 6
  • 21
  • 1
    This may help https://stackoverflow.com/a/57396553/1236044 – jbl Sep 26 '19 at 12:07
  • Thanks. This solve part of my question. I went further with it already. Hope to solve it nearest time and publish solution. There are few things related to DI, which I still struggling with. One of them is how to make filter work as scoped (or transient service) and pass dependencies.Currently I passing ServiceProvider into constructor. Most important thing that I'm trying to achieve is to create new Instance of(ICurrentUser) from job parameters to order dependent services using this instance in my background job. I implemented JobActivator for doing this, but it still doesn't work correct. – grinay Sep 26 '19 at 12:22

1 Answers1

11

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.

grinay
  • 709
  • 1
  • 6
  • 21
  • Hi, I realize this older but I have a question. I am attempting to do the same and pass tenantId into the jobfilter. However, I'm having some trouble discovering the service. In your above example BackgroundJobFilter.OnCreating, I assume that your ICurrentUser is a scoped or transient service. However, those services don't seem to be available within the _serviceProvider (my testing showing only singleton services showing up). How were you able to get around that? – Feech Oct 27 '20 at 14:22
  • I CurrentUser is scoped. Can you provide more details . Maybe that because i'm using aurofac. No sure. If you give me more details , i'll try to help. Where are you trying to resolve service? – grinay Oct 28 '20 at 14:48
  • Hi, thanks for your response. We found a way around the issue for now, and since we aren't using Autofac it may be difficult to get an apples to apples comparison. Thanks again! – Feech Nov 01 '20 at 21:11
  • Do you know by chance why SetJobParameter does not actually save the new parameter into DB? Can't make it work so far... Weird - it just stores it in filter context and that's all. – Alexander Jan 17 '21 at 23:22
  • 1
    Probably you didn't register your BackgroundJobFilter correctly , set breakpoint and make sure that your SetJobParameter actually called. – grinay Jan 20 '21 at 13:05
  • 1
    In my case, ICurrentUser was scoped and I was not getting data on the IClientFilter OnCreating method I ended up manually setting parameter for the job id like using (var connection = JobStorage.Current.GetConnection()) { connection.SetJobParameter(jobId, nameof(ICurrentUser), Newtonsoft.Json.JsonConvert.SerializeObject(CurrentUser)); } – Nithya May 20 '21 at 14:36
  • @grinay can you share your CurrentUser implementation? Thx! – DarkLeafyGreen Feb 16 '23 at 13:47
  • @DarkLeafyGreen added implementation example into the answer. – grinay Feb 25 '23 at 12:57