如何将 Hangfire 作业参数获取到该作业执行的方法中

How to get a Hangfire Job Parameter into the method that is executed by that job

我已经在 ASP.Net Core 5 Web 应用程序中安装了 Hangfire。 我遵循了 Hangfire 网站上的 Getting Started 指南,初始安装和配置很快并且有效。

因为它是一个多租户应用程序(每个租户一个数据库),当在服务器上处理作业时,我需要能够连接到正确的数据库。

我遇到了 据我所知,这正是我所需要的,但我不知道最后一步是如何从作业参数(租户标识)中获取值) 到 Hangfire 正在执行的方法中。

我已经创建了客户端过滤器

#region Hangfire Job Filters
#region Client filters
public class HfClientTenantFilter : IClientFilter
{
    private readonly IMultiTenant _multiTenant;
    public HfClientTenantFilter(IMultiTenant multiTenant)
    {
        _multiTenant = multiTenant;
    }
    public async void OnCreating(CreatingContext filterContext)
    {
        if (filterContext == null) throw new ArgumentNullException(nameof(filterContext));

        MdxTenantConfig tenantConfig = await _multiTenant.GetTenantConfig();
        filterContext.SetJobParameter("TenantId", tenantConfig.Id);
    }

    public void OnCreated(CreatedContext filterContext)
    {
        if (filterContext == null) throw new ArgumentNullException(nameof(filterContext));
    }
}

public class HfClientFilterProvider : IJobFilterProvider
{
    private readonly IMultiTenant _multiTenant;
    public HfClientFilterProvider(IMultiTenant multiTenant) {
        _multiTenant = multiTenant;
    }
    public IEnumerable<JobFilter> GetFilters(Job job)
    {
        return new JobFilter[]
        {
            new JobFilter(new CaptureCultureAttribute(), JobFilterScope.Global, null),
            new JobFilter(new HfClientTenantFilter(_multiTenant),JobFilterScope.Global, null)
        };
    }
}
#endregion Client filters

和服务器过滤器:

#region Server filters
public class HfServerTenantFilter : IServerFilter
{
    private readonly IHfTenantProvider _hfTenantProvider;
    public HfServerTenantFilter(IHfTenantProvider hfTenantProvider)
    {
        _hfTenantProvider = hfTenantProvider;
    }

    public void OnPerforming(PerformingContext filterContext)
    {
        if (filterContext == null) throw new ArgumentNullException(nameof(filterContext));

        var tenantId = filterContext.GetJobParameter<string>("TenantId");
        // need to get the tenantId passed to the method that calls the creation of the DbContext
        _hfTenantProvider.HfSetTenant(tenantId);
    }

    public void OnPerformed(PerformedContext filterContext)
    {
        if (filterContext == null) throw new ArgumentNullException(nameof(filterContext));
    }
}

public class HfServerFilterProvider : IJobFilterProvider
{
    private readonly IHfTenantProvider _hfTenantProvider;
    public HfServerFilterProvider(IHfTenantProvider hfTenantProvider)
    {
        _hfTenantProvider = hfTenantProvider;
    }
    public IEnumerable<JobFilter> GetFilters(Job job)
    {
        return new JobFilter[]
        {
            new JobFilter(new CaptureCultureAttribute(), JobFilterScope.Global, null),
            new JobFilter(new HfServerTenantFilter(_hfTenantProvider), JobFilterScope.Global,  null),
        };
    }
}
#endregion Server filters
#endregion Hangfire Job Filters

在启动中附加服务器过滤器

services.AddHangfireServer(opt => {
                opt.FilterProvider = new HfServerFilterProvider(new HfTenantProvider());  
});

在调试中执行时,我看到参数 TenantID 在客户端过滤器中正确设置,并且也被服务器过滤器正确检索。
我现在正在尝试使该值可用于正在执行但尚未成功的方法。
我尝试使用范围服务:

#region Scoped Tenant provider service

public interface IHfTenantProvider {
    void HfSetTenant(string TenantCode);
    string HfGetTenant();
}

public class HfTenantProvider: IHfTenantProvider
{
    public string HfTenantCode;

    public void HfSetTenant(string TenantCode)
    {
        HfTenantCode = TenantCode;
    }

    public string HfGetTenant()
    {
        return HfTenantCode;
    }
}
#endregion Scoped Tenant provider service

并在启动中:

services.AddScoped<IHfTenantProvider, HfTenantProvider>();

在服务器过滤器的 OnPerforming 方法中,我在 Scoped 服务中设置了从作业参数(完整代码见上文)中检索到的值,正如我在调试中看到的那样。

_hfTenantProvider.HfSetTenant(tenantId);

这是正在安排的测试方法:

public bool HfTest()
{
    try
    {
        BackgroundJobClient jobClient = GetJobClient();
        jobClient.Enqueue<MdxMetaCRUD>(crud => crud.HfTest());
    }
    catch (Exception)
    {
        return false;
    }
    return true;
}

这是我从 Scoped 服务中检索值的方法本身,但是 null:

public async Task HfTest()
{
    string tenant =_hfTenantProvider.HfGetTenant();
    using (var _dbContext = _contextHelper.CreateContext(true, tenant))
    {
        Entity entity = new()
        {
            Name = "HFtest",
            Description = tenant,
            DefaultAuditTrackRetention = 1
        };
        await _dbContext.Entities.AddAsync(entity);
    }
}

ContextHelper returns 一个新的 DbContext:

public ApplicationDbContext CreateContext(bool backgroundProcess, string backgroundTenant)
{
    return new ApplicationDbContext(_multiTenant, backgroundProcess, backgroundTenant);
}

DbContext 有一个重写来检索租户的连接字符串(在用户上下文中工作正常)。在 Hangfire 执行作业的情况下,我试图将 TenantId 传递给 _backgroundTenant

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    try
    {
        MdxTenantConfig tenantConfig = Task.Run(() => _multiTenant.GetTenantConfig(_backgroundProcess, _backgroundTenant)).Result;
        optionsBuilder.UseSqlServer(tenantConfig.ConnectionString);
        base.OnConfiguring(optionsBuilder);
    }
    catch //(Exception)
    {
        throw;
    }
}

我希望正在启动的作业(正在检索的服务器过滤器)和正在执行的作业在同一范围内(请求),但似乎不是。
我在范围服务上做错了什么还是我的方法错了?我看到一些文章提到了“工厂”,但这超出了我的知识范围(目前)。

您可以尝试像这样实现您的 HfTenantProvider :

public class HfTenantProvider : IHfTenantProvider
{
    private static System.Threading.AsyncLocal<string> HfTenantCode { get; } = new System.Threading.AsyncLocal<string>();

    public void HfSetTenant(string TenantCode)
    {
        HfTenantCode.Value = TenantCode;
    }

    public string HfGetTenant()
    {
        return HfTenantCode.Value;
    }
}

我同意这会使 HfTenantProvider 的范围界定有些无用。 此外,您可以在服务器过滤器的 OnPreformed 方法中调用 HfSetTenant(null)。我认为这更干净,尽管这应该不会有太大区别。

如上所述,@jbl 的答案是正确的,并且完全按照我在原始问题中的要求工作。我希望这对其他人有用。
然而,我找到了一个更简单的解决方案,其中我不需要作业参数或过滤器并且适合我的场景。正如我所见,很多人在多租户环境中实施 Hangfire 时都在寻求指导,我也发布了这个解决方案,希望它也能帮助到一些人。

在我的案例中,作业的调度是由用户通过 UI 完成的。这意味着我总是在安排工作时认识租户。
所以我只是将租户 ID 作为参数传递给计划的方法。
而不是将作业安排为:

backgroundClient.Enqueue<MyClass>(c => c.MyMethod(taskId)):

我将作业安排如下:

string tenantCode = _multiTenant.GetTenant();
backgroundClient.Enqueue<MyClass>(c => c.MyMethod(taskId, tenantCode)):

其中 _multiTenant 是我从 HttpContextAccessor 检索租户 ID 的实现。

要针对正确的数据库上下文执行该方法,我只需要在创建 DbContext 时传递 tenantId

public async Task MyMethod(int taskId, string tenantCode)
{
    using (var _dbContext = _contextHelper.CreateContext(true, tenantCode))
    {
        ...

我的 DbContext 的覆盖 OnConfiguring 然后可以直接获取租户配置(包括数据源字符串)。