Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Replace existing static dependency call sites with a wrapper or built-in abstraction that already exists or is registered in DI. Codemod-style bulk replacement of DateTime.Now/UtcNow to TimeProvider, File.ReadAllText to IFileSystem, and similar, across a bounded scope (file, project, namespace), adding constructor injection to affected classes and updating their unit tests to use a test double. USE FOR: replace DateTime.UtcNow/DateTime.Now with TimeProvider and add the constructor parameter, mig
.claude/skills/managedcode-migrate-static-to-wrapper/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 302% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 72% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 396% | 0% |
| case-21 | ✗→✓ | ▲ Improved | 91% | 0% |
| case-02 | ✓→✗ | ▼ Worse | 208% | 0% |
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.
generate-testability-wrappers) or built-in abstractions identifiedDateTime.UtcNow → TimeProvider.GetUtcNow() across a projectFile.* → IFileSystem.File.* across a namespacegenerate-testability-wrappers first)detect-static-dependencies)| 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 |
Before modifying any code:
TimeProvider, verify the target framework is .NET 8+ or Microsoft.Bcl.TimeProvider is referenced. For System.IO.Abstractions, verify the NuGet package is referenced.Program.cs or Startup.cs for the service registration. If missing, add it before proceeding..cs files that will be modified. Exclude test projects, obj/, bin/, and generated code.For each file containing the static pattern, determine:
TimeProvider, IFileSystem, etc. parameters| Category | Original | DI replacement | |----------|----------|----------------| | Time | DateTime.Now | _timeProvider.GetLocalNow().LocalDateTime | | Time | DateTime.UtcNow | _timeProvider.GetUtcNow().UtcDateTime | | Time | DateTime.Today | _timeProvider.GetLocalNow().LocalDateTime.Date | | Time | DateTimeOffset.Now | _timeProvider.GetLocalNow() | | 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.
> Preserve DateTimeKind — this is the most common silent regression. TimeProvider.GetUtcNow() / GetLocalNow() return a DateTimeOffset. Converting back to DateTime must keep the original Kind, otherwise you introduce a behavioral change even though the code still compiles: > > - DateTime.UtcNow has Kind == Utc → use .UtcDateTime (not .DateTime, which yields Kind == Unspecified). > - DateTime.Now has Kind == Local → use .LocalDateTime (not .DateTime). > - When a call site consumes a DateTimeOffset directly (a field/parameter/return already typed DateTimeOffset), drop the .UtcDateTime/.LocalDateTime suffix and assign the DateTimeOffset as-is — don't force it back through DateTime. > > Match the target member's type: if the surrounding field/property is DateTime, keep it DateTime (via the Kind-correct property above); do not change it to DateTimeOffset as part of a "mechanical" migration — that is a design change, not a delegation.
Add the new dependency following the class's existing pattern:
public class OrderProcessor(ILogger<OrderProcessor> logger, TimeProvider timeProvider)private readonly field + constructor parameter, matching the existing field naming convention (_camelCase or m_camelCase)A static class with only static members cannot receive constructor injection — adding an instance constructor or instance field would break it. Do not convert it to a non-static class just to inject the dependency; that changes its design and every call site. Instead, apply the ambient context pattern: expose a static, settable seam that defaults to the real implementation and is overridden once at composition/test setup.
When the user wants to keep the class static, the ambient seam below is the answer — present it as the solution and implement it directly. Do not hedge by offering "convert it to a non-static class" or "pass TimeProvider as a method parameter" as co-equal alternatives; those change the class's design or public API and are not what was asked. Lead with the seam, then note the parallelism trade-off.
csharppublic static class TimestampFormatter { // Ambient seam — defaults to the real clock, swap in tests. public static TimeProvider Clock { get; set; } = TimeProvider.System; public static string Now() => Clock.GetUtcNow().ToString("O"); }
Clock at its TimeProvider.System default, or assign the DI-resolved TimeProvider once at startup (TimestampFormatter.Clock = app.Services.GetRequiredService<TimeProvider>();).Clock with a FakeTimeProvider and always restore it in a finally so a failing assertion can't leak the fake into other tests:csharp var original = TimestampFormatter.Clock; TimestampFormatter.Clock = new FakeTimeProvider(instant); try { // exercise code under test } finally { TimestampFormatter.Clock = original; }
[Collection] with parallelization disabled, or MSTest [DoNotParallelize]). Only if the class is not required to stay static and its tests must run fully parallel should you consider converting the caller to an instance with constructor injection instead — otherwise keep the ambient seam.IFileSystem, custom wrappers): a public static <Abstraction> X { get; set; } defaulting to the real implementation, with the same restore-in-finally and non-parallel discipline.Perform each replacement mechanically. For each call site:
using directives if not already present| 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>; |
If test files exist for the migrated classes:
TimeProvider → new FakeTimeProvider() from Microsoft.Extensions.TimeProvider.TestingIFileSystem → new MockFileSystem() from System.IO.Abstractions.TestingHelpersnew Mock<IWrapperName>() or hand-rolled fakeAfter all changes in the current scope:
bashdotnet build <project.csproj>
If the build fails:
using directivedotnet add package <name>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)using directives addedDateTimeKind preserved — former DateTime.UtcNow stays Utc (.UtcDateTime), former DateTime.Now stays Local (.LocalDateTime)| 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 the ambient context seam (Step 3) instead of converting them to non-static | | Missing FakeTimeProvider NuGet | Add Microsoft.Extensions.TimeProvider.Testing to test project | | Replacing a DateTime value with .DateTime off a DateTimeOffset | DateTimeOffset.DateTime returns Kind == Unspecified — use .UtcDateTime (for former DateTime.UtcNow) or .LocalDateTime (for former DateTime.Now) to preserve the original DateTimeKind. Only change the field/return type to DateTimeOffset if the user asked for it. | | 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 |
Other measured skills in the registry, with their headline benchmark lift.