migrate-static-to-wrapper

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Migrate 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
    generate-testability-wrappers
    ) or built-in abstractions identified
  • Migrating
    DateTime.UtcNow
    TimeProvider.GetUtcNow()
    across a project
  • Migrating
    File.*
    IFileSystem.File.*
    across a namespace
  • Adding constructor injection for the new abstraction to affected classes
  • Incremental migration: one project or namespace at a time
  • 已生成包装器(通过
    generate-testability-wrappers
    )或已确定内置抽象之后
  • 在整个项目中将
    DateTime.UtcNow
    迁移为
    TimeProvider.GetUtcNow()
  • 在整个命名空间中将
    File.*
    迁移为
    IFileSystem.File.*
  • 为受影响的类添加针对新抽象的构造函数注入
  • 增量迁移:一次迁移一个项目或命名空间

When Not to Use

不适用场景

  • No wrapper or abstraction exists yet (use
    generate-testability-wrappers
    first)
  • 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

输入项

InputRequiredDescription
Static patternYesWhat to replace (e.g.,
DateTime.UtcNow
,
File.ReadAllText
)
Replacement abstractionYesWhat to use instead (e.g.,
TimeProvider
,
IFileSystem
)
ScopeYesFile path, project (.csproj), namespace, or directory to migrate
Injection strategyNo
constructor
(default),
primary-constructor
, or
ambient
输入项是否必填描述
静态调用模式需要替换的内容(例如:
DateTime.UtcNow
File.ReadAllText
替代抽象用于替换的目标(例如:
TimeProvider
IFileSystem
范围要迁移的文件路径、项目(.csproj)、命名空间或目录
注入策略
constructor
(默认)、
primary-constructor
ambient

Workflow

工作流程

Step 1: Verify prerequisites

步骤1:验证前置条件

Before modifying any code:
  1. Confirm the wrapper/abstraction exists: Check that the interface or built-in abstraction is available in the project. For
    TimeProvider
    , verify the target framework is .NET 8+ or
    Microsoft.Bcl.TimeProvider
    is referenced. For
    System.IO.Abstractions
    , verify the NuGet package is referenced.
  2. Confirm DI registration exists: Check
    Program.cs
    or
    Startup.cs
    for the service registration. If missing, add it before proceeding.
  3. Identify all files in scope: List the
    .cs
    files that will be modified. Exclude test projects,
    obj/
    ,
    bin/
    , and generated code.
在修改代码之前:
  1. 确认包装器/抽象已存在:检查项目中是否已存在对应接口或内置抽象。对于
    TimeProvider
    ,验证目标框架为.NET 8+或已引用
    Microsoft.Bcl.TimeProvider
    。对于
    System.IO.Abstractions
    ,验证已引用对应的NuGet包。
  2. 确认DI注册已存在:检查
    Program.cs
    Startup.cs
    中的服务注册。如果缺失,请先添加再继续。
  3. 确定范围内的所有文件:列出需要修改的
    .cs
    文件。排除测试项目、
    obj/
    bin/
    目录及生成的代码。

Step 2: Plan the migration for each file

步骤2:为每个文件规划迁移

For each file containing the static pattern, determine:
  1. Which class(es) contain the call sites — identify the class declarations
  2. Whether the class already has the dependency injected — check constructors for existing
    TimeProvider
    ,
    IFileSystem
    , etc. parameters
  3. The replacement expression for each call site
对于包含静态调用模式的每个文件,确定:
  1. 哪些类包含调用站点 —— 识别类声明
  2. 类是否已注入该依赖 —— 检查构造函数中是否已有
    TimeProvider
    IFileSystem
    等参数
  3. 每个调用站点的替代表达式

Replacement mapping

替换映射

CategoryOriginalDI replacement
Time
DateTime.Now
_timeProvider.GetLocalNow().DateTime
Time
DateTime.UtcNow
_timeProvider.GetUtcNow().DateTime
Time
DateTime.Today
_timeProvider.GetLocalNow().Date
Time
DateTimeOffset.UtcNow
_timeProvider.GetUtcNow()
File
File.ReadAllText(path)
_fileSystem.File.ReadAllText(path)
File
File.WriteAllText(path, text)
_fileSystem.File.WriteAllText(path, text)
File
File.Exists(path)
_fileSystem.File.Exists(path)
File
Directory.Exists(path)
_fileSystem.Directory.Exists(path)
Env
Environment.GetEnvironmentVariable(name)
_env.GetEnvironmentVariable(name)
Console
Console.WriteLine(msg)
_console.WriteLine(msg)
Process
Process.Start(info)
_processRunner.Start(info)
Apply the same pattern for other members in each category.
类别原调用DI替代调用
时间
DateTime.Now
_timeProvider.GetLocalNow().DateTime
时间
DateTime.UtcNow
_timeProvider.GetUtcNow().DateTime
时间
DateTime.Today
_timeProvider.GetLocalNow().Date
时间
DateTimeOffset.UtcNow
_timeProvider.GetUtcNow()
文件
File.ReadAllText(path)
_fileSystem.File.ReadAllText(path)
文件
File.WriteAllText(path, text)
_fileSystem.File.WriteAllText(path, text)
文件
File.Exists(path)
_fileSystem.File.Exists(path)
文件
Directory.Exists(path)
_fileSystem.Directory.Exists(path)
环境
Environment.GetEnvironmentVariable(name)
_env.GetEnvironmentVariable(name)
控制台
Console.WriteLine(msg)
_console.WriteLine(msg)
进程
Process.Start(info)
_processRunner.Start(info)
对每个类别中的其他成员应用相同的替换模式。

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
    private readonly
    field + constructor parameter, matching the existing field naming convention (
    _camelCase
    or
    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:
  1. Replace the static call with the wrapper call
  2. Preserve the surrounding code structure (whitespace, comments, chaining)
  3. Add required
    using
    directives if not already present
以机械方式执行每个替换。对于每个调用站点:
  1. 将静态调用替换为包装器调用
  2. 保留周围代码结构(空格、注释、链式调用)
  3. 如果尚未存在,添加必要的
    using
    指令

Adding using directives

添加using指令

AbstractionUsing directive
TimeProvider
None (in
System
namespace)
IFileSystem
using System.IO.Abstractions;
IHttpClientFactory
using System.Net.Http;
(usually already present)
Custom wrappers
using <wrapper namespace>;
抽象Using指令
TimeProvider
无(属于
System
命名空间)
IFileSystem
using System.IO.Abstractions;
IHttpClientFactory
using System.Net.Http;
(通常已存在)
自定义包装器
using <包装器命名空间>;

Step 5: Update affected test files

步骤5:更新受影响的测试文件

If test files exist for the migrated classes:
  1. Update constructor calls — add the new parameter to test class instantiation
  2. Use test doubles:
    • TimeProvider
      new FakeTimeProvider()
      from
      Microsoft.Extensions.TimeProvider.Testing
    • IFileSystem
      new MockFileSystem()
      from
      System.IO.Abstractions.TestingHelpers
    • Custom wrappers →
      new Mock<IWrapperName>()
      or hand-rolled fake
如果迁移的类存在对应的测试文件:
  1. 更新构造函数调用 —— 在测试类实例化时添加新参数
  2. 使用测试替身
    • 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
    using
    directive
  • 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
总结已完成的操作:
undefined

Migration Summary

迁移总结

Pattern: DateTime.UtcNow → TimeProvider.GetUtcNow() Scope: MyProject/Services/
替换模式: DateTime.UtcNow → TimeProvider.GetUtcNow() 范围: MyProject/Services/

Files Modified (production)

已修改文件(生产环境)

FileCall Sites ReplacedInjection Added
OrderProcessor.cs3Yes (constructor)
NotificationService.cs1Yes (primary ctor)
文件替换的调用站点数量是否添加注入
OrderProcessor.cs3是(构造函数)
NotificationService.cs1是(主构造函数)

Files Modified (tests)

已修改文件(测试环境)

FileChange
OrderProcessorTests.csAdded FakeTimeProvider parameter
文件变更内容
OrderProcessorTests.cs添加FakeTimeProvider参数

Remaining (out of scope)

未迁移内容(超出范围)

  • MyProject/Legacy/ — 8 call sites not migrated (different namespace)
undefined
  • MyProject/Legacy/ —— 8个调用站点未迁移(属于不同命名空间)
undefined

Validation

验证清单

  • All call sites in scope were replaced (none missed)
  • Constructor injection added to all affected classes
  • Field naming follows existing class conventions
  • Required
    using
    directives added
  • 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

常见陷阱

PitfallSolution
Replacing statics in test codeOnly replace in production code; tests should use fakes/mocks
Breaking static classesStatic classes can't have constructors — use ambient context for these
Missing
FakeTimeProvider
NuGet
Add
Microsoft.Extensions.TimeProvider.Testing
to test project
Replacing in expression-bodied members without updating return type
DateTime
DateTimeOffset
when using
TimeProvider.GetUtcNow()
— verify type compatibility
Migrating too much at onceStick to the defined scope — one project or namespace per run
Forgetting DI registrationAlways verify
Program.cs
/
Startup.cs
has the registration before replacing call sites
陷阱解决方案
在测试代码中替换静态调用仅在生产代码中替换;测试代码应使用伪造/模拟对象
破坏静态类静态类无法拥有构造函数 —— 对这类类使用环境上下文模式
缺少
FakeTimeProvider
NuGet包
为测试项目添加
Microsoft.Extensions.TimeProvider.Testing
替换表达式体成员时未更新返回类型使用
TimeProvider.GetUtcNow()
时,
DateTime
会变为
DateTimeOffset
—— 验证类型兼容性
一次迁移过多内容严格遵循定义的范围 —— 每次运行仅迁移一个项目或命名空间
忘记DI注册在替换调用站点之前,务必验证
Program.cs
/
Startup.cs
中是否已完成注册