Best for
- 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
dotnet/skills/plugins/dotnet-test/skills/migrate-static-to-wrapper/SKILL.md
Migrate C# static calls to a wrapper or built-in abstraction the user already named, within named files/projects, including affected fake-based test updates. USE FOR explicit DateTime.UtcNow/Now to TimeProvider, File.* to IFileSystem, existing IEnvironmentReader/ITextFileStore, scoped migrations, constructor injection, or a static API seam that keeps callers compiling and DateTimeKind unchanged. DO NOT USE when the user asks for behavior tests but leaves seam selection open (testability-obstacle
Decision brief
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.
Compatibility matrix
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/dotnet/skills --skill "plugins/dotnet-test/skills/migrate-static-to-wrapper"Inspect the Agent Skill "migrate-static-to-wrapper" from https://github.com/dotnet/skills/blob/2b9056bd9152490cc698c5b3e61c9f9a1c135776/plugins/dotnet-test/skills/migrate-static-to-wrapper/SKILL.md at commit 2b9056bd9152490cc698c5b3e61c9f9a1c135776. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
Before modifying any code:
Before modifying any code:
Migrate exactly what was asked — nothing adjacent. If the user named a member (DateTime.UtcNow), migrate only that member and leave siblings such as DateTime.Now untouched. If the user named files, do not touch other files. Never migrate a call site whose comment or name marks i…
Add the new dependency following the class's existing pattern:
Perform each replacement mechanically. For each call site:
Permission review
The documentation asks the agent to read local files, directories, or repositories.
in-scope production file. Re-open each changed file and compare the result toEvidence record
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 97/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 5,277 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
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 namespacestatic utility class testable by adding an ambient seam (Step 3) while its existing call sites keep
compiling unchangedgenerate-testability-wrappers first).
A built-in abstraction such as TimeProvider or IFileSystem always counts as existing.detect-static-dependencies)testability-obstacle)A class that is
static, or a project with no DI container, is not a reason to skip this skill — that is exactly what the ambient seam in Step 3 is for. Use it whenever the call sites must keep compiling unchanged.
| 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:
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. A package that could provide an abstraction is not the same as an abstraction already available to this project.
Confirm production composition exists: Check Program.cs, Startup.cs, or manual construction sites. If package, wrapper, or registration work is missing, add it only when the user explicitly authorized those dependency/composition changes. Otherwise stop before editing call sites and report the exact prerequisite; do not turn a scoped migration into first-time abstraction design.
Identify all files in scope: List the .cs files that will be modified. Exclude test projects, obj/, bin/, and generated code.
Count every in-scope occurrence before editing: Search the exact member named by the user and record its file/line inventory. Do not infer the count from a partial read or from how many methods were initially noticed.
Migrate exactly what was asked — nothing adjacent. If the user named a member (DateTime.UtcNow), migrate only that member and leave siblings such as DateTime.Now untouched. If the user named files, do not touch other files. Never migrate a call site whose comment or name marks it as deliberate (e.g. // intentional local time). List everything you deliberately left alone under "Remaining (out of scope)" so the user can ask for it in a follow-up; suggesting is fine, silently widening the scope is not.
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 aDateTimeOffset. Converting back toDateTimemust keep the originalKind, otherwise you introduce a behavioral change even though the code still compiles:
DateTime.UtcNowhasKind == Utc→ use.UtcDateTime(not.DateTime, which yieldsKind == Unspecified).DateTime.NowhasKind == Local→ use.LocalDateTime(not.DateTime).- When a call site consumes a
DateTimeOffsetdirectly (a field/parameter/return already typedDateTimeOffset), drop the.UtcDateTime/.LocalDateTimesuffix and assign theDateTimeOffsetas-is — don't force it back throughDateTime.Match the target member's type: if the surrounding field/property is
DateTime, keep itDateTime(via the Kind-correct property above); do not change it toDateTimeOffsetas part of a "mechanical" migration — that is a design change, not a delegation.Preserve the number, order, and location of reads as well as the value type. Replace each original clock read in place with one provider read. Do not hoist, cache, or coalesce two reads into a shared
nowlocal, even when they are in the same object initializer or method. Two consecutiveDateTime.UtcNowcalls could observe different instants; makingCreatedAtandExpiresAtderive from one captured value is a behavior change, not a mechanical migration. Reuse a value only when the original code already captured and reused one.
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 a scoped ambient seam that defaults to the real implementation and can be overridden without leaking process-global state.
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.
public static class TimestampFormatter
{
private static readonly AsyncLocal<TimeProvider?> s_clock = new();
private static TimeProvider Clock => s_clock.Value ?? TimeProvider.System;
public static string Now() => Clock.GetUtcNow().ToString("O");
public static IDisposable OverrideClock(TimeProvider clock)
{
ArgumentNullException.ThrowIfNull(clock);
var previous = s_clock.Value;
s_clock.Value = clock;
return new Scope(() => s_clock.Value = previous);
}
private sealed class Scope : IDisposable
{
private Action? _restore;
public Scope(Action restore)
{
_restore = restore;
}
public void Dispose() => Interlocked.Exchange(ref _restore, null)?.Invoke();
}
}
TimeProvider.System whenever no override is active; no startup mutation is required.AsyncLocal<T> keeps independently established test flows isolated across await. Do not store a mutable stack/list in the slot or mutate one fake inherited by multiple child flows.IFileSystem, custom wrappers): store the abstraction value in AsyncLocal<T>, default to the real implementation, and restore the previous value from the scope.Perform each replacement mechanically. For each call site:
using directives if not already presentAfter editing, repeat the exact search and require zero occurrences in every in-scope production file. Re-open each changed file and compare the result to the pre-edit inventory. A summary count is not evidence if one method was silently missed.
| 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 fakePreserve every observable branch that depended on the original static result. For
example, migrating Environment.GetEnvironmentVariable(name) ?? "production"
requires tests for both a configured value and null/missing input selecting the
fallback. A fake-only happy path is not enough to prove a mechanical migration.
After all changes in the current scope, build the affected production project and run the narrowest affected test project whenever tests exist or were changed:
dotnet build <project.csproj>
dotnet test <affected-test-project.csproj>
Report the build result you actually observed. Only write "build succeeded" when the command exited 0; if it failed — including restore/NuGet failures such as "assets file not found" — say so, quote the error, and either fix it (dotnet restore, add the missing package) or hand the user a precise blocker. A false success claim is worse than an unfinished migration.
If the build fails:
using directiveDo not substitute a successful build for the requested test run. When migration changes constructor calls, fakes, process-global state, or real I/O, only the targeted tests prove the complete path. If the test command is blocked, report that blocker rather than claiming the migration is fully validated.
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. |
| Capturing one provider value for multiple original clock reads | Replace each read in place. Coalescing reads changes observable timing even when it looks cleaner. |
| Migrating too much at once | Stick to the defined scope — one project or namespace per run |
Migrating DateTime.Now when only UtcNow was requested | Respect the literal request; list the other call sites as out-of-scope suggestions instead of rewriting them |
| Claiming "Build succeeded" after a failed restore | Read the exit code and output; report the real failure and fix it or surface it as a blocker |
| Adding a package during a call-site-only migration | Stop and request authorization or run wrapper/adoption setup first |
| Forgetting production composition | Verify DI registration, manual construction, or the ambient production default before replacing call sites |
Frequently asked questions
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.
The source record exposes this install command: npx skills add https://github.com/dotnet/skills --skill "plugins/dotnet-test/skills/migrate-static-to-wrapper". Inspect the command and pinned source before running it.
Static rules flagged read-files in the source; the page lists the matching lines and excerpts.
Alternatives
alirezarezvani/claude-skills
App Store Optimization (ASO) toolkit for researching keywords, analyzing competitor rankings, generating metadata suggestions, and improving app visibility on Apple App Store and Google Play Store. Use when the user asks about ASO, app store rankings, app metadata, app titles and descriptions, app store listings, app visibility, or mobile app marketing on iOS or Android. Supports keyword research and scoring, competitor keyword analysis, metadata optimization, A/B test planning, launch checklist
dotnet/skills
Classifies existing tests by standard traits and reports their distribution. MUST USE to categorize/tag/label tests, compare happy vs error paths, audit the test mix, or describe coverage shape by test type. Read bodies when names mislead. Apply canonical attributes; otherwise report only. DO NOT USE for test-quality audits, executed coverage or CRAP, behavioral gaps, writing tests, or migration.
yonatangross/orchestkit
Grade work that already exists and decide whether it can merge. Runs the project's current unit, integration, and E2E suites plus security scanning and type checking, scores every dimension 0-10, and returns a merge verdict with a VERIFIED-vs-CLAIMED evidence manifest. Writes no test files and edits no source. Use when verifying changes are ready to merge. Use /ork:cover instead when the tests still have to be written.
PramodDutta/qaskills
Gate RAG pipelines in CI with versioned golden eval sets, per-metric thresholds, baseline drift detection, and a build that fails when retrieval or answer quality regresses.