agentleFS
VincentH-NetVincentH-Net/dotnet-agentic-engineering/.agents/skills/migrate-static-to-wrapper/SKILL.md

migrate-static-to-wrapper

Skill9 starsChanged 6 months agoMITView on GitHub

Mechanically replace static dependency call sites with wrapper or built-in abstraction calls across a bounded scope (file, project, or namespace). Performs codemod-style bulk replacement of DateTime.UtcNow to TimeProvider.GetUtcNow(), File.ReadAllText to IFileSystem, and similar transformations. Adds constructor injection parameters and updates DI registration. USE FOR: replace DateTime.UtcNow with TimeProvider, replace DateTime.Now with TimeProvider, migrate static calls to wrapper, bulk replace File.* with IFileSystem, codemod static to injectable, add constructor injection for time provider, mechanical migration of statics, refactor DateTime to TimeProvider, swap static for injected dependency, convert static calls to use abstraction, replace statics in a class, migrate one file to TimeProvider, scoped migration, update call sites. DO NOT USE FOR: detecting statics (use detect-static-dependencies), generating wrappers (use generate-testability-wrappers), migrating between test frameworks.

1 other project uses a version of this, 1 of them edited.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

14 rules. About 0 are common, 6 are distinctive, and 8 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 8

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    After wrappers have been generated (via generate-testability-wrappers) or built-in abstractions identified

    line 16
  2. Very specific:

    No wrapper or abstraction exists yet (use generate-testability-wrappers first)

    line 24
  3. Distinctive:

    Before modifying any code:

    line 42
  4. Very specific:

    Confirm the wrapper/abstraction exists: Check that the interface or built-in abstraction is available in the project.

    line 44
  5. Very specific:

    Confirm DI registration exists: Check Program.cs or Startup.cs for the service registration.

    line 46
  6. Distinctive:

    Add the new dependency following the class's existing pattern:

    line 78
  7. Very specific:

    Traditional constructor: Add private readonly field + constructor parameter, matching the existing field naming convention (_camelCase or …

    line 81
  8. Very specific:

    Add required using directives if not already present

    line 89
  9. Distinctive:

    If test files exist for the migrated classes:

    line 102
  10. Distinctive:

    After all changes in the current scope:

    line 112
  11. Distinctive:

    If the build fails:

    line 118
  12. Very specific:

    Missing using: Add the required using directive

    line 119
  13. Very specific:

    Missing NuGet package: Run dotnet add package <name>

    line 120
  14. Distinctive:

    Constructor mismatch in tests: Update test instantiation (Step 5)

    line 121

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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 strategyNoconstructor (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
TimeDateTime.Now_timeProvider.GetLocalNow().DateTime
TimeDateTime.UtcNow_timeProvider.GetUtcNow().DateTime
TimeDateTime.Today_timeProvider.GetLocalNow().Date
TimeDateTimeOffset.UtcNow_timeProvider.GetUtcNow()
FileFile.ReadAllText(path)_fileSystem.File.ReadAllText(path)
FileFile.WriteAllText(path, text)_fileSystem.File.WriteAllText(path, text)
FileFile.Exists(path)_fileSystem.File.Exists(path)
FileDirectory.Exists(path)_fileSystem.Directory.Exists(path)
EnvEnvironment.GetEnvironmentVariable(name)_env.GetEnvironmentVariable(name)
ConsoleConsole.WriteLine(msg)_console.WriteLine(msg)
ProcessProcess.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
TimeProviderNone (in System namespace)
IFileSystemusing System.IO.Abstractions;
IHttpClientFactoryusing System.Net.Http; (usually already present)
Custom wrappersusing <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 NuGetAdd Microsoft.Extensions.TimeProvider.Testing to test project
Replacing in expression-bodied members without updating return typeDateTime → 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

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.