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

3
我已经在一个ASP.Net Core 5的Web应用中安装了Hangfire,并按照Hangfire网站上的入门指南进行了操作,初始安装和配置很快就完成了,并且可以正常工作。
由于这是一个多租户应用程序(每个租户一个数据库),因此我需要在服务器处理作业时能够连接到正确的数据库。
我看到了这篇非常详细的文章,正是我需要的,但我无法弄清楚最后一步是如何将作业参数(租户标识)的值传递给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方法中,我将从作业参数中检索到的值设置为作用域服务中的值(完整代码请参见上文),在调试中可以看到这个方法是有效的。

_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 返回一个新的 DbContext:

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

DbContext有一个覆盖方法,用于检索租户的连接字符串(在用户上下文中运行良好)。如果由Hangfire执行的作业,我正在尝试将传递到_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;
    }
}

我原以为(服务器过滤器被检索时)启动的工作和执行的工作在同一范围内(请求),但事实并非如此。
我是否对作用域服务做错了什么或者我的方法是错误的?我看到有些文章提到了“工厂”,但那超出了我的知识范围(目前为止)。

2个回答

2
如上所述,@jbl的答案是正确的,并且完全符合我在原始问题中提出的要求。希望这对其他人有所帮助。
然而,我找到了一种更简单的解决方案,我不需要作业参数或过滤器,它适用于我的场景。由于我看到很多人在实现Hangfire时都在寻求指导,特此发布此解决方案,希望能够帮助一些人。
在我的情况下,作业的调度是通过用户界面完成的。这意味着当作业被调度时,我总是知道租户。
因此,我只需将租户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方法可以直接获取租户配置(其中包括数据源字符串)。

1
你可以尝试像这样实现你的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)。我认为这更加简洁,尽管这不会有太大的区别。


1
谢谢,这个完美地解决了问题! 与此同时,我已经找到了一个更简单的解决方案(适用于我的情况),它不涉及任何作业参数/过滤器。我也会发布它,以供其他人寻找解决方案,但我会接受您的答案,因为它是我最初问题的正确解决方案。 我会阅读一些内容,以充分理解您的解决方案 :) - RJD

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接