Hangfire 中使用 WorkContext
本页说明 Hangfire Job 中如何使用 WorkContext。核心原则是:每次 Job 执行都必须有独立的 WorkContext Scope,Job 依赖应从该 Scope 解析,并在 Job 结束后释放。
场景
适用于:
- 使用
AgileLabs.WebApp.Hangfires接入 Hangfire Server。 - Job 内需要 Repository、
DbContext、应用服务、日志、审计字段、当前身份或时区。 - 项目自定义 Hangfire
JobActivator。 - Dashboard 与 Server 组合接入的宿主项目。
正确入口
框架提供内置 WorkContextJobActivator:
public class WorkContextJobActivator : JobActivator
{
public override JobActivatorScope BeginScope(JobActivatorContext context)
=> new WorkContextJobActivatorScope(
AgileLabContexts.Context.CreateScopeWithWorkContextForNewTask());
}
它的关键动作是:
- 在
BeginScope()中创建IWorkContextScope。 - 通过
CreateScopeWithWorkContextForNewTask()为 Job 创建独立 Holder。 - 在
Resolve(Type)中从WorkContextScope解析 Job 依赖。 - 在
DisposeScope()中释放IWorkContextScope。
配置 Hangfire
AgileLabs.WebApp.Hangfires 中的 AddAgileLabsHangfire(...) 会注册:
- Hangfire 配置。
WorkContextJobActivator。JobActivator到内置 WorkContext 激活器。- 可选 Hangfire Server。
Dashboard 使用:
app.UseAgileLabsHangfireDashboard(
pathMatch: "/jobs",
dashboardTitle: "Jobs",
authFunc: context => true);
实际项目应替换 authFunc,不要把 Dashboard 裸露给公网或未授权用户。
Job 中使用依赖
使用内置激活器后,Job 可以按常规构造函数注入 Scoped 服务,前提是这些服务最终在 Job Scope 内解析。
public class SyncOrdersJob
{
private readonly IWorkContextCore _workContext;
private readonly IOrderSyncService _syncService;
public SyncOrdersJob(IWorkContextCore workContext, IOrderSyncService syncService)
{
_workContext = workContext;
_syncService = syncService;
}
public async Task ExecuteAsync()
{
_workContext.SetName("SyncOrdersJob");
await _syncService.SyncAsync();
}
}
如果需要系统身份,可在 Job 开始时设置当前 Job Scope 的身份:
public async Task ExecuteAsync()
{
var setter = _workContext.Resolve<IWorkContextCoreSetter>();
setter.SetIdentity(new WorkContextIdentityInfo
{
Type = "System",
Id = "hangfire",
Name = "Hangfire",
IsAuthenticated = true
});
await _syncService.SyncAsync();
}
自定义 JobActivator 的最低要求
项目如果自定义 JobActivator,必须保持这些最低要求:
- 每次
BeginScope()创建一个新的IWorkContextScope。 - Job Scope 使用
CreateScopeWithWorkContextForNewTask(),不要复用 HTTP 请求上下文。 Resolve(Type)从scope.WorkContext或scope.WorkContext.ServiceProvider解析依赖。DisposeScope()必须释放IWorkContextScope。
推荐骨架:
public class ProjectJobActivator : JobActivator
{
public override JobActivatorScope BeginScope(JobActivatorContext context)
{
var scope = AgileLabContexts.Context.CreateScopeWithWorkContextForNewTask();
scope.WorkContext.SetName($"Hangfire:{context.BackgroundJob?.Job?.Type?.Name}");
return new ProjectJobActivatorScope(scope);
}
}
public class ProjectJobActivatorScope : JobActivatorScope
{
private readonly IWorkContextScope _scope;
public ProjectJobActivatorScope(IWorkContextScope scope)
{
_scope = scope;
}
public override object Resolve(Type type)
{
return _scope.WorkContext.Resolve(type);
}
public override void DisposeScope()
{
_scope.Dispose();
}
}
与 EF Core 审计字段的关系
AgileLabDbContext.SaveChanges() / SaveChangesAsync() 会读取当前 WorkContext 来补审计字段。如果 Job 没有 WorkContext,保存数据时可能抛出 workContext为null,请检查上下文环境。
因此 Job 中所有会保存数据的路径,都必须在 WorkContext Scope 内解析服务并执行。
错误用法
- JobActivator 直接从 RootServiceProvider 解析 Job。
- Job 中缓存 RootServiceProvider 或 Scoped Service。
- Job 开始时没有 WorkContext,却直接保存数据库。
- 自定义 JobActivator 创建了 Scope 但没有在
DisposeScope()中释放。 - 在 Job 内启动
Task.Run后继续使用 Job Scope 中的服务。
如果 Job 内再启动新 Task,请按 Task.Run 与新线程中使用 WorkContext 的规则重新创建 Scope。
排障
Job 中 IWorkContextCore 为空
检查:
- 是否使用了
AddAgileLabsHangfire(...)注册内置激活器。 - 是否被项目自定义
JobActivator覆盖。 - 自定义激活器是否在
BeginScope()中创建了 WorkContext Scope。
Job 保存数据库时报审计上下文为空
检查:
- DbContext 是否在 Job Scope 内解析。
- Job 是否绕过 WorkContext 从 RootServiceProvider 解析服务。
- 是否有 Job 内部新 Task 复用了 Job Scope 服务。
Dashboard 可访问但 Job 上下文异常
Dashboard 只是 Hangfire UI 入口。Job 是否拥有 WorkContext 取决于 Server 侧 JobActivator,不取决于 Dashboard 是否能打开。