如何将 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
然后可以直接获取租户配置(包括数据源字符串)。
我已经在 ASP.Net Core 5 Web 应用程序中安装了 Hangfire。 我遵循了 Hangfire 网站上的 Getting Started 指南,初始安装和配置很快并且有效。
因为它是一个多租户应用程序(每个租户一个数据库),当在服务器上处理作业时,我需要能够连接到正确的数据库。
我遇到了
我已经创建了客户端过滤器
#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
然后可以直接获取租户配置(包括数据源字符串)。