WorkContext
本页是 WorkContext 在线程、请求、Job 和多租户场景中的规范主入口。
适用场景
- HTTP 请求中的 MVC / WebAPI。
BackgroundService、异步任务、并发Task.Run。- Hangfire Job、消息消费和多租户后台处理。
必须遵守
- HTTP 请求中的逻辑使用框架自动创建的 WorkContext。
- 后台任务、Job、并发 Task 必须显式创建新的 WorkContext Scope。
- 新线程或
Task.Run中使用CreateScopeWithWorkContextForNewTask(),不要复用父线程上下文。 - 当前用户、时区、语言、TraceId 统一从 WorkContext 获取。
- 作用域结束后不缓存其中解析出的
ServiceProvider、DbContext或其他 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 复制父上下文中的字段,常见会继承:
ItemsIdentityLogTransIdCultureInfoTimeZone
而 WorkContextScope.Dispose() 的职责有两层:
- 释放当前子 Scope 的 WorkContext 和
IServiceScope。 - 如果当前 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。