migrate-static-to-wrapper
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMigrate Static to Wrapper
将静态依赖迁移到包装器
Perform mechanical, codemod-style replacement of static dependency call sites with calls to injected wrapper interfaces or built-in abstractions. Operates on a bounded scope (single file, project, or namespace) so migrations can be done incrementally.
以机械的、代码修改(codemod)风格的方式,将静态依赖调用替换为对注入式包装器接口或内置抽象的调用。支持在限定范围(单个文件、项目或命名空间)内操作,因此可以逐步完成迁移。
When to Use
适用场景
- After wrappers have been generated (via ) or built-in abstractions identified
generate-testability-wrappers - Migrating →
DateTime.UtcNowacross a projectTimeProvider.GetUtcNow() - Migrating →
File.*across a namespaceIFileSystem.File.* - Adding constructor injection for the new abstraction to affected classes
- Incremental migration: one project or namespace at a time
- 已生成包装器(通过)或已确定内置抽象之后
generate-testability-wrappers - 在整个项目中将迁移为
DateTime.UtcNowTimeProvider.GetUtcNow() - 在整个命名空间中将迁移为
File.*IFileSystem.File.* - 为受影响的类添加针对新抽象的构造函数注入
- 增量迁移:一次迁移一个项目或命名空间
When Not to Use
不适用场景
- No wrapper or abstraction exists yet (use first)
generate-testability-wrappers - The user wants to detect statics, not migrate them (use )
detect-static-dependencies - The code does not use dependency injection and the user hasn't chosen ambient context
- Migrating between test frameworks (use the appropriate migration skill)
- 尚未存在包装器或抽象(请先使用)
generate-testability-wrappers - 用户仅需检测静态依赖,而非迁移(使用)
detect-static-dependencies - 代码未使用依赖注入且用户未选择环境上下文模式
- 测试框架之间的迁移(使用对应的迁移技能)
Inputs
输入项
| Input | Required | Description |
|---|---|---|
| Static pattern | Yes | What to replace (e.g., |
| Replacement abstraction | Yes | What to use instead (e.g., |
| Scope | Yes | File path, project (.csproj), namespace, or directory to migrate |
| Injection strategy | No | |
| 输入项 | 是否必填 | 描述 |
|---|---|---|
| 静态调用模式 | 是 | 需要替换的内容(例如: |
| 替代抽象 | 是 | 用于替换的目标(例如: |
| 范围 | 是 | 要迁移的文件路径、项目(.csproj)、命名空间或目录 |
| 注入策略 | 否 | |
Workflow
工作流程
Step 1: Verify prerequisites
步骤1:验证前置条件
Before modifying any code:
-
Confirm the wrapper/abstraction exists: Check that the interface or built-in abstraction is available in the project. For, verify the target framework is .NET 8+ or
TimeProvideris referenced. ForMicrosoft.Bcl.TimeProvider, verify the NuGet package is referenced.System.IO.Abstractions -
Confirm DI registration exists: Checkor
Program.csfor the service registration. If missing, add it before proceeding.Startup.cs -
Identify all files in scope: List thefiles that will be modified. Exclude test projects,
.cs,obj/, and generated code.bin/
在修改代码之前:
-
确认包装器/抽象已存在:检查项目中是否已存在对应接口或内置抽象。对于,验证目标框架为.NET 8+或已引用
TimeProvider。对于Microsoft.Bcl.TimeProvider,验证已引用对应的NuGet包。System.IO.Abstractions -
确认DI注册已存在:检查或
Program.cs中的服务注册。如果缺失,请先添加再继续。Startup.cs -
确定范围内的所有文件:列出需要修改的文件。排除测试项目、
.cs、obj/目录及生成的代码。bin/
Step 2: Plan the migration for each file
步骤2:为每个文件规划迁移
For each file containing the static pattern, determine:
- Which class(es) contain the call sites — identify the class declarations
- Whether the class already has the dependency injected — check constructors for existing ,
TimeProvider, etc. parametersIFileSystem - The replacement expression for each call site
对于包含静态调用模式的每个文件,确定:
- 哪些类包含调用站点 —— 识别类声明
- 类是否已注入该依赖 —— 检查构造函数中是否已有、
TimeProvider等参数IFileSystem - 每个调用站点的替代表达式
Replacement mapping
替换映射
| Category | Original | DI replacement |
|---|---|---|
| Time | | |
| Time | | |
| Time | | |
| Time | | |
| File | | |
| File | | |
| File | | |
| File | | |
| Env | | |
| Console | | |
| Process | | |
Apply the same pattern for other members in each category.
| 类别 | 原调用 | DI替代调用 |
|---|---|---|
| 时间 | | |
| 时间 | | |
| 时间 | | |
| 时间 | | |
| 文件 | | |
| 文件 | | |
| 文件 | | |
| 文件 | | |
| 环境 | | |
| 控制台 | | |
| 进程 | | |
对每个类别中的其他成员应用相同的替换模式。
Step 3: Add constructor injection
步骤3:添加构造函数注入
Add the new dependency following the class's existing pattern:
- Primary constructor (C# 12+): Add parameter to primary constructor:
public class OrderProcessor(ILogger<OrderProcessor> logger, TimeProvider timeProvider) - Traditional constructor: Add field + constructor parameter, matching the existing field naming convention (
private readonlyor_camelCase)m_camelCase
遵循类的现有模式添加新依赖:
- 主构造函数(C# 12+):在主构造函数中添加参数:
public class OrderProcessor(ILogger<OrderProcessor> logger, TimeProvider timeProvider) - 传统构造函数:添加字段 + 构造函数参数,匹配现有字段命名约定(
private readonly或_小驼峰)m_小驼峰
Step 4: Replace call sites
步骤4:替换调用站点
Perform each replacement mechanically. For each call site:
- Replace the static call with the wrapper call
- Preserve the surrounding code structure (whitespace, comments, chaining)
- Add required directives if not already present
using
以机械方式执行每个替换。对于每个调用站点:
- 将静态调用替换为包装器调用
- 保留周围代码结构(空格、注释、链式调用)
- 如果尚未存在,添加必要的指令
using
Adding using directives
添加using指令
| Abstraction | Using directive |
|---|---|
| None (in |
| |
| |
| Custom wrappers | |
| 抽象 | Using指令 |
|---|---|
| 无(属于 |
| |
| |
| 自定义包装器 | |
Step 5: Update affected test files
步骤5:更新受影响的测试文件
If test files exist for the migrated classes:
- Update constructor calls — add the new parameter to test class instantiation
- Use test doubles:
- →
TimeProviderfromnew FakeTimeProvider()Microsoft.Extensions.TimeProvider.Testing - →
IFileSystemfromnew MockFileSystem()System.IO.Abstractions.TestingHelpers - Custom wrappers → or hand-rolled fake
new Mock<IWrapperName>()
如果迁移的类存在对应的测试文件:
- 更新构造函数调用 —— 在测试类实例化时添加新参数
- 使用测试替身:
- →
TimeProvider(来自new FakeTimeProvider())Microsoft.Extensions.TimeProvider.Testing - →
IFileSystem(来自new MockFileSystem())System.IO.Abstractions.TestingHelpers - 自定义包装器 → 或手动实现的伪造对象
new Mock<IWrapperName>()
Step 6: Build verification
步骤6:构建验证
After all changes in the current scope:
bash
dotnet build <project.csproj>If the build fails:
- Missing using: Add the required directive
using - Missing NuGet package: Run
dotnet add package <name> - Constructor mismatch in tests: Update test instantiation (Step 5)
- Ambiguous call: Fully qualify the wrapper call
完成当前范围内的所有修改后:
bash
dotnet build <project.csproj>如果构建失败:
- 缺少using指令:添加所需的指令
using - 缺少NuGet包:运行
dotnet add package <包名> - 测试中构造函数不匹配:更新测试实例化(步骤5)
- 调用歧义:使用完全限定名调用包装器
Step 7: Report changes
步骤7:报告变更
Summarize what was done:
undefined总结已完成的操作:
undefinedMigration Summary
迁移总结
Pattern: DateTime.UtcNow → TimeProvider.GetUtcNow()
Scope: MyProject/Services/
替换模式: DateTime.UtcNow → TimeProvider.GetUtcNow()
范围: MyProject/Services/
Files Modified (production)
已修改文件(生产环境)
| File | Call Sites Replaced | Injection Added |
|---|---|---|
| OrderProcessor.cs | 3 | Yes (constructor) |
| NotificationService.cs | 1 | Yes (primary ctor) |
| 文件 | 替换的调用站点数量 | 是否添加注入 |
|---|---|---|
| OrderProcessor.cs | 3 | 是(构造函数) |
| NotificationService.cs | 1 | 是(主构造函数) |
Files Modified (tests)
已修改文件(测试环境)
| File | Change |
|---|---|
| OrderProcessorTests.cs | Added FakeTimeProvider parameter |
| 文件 | 变更内容 |
|---|---|
| OrderProcessorTests.cs | 添加FakeTimeProvider参数 |
Remaining (out of scope)
未迁移内容(超出范围)
- MyProject/Legacy/ — 8 call sites not migrated (different namespace)
undefined- MyProject/Legacy/ —— 8个调用站点未迁移(属于不同命名空间)
undefinedValidation
验证清单
- All call sites in scope were replaced (none missed)
- Constructor injection added to all affected classes
- Field naming follows existing class conventions
- Required directives added
using - Required NuGet packages referenced
- Build succeeds after migration
- Test files updated with appropriate test doubles
- No behavioral changes introduced (wrapper delegates directly to the static)
- 范围内的所有调用站点均已替换(无遗漏)
- 所有受影响的类均已添加构造函数注入
- 字段命名符合类的现有约定
- 已添加所需的指令
using - 已引用所需的NuGet包
- 迁移后构建成功
- 测试文件已使用合适的测试替身更新
- 未引入行为变更(包装器直接委托给静态调用)
Common Pitfalls
常见陷阱
| Pitfall | Solution |
|---|---|
| Replacing statics in test code | Only replace in production code; tests should use fakes/mocks |
| Breaking static classes | Static classes can't have constructors — use ambient context for these |
Missing | Add |
| Replacing in expression-bodied members without updating return type | |
| Migrating too much at once | Stick to the defined scope — one project or namespace per run |
| Forgetting DI registration | Always verify |
| 陷阱 | 解决方案 |
|---|---|
| 在测试代码中替换静态调用 | 仅在生产代码中替换;测试代码应使用伪造/模拟对象 |
| 破坏静态类 | 静态类无法拥有构造函数 —— 对这类类使用环境上下文模式 |
缺少 | 为测试项目添加 |
| 替换表达式体成员时未更新返回类型 | 使用 |
| 一次迁移过多内容 | 严格遵循定义的范围 —— 每次运行仅迁移一个项目或命名空间 |
| 忘记DI注册 | 在替换调用站点之前,务必验证 |