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
| Input |
Required |
Description |
| Static pattern |
Yes |
What to replace (e.g., DateTime.UtcNow, File.ReadAllText) |
| Replacement abstraction |
Yes |
What to use instead (e.g., TimeProvider, IFileSystem) |
| Scope |
Yes |
File path, project (.csproj), namespace, or directory to migrate |
| Injection strategy |
No |
constructor (default), primary-constructor, or ambient |
Workflow
Step 1: Verify prerequisites
Before modifying any code:
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.
Confirm DI registration exists: Check Program.cs or Startup.cs for the service registration. If missing, add it before proceeding.
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:
- Which class(es) contain the call sites — identify the class declarations
- Whether the class already has the dependency injected — check constructors for existing
TimeProvider, IFileSystem, etc. parameters
- The replacement expression for each call site
Replacement mapping
| Category |
Original |
DI 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:
- Replace the static call with the wrapper call
- Preserve the surrounding code structure (whitespace, comments, chaining)
- Add required
using directives if not already present
Adding using directives
| Abstraction |
Using 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:
- Update constructor calls — add the new parameter to test class instantiation
- 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
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 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 once |
Stick to the defined scope — one project or namespace per run |
| Forgetting DI registration |
Always verify Program.cs/Startup.cs has the registration before replacing call sites |
1---2name: migrate-static-to-wrapper3description: Mechanically replace static dependency call sites with wrapper or built-in abstraction calls across a bounded scope, such as migrating DateTime.UtcNow to TimeProvider.GetUtcNow() or File.ReadAllText to IFileSystem.4license: MIT5---67# Migrate Static to Wrapper89Perform 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.1011## When to Use1213- After wrappers have been generated (via `generate-testability-wrappers`) or built-in abstractions identified14- Migrating `DateTime.UtcNow` → `TimeProvider.GetUtcNow()` across a project15- Migrating `File.*` → `IFileSystem.File.*` across a namespace16- Adding constructor injection for the new abstraction to affected classes17- Incremental migration: one project or namespace at a time1819## When Not to Use2021- No wrapper or abstraction exists yet (use `generate-testability-wrappers` first)22- The user wants to detect statics, not migrate them (use `detect-static-dependencies`)23- The code does not use dependency injection and the user hasn't chosen ambient context24- Migrating between test frameworks (use the appropriate migration skill)2526## Inputs2728| Input | Required | Description |29|-------|----------|-------------|30| Static pattern | Yes | What to replace (e.g., `DateTime.UtcNow`, `File.ReadAllText`) |31| Replacement abstraction | Yes | What to use instead (e.g., `TimeProvider`, `IFileSystem`) |32| Scope | Yes | File path, project (.csproj), namespace, or directory to migrate |33| Injection strategy | No | `constructor` (default), `primary-constructor`, or `ambient` |3435## Workflow3637### Step 1: Verify prerequisites3839Before modifying any code:40411. **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.42432. **Confirm DI registration exists**: Check `Program.cs` or `Startup.cs` for the service registration. If missing, add it before proceeding.44453. **Identify all files in scope**: List the `.cs` files that will be modified. Exclude test projects, `obj/`, `bin/`, and generated code.4647### Step 2: Plan the migration for each file4849For each file containing the static pattern, determine:50511. **Which class(es) contain the call sites** — identify the class declarations522. **Whether the class already has the dependency injected** — check constructors for existing `TimeProvider`, `IFileSystem`, etc. parameters533. **The replacement expression** for each call site5455#### Replacement mapping5657| Category | Original | DI replacement |58|----------|----------|----------------|59| Time | `DateTime.Now` | `_timeProvider.GetLocalNow().DateTime` |60| Time | `DateTime.UtcNow` | `_timeProvider.GetUtcNow().DateTime` |61| Time | `DateTime.Today` | `_timeProvider.GetLocalNow().Date` |62| Time | `DateTimeOffset.UtcNow` | `_timeProvider.GetUtcNow()` |63| File | `File.ReadAllText(path)` | `_fileSystem.File.ReadAllText(path)` |64| File | `File.WriteAllText(path, text)` | `_fileSystem.File.WriteAllText(path, text)` |65| File | `File.Exists(path)` | `_fileSystem.File.Exists(path)` |66| File | `Directory.Exists(path)` | `_fileSystem.Directory.Exists(path)` |67| Env | `Environment.GetEnvironmentVariable(name)` | `_env.GetEnvironmentVariable(name)` |68| Console | `Console.WriteLine(msg)` | `_console.WriteLine(msg)` |69| Process | `Process.Start(info)` | `_processRunner.Start(info)` |7071Apply the same pattern for other members in each category.7273### Step 3: Add constructor injection7475Add the new dependency following the class's existing pattern:7677- **Primary constructor** (C# 12+): Add parameter to primary constructor: `public class OrderProcessor(ILogger<OrderProcessor> logger, TimeProvider timeProvider)`78- **Traditional constructor**: Add `private readonly` field + constructor parameter, matching the existing field naming convention (`_camelCase` or `m_camelCase`)7980### Step 4: Replace call sites8182Perform each replacement mechanically. For each call site:83841. Replace the static call with the wrapper call852. Preserve the surrounding code structure (whitespace, comments, chaining)863. Add required `using` directives if not already present8788#### Adding using directives8990| Abstraction | Using directive |91|------------|-----------------|92| `TimeProvider` | None (in `System` namespace) |93| `IFileSystem` | `using System.IO.Abstractions;` |94| `IHttpClientFactory` | `using System.Net.Http;` (usually already present) |95| Custom wrappers | `using <wrapper namespace>;` |9697### Step 5: Update affected test files9899If test files exist for the migrated classes:1001011. **Update constructor calls** — add the new parameter to test class instantiation1022. **Use test doubles**:103 - `TimeProvider` → `new FakeTimeProvider()` from `Microsoft.Extensions.TimeProvider.Testing`104 - `IFileSystem` → `new MockFileSystem()` from `System.IO.Abstractions.TestingHelpers`105 - Custom wrappers → `new Mock<IWrapperName>()` or hand-rolled fake106107### Step 6: Build verification108109After all changes in the current scope:110111```bash112dotnet build <project.csproj>113```114115If the build fails:116- **Missing using**: Add the required `using` directive117- **Missing NuGet package**: Run `dotnet add package <name>`118- **Constructor mismatch in tests**: Update test instantiation (Step 5)119- **Ambiguous call**: Fully qualify the wrapper call120121### Step 7: Report changes122123Summarize what was done:124125```126## Migration Summary127128**Pattern**: DateTime.UtcNow → TimeProvider.GetUtcNow()129**Scope**: MyProject/Services/130131### Files Modified (production)132| File | Call Sites Replaced | Injection Added |133|------|--------------------:|:----------------|134| OrderProcessor.cs | 3 | Yes (constructor) |135| NotificationService.cs | 1 | Yes (primary ctor) |136137### Files Modified (tests)138| File | Change |139|------|--------|140| OrderProcessorTests.cs | Added FakeTimeProvider parameter |141142### Remaining (out of scope)143- MyProject/Legacy/ — 8 call sites not migrated (different namespace)144```145146## Validation147148- [ ] All call sites in scope were replaced (none missed)149- [ ] Constructor injection added to all affected classes150- [ ] Field naming follows existing class conventions151- [ ] Required `using` directives added152- [ ] Required NuGet packages referenced153- [ ] Build succeeds after migration154- [ ] Test files updated with appropriate test doubles155- [ ] No behavioral changes introduced (wrapper delegates directly to the static)156157## Common Pitfalls158159| Pitfall | Solution |160|---------|----------|161| Replacing statics in test code | Only replace in production code; tests should use fakes/mocks |162| Breaking static classes | Static classes can't have constructors — use ambient context for these |163| Missing `FakeTimeProvider` NuGet | Add `Microsoft.Extensions.TimeProvider.Testing` to test project |164| Replacing in expression-bodied members without updating return type | `DateTime` → `DateTimeOffset` when using `TimeProvider.GetUtcNow()` — verify type compatibility |165| Migrating too much at once | Stick to the defined scope — one project or namespace per run |166| Forgetting DI registration | Always verify `Program.cs`/`Startup.cs` has the registration before replacing call sites |