agilelabs-fx-docs main topics/workcontext.md

WorkContext

本页是 WorkContext 在线程、请求、Job 和多租户场景中的规范主入口。

适用场景

  • HTTP 请求中的 MVC / WebAPI。
  • BackgroundService、异步任务、并发 Task.Run
  • Hangfire Job、消息消费和多租户后台处理。

必须遵守

  • HTTP 请求中的逻辑使用框架自动创建的 WorkContext。
  • 后台任务、Job、并发 Task 必须显式创建新的 WorkContext Scope。
  • 新线程或 Task.Run 中使用 CreateScopeWithWorkContextForNewTask(),不要复用父线程上下文。
  • 当前用户、时区、语言、TraceId 统一从 WorkContext 获取。
  • 作用域结束后不缓存其中解析出的 ServiceProviderDbContext 或其他 Scoped 服务。
  • 依赖 SaveChanges() 的逻辑必须运行在有效 WorkContext 内。

推荐做法

  • 请求内:依赖 WebWorkContextInitMiddleware 自动初始化。
  • 后台线程:显式创建 Scope,再解析服务。
  • Hangfire:使用框架激活器或项目自定义 JobActivator,但都要明确生命周期管理。
  • 多租户:在子 Scope 内切换租户身份,不污染父作用域。
  • 需要排查链路时,为 WorkContext 显式命名,方便日志与 TraceTable 定位。

源码入口

  • AgileLabs.WebApp/WorkContexts/HttpWorkContextInitMiddleware.cs:HTTP 请求自动附着 WorkContext。
  • AgileLabs.WebApp/WorkContexts/Extensions/WorkContextScopeCreateExtensions.cs:手工创建 Scope 的主入口。
  • AgileLabs.WebApp/WorkContexts/DefaultWorkContextCoreFactory.cs:新建 WorkContext 并继承父属性。
  • AgileLabs.WebApp/WorkContexts/WorkContextScope.cs:释放当前上下文并恢复父上下文。
  • AgileLabs.WebApp/WorkContexts/IWorkContextAccessor.cs:保存当前线程或异步流里的上下文引用。

HTTP 请求生命周期

HTTP 请求里的 WorkContext 是自动建立的,不需要控制器自己创建。进入请求后,HttpWorkContextInitMiddleware 会调用 AttachWorkContextForCurrentScope();请求结束时,再调用 DisposeCurrentWorkContext() 释放。

sequenceDiagram
    participant Client as Client
    participant Middleware as HttpWorkContextInitMiddleware
    participant Accessor as IWorkContextAccessor
    participant Action as Controller/Service

    Client->>Middleware: HTTP Request
    Middleware->>Accessor: AttachWorkContextForCurrentScope()
    Accessor-->>Middleware: 当前 Scope 已附着 WorkContext
    Middleware->>Action: 执行业务逻辑
    Action-->>Middleware: 返回结果
    Middleware->>Accessor: DisposeCurrentWorkContext()
    Middleware-->>Client: Response

这也是为什么在 MVC / WebAPI 请求里,业务代码通常可以直接注入 IWorkContextCore,而不需要自己 new 一个上下文。

子 Scope、子线程和新任务的区别

框架没有把“子 Scope”都当成一种场景处理,而是显式区分:

场景 推荐入口 核心原因
同一请求内再开一个子 Scope CreateScopeWithWorkContext() 允许复用当前 Holder,并在 Dispose 时恢复父上下文
Task.Run / 新线程 CreateScopeWithWorkContextForNewTask() 强制创建新的 Holder,隔离父线程 AsyncLocal 值
当前上下文没有 WorkContext AgileLabContexts.Context.CreateScopeWithWorkContext() 从 Root 容器重新起一个完整上下文
Hangfire / 后台宿主 显式在 Job 执行边界创建 Scope 生命周期不能依赖 HTTP 请求自动初始化

父子 Scope 继承与恢复

DefaultWorkContextCoreFactory 会在创建子上下文时根据 inheritFlag 复制父上下文中的字段,常见会继承:

  • Items
  • Identity
  • LogTransId
  • CultureInfo
  • TimeZone

WorkContextScope.Dispose() 的职责有两层:

  1. 释放当前子 Scope 的 WorkContext 和 IServiceScope
  2. 如果当前 Holder 不是强制新建的,就把父 WorkContext 恢复回去。
sequenceDiagram
    participant Parent as Parent WorkContext
    participant Factory as DefaultWorkContextCoreFactory
    participant Scope as WorkContextScope
    participant Accessor as IWorkContextAccessor

    Parent->>Factory: CreateWorkContext(parent, inheritFlag)
    Factory->>Factory: 继承 Identity/Items/TimeZone 等属性
    Factory-->>Scope: 新 WorkContext + 新 IServiceScope
    Scope->>Accessor: SetContext(child)
    Note over Scope: 业务代码在 child 中运行
    Scope->>Accessor: DisposeCurrentWorkContext()
    Scope->>Accessor: SetContext(parent)

为什么不能跨线程复用 Scoped 服务

  • DbContext、Repository、当前用户信息这些对象都依赖当前 Scope 和当前 WorkContext。
  • 父线程的 Scoped 服务如果被子线程直接引用,会让上下文、连接和释放顺序失真。
  • 框架专门提供 CreateScopeWithWorkContextForNewTask(),就是为了让新任务拿到新的 IServiceScope 和新的 WorkContext Holder,而不是偷偷复用父线程状态。

最小用法示例:

public async Task RunAsync(IWorkContextCore workContext)
{
    _ = Task.Run(async () =>
    {
        using var scope = workContext.CreateScopeWithWorkContextForNewTask(scopeName: "SendMail");
        var mailService = scope.WorkContext.Resolve<IMailService>();
        await mailService.SendAsync();
    });
}

排障要点

  • 请求里拿不到 IWorkContextCore:先查 HttpWorkContextInitMiddleware 是否已进入请求管道。
  • Task.Run 中身份串线:检查是否误用了 CreateScopeWithWorkContext() 而不是 CreateScopeWithWorkContextForNewTask()
  • Scope 结束后对象还能继续被用:通常是业务代码缓存了 ServiceProvider、Repository 或 DbContext

常见坑

  • Task.Run 里直接继续使用请求中的 Scoped 服务。
  • 把父请求的 Items 当成天然线程安全对象。
  • 从 Root 容器直接取服务绕开当前 WorkContext。
  • 忘记释放手动创建的 Scope,造成上下文泄漏。

真实用例

  • niusys-webapi:后台逻辑显式 CreateScopeWithWorkContext()
  • gmandarin-backend:Hangfire JobActivatorScope 附着与释放 WorkContext。
  • woscm:后台服务与多租户嵌套 Scope。

相关页面