Skills migrate-static-to-wrapper

install
source · Clone the upstream repo
git clone https://github.com/dotnet/skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/dotnet/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/dotnet-test/skills/migrate-static-to-wrapper" ~/.claude/skills/dotnet-skills-migrate-static-to-wrapper && rm -rf "$T"
manifest: plugins/dotnet-test/skills/migrate-static-to-wrapper/SKILL.md
source content

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.

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

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)

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

Workflow

Step 1: Verify prerequisites

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.

Step 2: Plan the migration for each file

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

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.

Step 3: Add constructor injection

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
    )

Step 4: Replace call sites

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

Adding using directives

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>;

Step 5: Update affected test files

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

Step 6: Build verification

After all changes in the current scope:

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

Step 7: Report changes

Summarize what was done:

## Migration Summary

**Pattern**: DateTime.UtcNow → TimeProvider.GetUtcNow()
**Scope**: MyProject/Services/

### Files Modified (production)
| File | Call Sites Replaced | Injection Added |
|------|--------------------:|:----------------|
| OrderProcessor.cs | 3 | Yes (constructor) |
| NotificationService.cs | 1 | Yes (primary ctor) |

### Files Modified (tests)
| File | Change |
|------|--------|
| OrderProcessorTests.cs | Added FakeTimeProvider parameter |

### Remaining (out of scope)
- MyProject/Legacy/ — 8 call sites not migrated (different namespace)

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)

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