agilelabs-fx-docs main topics/workcontext/hangfire.md

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.WorkContextscope.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 是否能打开。

相关页面