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

WorkContext

本目录是 AgileLabs Framework 中 WorkContext 的独立专题入口,用来帮助项目理解、接入和排查 WorkContext。它覆盖 WorkContext 的产生背景、核心设计、生命周期、接口能力、继承策略、自定义方式和典型使用场景。

如果只记一句话:WorkContext 是“业务执行单元上下文”。它不是单纯替代 HttpContext,而是把 HTTP 请求、后台任务、Hangfire Job、消息消费、并发 Task 等不同入口统一成一套可追踪、可释放、可继承、可隔离的执行上下文。

子页面

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 解决这些问题的方式是:每个业务执行单元都有自己的 IServiceScopeIWorkContextCore;子执行单元可以按规则继承父级属性;新线程或新 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:业务读取的主对象,包含当前 Scoped ServiceProvider、身份、语言、时区、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 当前身份信息,包含 TypeIdNameIsAuthenticated
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、PropertiesInheritFlagPropertiesAssignMode 继承属性。

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 放入 ItemsTempItems 不继承,适合当前 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 存活列表和创建栈

相关页面