WorkContext
本目录是 AgileLabs Framework 中 WorkContext 的独立专题入口,用来帮助项目理解、接入和排查 WorkContext。它覆盖 WorkContext 的产生背景、核心设计、生命周期、接口能力、继承策略、自定义方式和典型使用场景。
如果只记一句话:WorkContext 是“业务执行单元上下文”。它不是单纯替代 HttpContext,而是把 HTTP 请求、后台任务、Hangfire Job、消息消费、并发 Task 等不同入口统一成一套可追踪、可释放、可继承、可隔离的执行上下文。
子页面
- ASP.NET Core 中使用 WorkContext:请求管道、中间件、Controller/Service 注入、请求外启动任务的边界。
- Task.Run 与新线程中使用 WorkContext:
Task.Run、线程池、新线程、Parallel中如何创建独立 Scope。 - Hangfire 中使用 WorkContext:内置
WorkContextJobActivator、Job Scope、Dashboard/Server 接入与自定义激活器要求。
WorkContext 是什么
WorkContext 表示一次业务执行单元的运行环境。这里的“Work”可以是一条 HTTP 请求、一次后台任务调度、一个 Hangfire Job、一条 RabbitMQ 消息消费、一次内存事件处理,也可以是请求内部派生出来的一个子任务。
它主要承载这些能力:
- 当前执行单元的
Scoped服务容器。 - 当前身份、语言、时区、日志事务号等上下文属性。
- 当前执行链路的
Activity、TraceId/SpanId 关联信息。 - 当前上下文内可读写的
Items与仅当前上下文有效的TempItems。 - 手动创建子 Scope、后台 Scope、新线程 Scope 的统一入口。
- 创建、销毁和泄漏排查所需的诊断事件。
在业务代码中,优先通过依赖注入获取 IWorkContextCore。只有框架启动、底层基础设施、当前没有业务执行单元的地方,才应该考虑通过 AgileLabContexts.Context 等应用级上下文能力兜底。
产生背景与解决的问题
WorkContext 的直接历史背景是:很多代码原本依赖 HttpContext 或当前请求的 IServiceProvider,但一旦离开 HTTP 管道,这些东西就不可靠了。
典型问题包括:
HttpContext为空:后台任务、Job、线程池任务、消息消费没有 HTTP 请求。- Scoped 服务边界不清:在后台线程中直接使用请求里注入的 Repository、
DbContext或业务服务,会导致生命周期和释放顺序失真。 - 身份与租户信息散落:当前用户、租户、语言、时区如果各自解析,会让审计字段、数据源选择、时间展示和日志链路不一致。
AsyncLocal串线:父线程的上下文会随ExecutionContext流入子线程,直接修改可能污染父线程。- Root ServiceProvider 滥用:为了绕过缺少 Scope 的问题从 Root 容器解析服务,会把 Scoped 服务变成事实上的长生命周期对象。
- 诊断困难:没有统一的上下文 ID、名称、创建栈和销毁事件时,排查 Scope 泄漏和身份串线会非常困难。
WorkContext 解决这些问题的方式是:每个业务执行单元都有自己的 IServiceScope 和 IWorkContextCore;子执行单元可以按规则继承父级属性;新线程或新 Task 可以强制创建新的 Holder;所有手动 Scope 都通过 IWorkContextScope.Dispose() 统一释放。
与 AgileLabContext 的关系
AgileLabContext 是应用级上下文,管理 RootServiceProvider、启动日志、全局配置和创建 WorkContext Scope 的入口。WorkContext 是业务执行单元上下文,只能拿到当前执行单元的 Scoped ServiceProvider。
| 场景 | 推荐使用 | 原因 |
|---|---|---|
| Controller、应用服务、Repository、Mapper Resolver | IWorkContextCore |
当前处于明确业务执行单元内,应使用当前 Scope |
| 后台任务开始、Job 激活、消息消费入口 | AgileLabContexts.Context.CreateScopeWithWorkContextForNewTask() |
先创建业务执行单元,再解析 Scoped 服务 |
| 框架启动、应用管道构建、无业务上下文的底层基础设施 | AgileLabContexts.Context |
此时还没有明确业务执行单元 |
| 获取当前用户、时区、TraceId、审计身份 | IWorkContextCore |
这些属于当前业务执行单元,不应从 Root 容器猜 |
简化理解:AgileLabContext 管“应用”,WorkContext 管“这一次业务执行”。
源码入口
核心实现位于 agilelabs.aspnet/src/AgileLabs.WebApp/WorkContexts 及少量接口文件:
| 源码 | 作用 |
|---|---|
IWorkContextCore.cs |
业务读取主接口 |
IWorkContextCoreSetter.cs |
身份、日志、语言、时区的写入口 |
IWorkContextAccessor.cs |
当前执行流 WorkContext 的访问器接口 |
DefaultWorkContextAccessor.cs |
基于 AsyncLocal<WorkContextHolder> 保存当前上下文 |
DefaultWorkContextCore.cs |
默认 WorkContext 实现 |
DefaultWorkContextCoreFactory.cs |
创建 WorkContext 并继承父级属性 |
WorkContextScope.cs |
手动 Scope 生命周期、释放和父上下文恢复 |
Extensions/WorkContextScopeCreateExtensions.cs |
创建 Scope 的主要扩展方法 |
WorkContextServcieCollectionExtensions.cs |
默认与自定义 WorkContext 注册 |
WorkContextTraceTable.cs |
存活上下文、创建/销毁事件、QPS 诊断 |
WebWorkContextInitMiddleware.cs |
HTTP 请求自动附着 WorkContext |
注意历史命名差异:源码文件名是 WebWorkContextInitMiddleware.cs,当前类名是 HttpWorkContextInitMiddleware。文档中讨论 HTTP 请求自动初始化时,以“文件 WebWorkContextInitMiddleware.cs 中的 HttpWorkContextInitMiddleware 类”为准,避免把文件名误写成不存在的 HttpWorkContextInitMiddleware.cs。
核心设计模型
flowchart TD
A["业务入口<br/>HTTP / Job / Task / Message"] --> B["创建或附着 IServiceScope"]
B --> C["IWorkContextFactory 创建 IWorkContextCore"]
C --> D["IWorkContextAccessor.SetContext"]
D --> E["AsyncLocal<WorkContextHolder> 保存当前上下文"]
E --> F["业务代码注入或解析 IWorkContextCore"]
F --> G["读取身份 / 时区 / Items / Scoped 服务"]
G --> H["IWorkContextScope.Dispose 或请求结束"]
H --> I["DisposeContext / TraceTable / 恢复父上下文"]
几个关键角色:
IWorkContextCore:业务读取的主对象,包含当前 ScopedServiceProvider、身份、语言、时区、Items、Activity 等。IWorkContextCoreSetter:框架或边界层写入上下文属性,例如认证完成后设置身份、任务开始时设置系统身份。IWorkContextAccessor:单例访问器,通过AsyncLocal<WorkContextHolder>让同一异步执行流能拿到当前 WorkContext。WorkContextHolder:Accessor 内部 Holder,用来包住当前 WorkContext;新 Task/线程场景可以强制 new 一个 Holder 做隔离。IWorkContextFactory:创建 WorkContext,并根据父上下文、继承标记和赋值模式决定哪些属性继承。IWorkContextScope:手动创建出来的 Scope 句柄,必须释放;释放时销毁当前 WorkContext,并在同线程子 Scope 场景恢复父上下文。WorkContextTraceTable:记录创建、销毁、存活列表、创建栈、QPS,用于排查泄漏和链路问题。
生命周期总览
| 场景 | 创建方式 | 释放方式 |
|---|---|---|
| ASP.NET Core HTTP 请求 | HttpWorkContextInitMiddleware 调用 AttachWorkContextForCurrentScope() |
请求结束时 DisposeCurrentWorkContext() |
| 同线程子 Scope | CreateScopeWithWorkContext() |
IWorkContextScope.Dispose(),并恢复父 WorkContext |
| 新 Task / 新线程 / 线程池 | CreateScopeWithWorkContextForNewTask() |
IWorkContextScope.Dispose(),清理独立 Holder |
| Hangfire Job | WorkContextJobActivator.BeginScope() |
JobActivatorScope.DisposeScope() |
| 消息消费 / BackgroundService | 执行边界手动创建 WorkContext Scope | using / Dispose() |
手动创建的 IWorkContextScope 必须释放。释放时会触发 WorkContextTraceTable 事件、调用 DisposeContext()、清理 Items、释放 Activity、释放当前 IServiceScope,并在同线程子 Scope 场景恢复父 WorkContext。
为什么子 Scope 从 RootServiceProvider 创建
WorkContext 子 Scope 始终从 RootServiceProvider 创建,而不是从父 Scope 的 ServiceProvider.CreateScope() 创建。
这是一个刻意设计,原因是 Autofac 的子 Scope 与父 Scope 有生命周期关联:父 Scope 释放后,子 Scope 可能无法继续正常解析服务。WorkContext 常用于 Task.Run 等场景,父请求可能已经结束,但子任务仍需要完成。因此框架保证所有 WorkContext 子 Scope 从 RootServiceProvider 创建,使它们的可用性不被父 Scope 提前释放影响。
这不代表可以忽略释放。每个手动创建的 WorkContext Scope 仍然必须 using 或显式 Dispose()。
接口与能力清单
IWorkContextCore
IWorkContextCore 是业务读取主接口。
| 成员 | 说明 |
|---|---|
ContextId |
当前 WorkContext 唯一 ID,用于日志和诊断 |
ContextName |
当前 WorkContext 名称,可通过 SetName() 设置 |
ServiceProvider |
当前 Scoped 容器,永远不应该是 RootServiceProvider |
Mapper |
从当前 Scope 解析的 AutoMapper IMapper |
Items |
可被子上下文按规则继承的上下文字典 |
TempItems |
当前上下文临时字典,不传递到子上下文 |
Identity |
当前身份信息,包含 Type、Id、Name、IsAuthenticated |
LogTransId |
日志事务号,可与请求链路对齐 |
WorkContextCultureInfo |
可继承的 Culture 包装信息 |
WorkContextTimeZoneInfo |
可继承的 TimeZone 包装信息 |
CultureInfo |
基于 WorkContextCultureInfo.Lcid 计算出的 CultureInfo |
TimeZoneInfo |
基于 WorkContextTimeZoneInfo.TimeZoneId 计算出的 TimeZoneInfo |
Activity |
当前上下文 Activity,用于链路追踪 |
SetName(string) |
设置上下文名称,并同步更新 TraceTable |
DisposeContext() |
释放上下文内部资源,通常由框架调用 |
IWorkContextCoreSetter
IWorkContextCoreSetter 继承 IWorkContextCore,提供写入方法:
| 方法 | 说明 |
|---|---|
SetLogTransId(string) |
设置日志事务号 |
SetIdentity(IIdentityInfo) |
设置当前身份 |
SetCultureInfo(IWorkContextCultureInfo) |
设置当前语言文化 |
SetTimeZone(IWorkContextTimeZoneInfo) |
设置当前时区 |
它适合认证、租户识别、任务入口初始化等边界层使用。普通业务服务不建议随意修改父上下文身份;需要临时切换身份或租户时,应创建子 Scope 后在子 Scope 内修改。
IWorkContextAccessor
IWorkContextAccessor 是单例服务,用来保存和读取当前执行流的 WorkContext。内部使用 AsyncLocal<WorkContextHolder> 保存 Holder。
业务代码通常不直接操作 Accessor,而是注入 IWorkContextCore。Accessor 属于框架和集成层工具。
IWorkContextScope
IWorkContextScope 是手动创建 Scope 的生命周期句柄:
public interface IWorkContextScope : IDisposable
{
IWorkContextCore WorkContext { get; }
}
使用规则很简单:谁创建,谁释放。
IWorkContextFactory
IWorkContextFactory 用于创建 WorkContext。默认实现 DefaultWorkContextCoreFactory<TWorkContext> 会创建新的 Activity、绑定当前 Scoped ServiceProvider、设置 ContextName、初始化 TempItems,并根据父 WorkContext、PropertiesInheritFlag 和 PropertiesAssignMode 继承属性。
Resolve 扩展
WorkContext 上提供了基于当前 Scoped 容器的解析扩展:
| 方法 | 用途 |
|---|---|
Resolve<T>() |
从当前 Scope 解析必需服务 |
Resolve(Type) |
按类型解析必需服务 |
ResolveOpentional(Type) |
按类型解析可选服务,源码方法名保留了 Opentional 拼写 |
ResolveAll<T>() |
解析服务集合 |
ResolveByName<T>(string) |
通过 Autofac 命名服务解析 |
ResolveByKey<T>(object) |
通过 Autofac Keyed 服务解析 |
ResolveUnregistered(Type) |
尝试构造未注册类型 |
推荐优先使用构造函数注入。Resolve 更适合框架入口、JobActivator、消息消费者、动态类型处理等无法静态注入的场景。
创建 Scope 的 API 对照
| 场景 | 推荐 API | 说明 |
|---|---|---|
| HTTP 请求内业务逻辑 | 直接注入 IWorkContextCore |
请求开始时已自动附着 |
| 同线程临时子业务单元 | workContext.CreateScopeWithWorkContext() |
Dispose 后恢复父上下文 |
Task.Run / 新线程 / 线程池 |
workContext.CreateScopeWithWorkContextForNewTask() |
强制新 Holder,隔离 AsyncLocal |
| 当前没有可用 WorkContext | AgileLabContexts.Context.CreateScopeWithWorkContextForNewTask() |
从应用 Root 创建新业务执行单元 |
| Hangfire Job 激活 | WorkContextJobActivator |
框架激活器内部已创建 Scope |
| HTTP 中间件初始化 | AttachWorkContextForCurrentScope() |
框架管道使用,业务通常不直接调用 |
| 当前 Scope 显式初始化 | InitWorkContextOnCurrentScope() |
框架/测试/兼容场景使用 |
属性继承与隔离策略
创建子 WorkContext 时,DefaultWorkContextCoreFactory 会根据 PropertiesInheritFlag 决定继承哪些属性。
| 标记 | 含义 |
|---|---|
Identity |
继承身份 |
Tenant |
预留/子系统租户标记 |
LogTransId |
继承日志事务号 |
CultureInfo |
继承语言文化 |
TimeZone |
继承时区 |
Items |
继承 Items |
FrameworkProperties |
框架属性集合 |
All |
全部继承,默认值 |
PropertiesAssignMode 决定继承属性时是引用还是拷贝:
| 模式 | 行为 | 使用建议 |
|---|---|---|
Reference |
直接引用父级属性对象 | 默认模式,性能好,但共享对象变更会互相影响 |
DeepClone |
使用 FastCloner 深拷贝 | 需要隔离身份、语言、时区对象时使用 |
DeepCloneByJsonSerilize |
使用 JSON 序列化深拷贝 | 兼容 JSON 可序列化对象,注意源码枚举名保留 Serilize 拼写 |
Items 的继承会创建新的字典,但字典内 Key/Value 使用原引用。不要把非线程安全对象、大对象、DbContext、Repository 放入 Items。TempItems 不继承,适合当前 Scope 内的一次性状态。
自定义 WorkContext
默认注册使用:
services.AddWorkContextCore();
如果项目需要扩展项目字段,例如租户、组织、门店、渠道、数据权限、客户端信息,可以使用:
services.AddCustomWorkContext<
ProjectWorkContext,
IProjectWorkContextSetter,
ProjectWorkContextFactory>();
推荐扩展方式:
public interface IProjectWorkContext : IWorkContextCore
{
string TenantId { get; }
}
public interface IProjectWorkContextSetter : IProjectWorkContext, IWorkContextCoreSetter
{
void SetTenant(string tenantId);
}
public class ProjectWorkContext : DefaultWorkContextCore, IProjectWorkContextSetter
{
public string TenantId { get; private set; } = string.Empty;
public void SetTenant(string tenantId)
{
TenantId = tenantId;
}
}
public class ProjectWorkContextFactory
: DefaultWorkContextCoreFactory<ProjectWorkContext>
{
}
自定义时要注意:
- 自定义 WorkContext 仍应继承或兼容
IWorkContextCore。 - Setter 接口应只暴露边界层需要写入的字段。
- 自定义 Factory 负责创建扩展类型,并按需处理扩展字段继承。
- 不要为了放一个临时字段就自定义 WorkContext;一次性状态优先用
TempItems,跨子上下文轻量状态再考虑Items。
诊断与监控
WorkContextTraceTable 提供系统级跟踪能力:
| 能力 | 说明 |
|---|---|
WorkContextCreatedEvent |
WorkContext 创建时触发 |
WorkContextDisposingEvent |
WorkContext 准备销毁时触发 |
GetWorkContexts(predicate, qpsItemCount) |
获取当前存活 WorkContext 和最近 QPS |
CountWorkContexts(predicate) |
获取存活数量 |
SetName(string) |
更新 TraceTable 中的上下文名称 |
| 创建栈记录 | 创建时记录过滤后的调用栈,便于查泄漏来源 |
为手动 Scope 命名是一个很小但很有价值的习惯:
using var scope = AgileLabContexts.Context.CreateScopeWithWorkContextForNewTask();
scope.WorkContext.SetName("MonthlyReportJob");
最佳实践
- HTTP 请求内直接注入
IWorkContextCore,不要手动 new WorkContext。 - 后台任务、Job、消息消费、
Task.Run中先创建 WorkContext Scope,再解析 Scoped 服务。 - 新线程、新 Task、线程池任务优先使用
CreateScopeWithWorkContextForNewTask()。 - 同线程临时隔离才使用
CreateScopeWithWorkContext()。 - 手动创建的
IWorkContextScope必须using或显式Dispose()。 - 自建 Scope 建议设置
ContextName,便于 TraceTable 和日志定位。 - 当前用户、租户、时区、语言、TraceId、审计身份统一从 WorkContext 读取。
- 需要临时切换身份/租户时,在子 Scope 内切换,不污染父上下文。
反模式
- 在
Task.Run里继续使用父请求注入出来的 Repository、DbContext或业务服务。 - 从 RootServiceProvider 直接解析 Scoped 服务来绕过缺少 WorkContext 的问题。
- 把
scope.WorkContext.ServiceProvider缓存成字段,Scope 释放后继续使用。 - 忘记释放手动创建的 WorkContext Scope。
- 把
Items当成线程安全的全局缓存。 - 在普通业务逻辑里随意调用
IWorkContextCoreSetter修改父上下文身份。 - 在后台保存数据库前没有创建 WorkContext,导致审计字段缺失或直接抛错。
快速决策表
| 问题 | 结论 |
|---|---|
| HTTP Controller 里要不要创建 WorkContext? | 不要,直接注入 IWorkContextCore |
| BackgroundService 里要不要创建 WorkContext? | 要,每次业务执行都创建 |
Task.Run 里用哪个 API? |
CreateScopeWithWorkContextForNewTask() |
| 同线程临时隔离用哪个 API? | CreateScopeWithWorkContext() |
能不能缓存 ServiceProvider? |
不能 |
能不能把 DbContext 放进 Items? |
不能 |
| 保存数据库是否依赖 WorkContext? | 是,EF Core 审计会读取当前 WorkContext |
| 自定义租户字段放哪里? | 简单场景可用 Items,长期项目字段应扩展自定义 WorkContext |
| 怎么排查泄漏? | 命名 Scope,查看 WorkContextTraceTable 存活列表和创建栈 |