migrate-static-to-wrapper
ALWAYS USE when asked to migrate, replace, or make testable existing C# static calls with a named wrapper or built-in abstraction: DateTime.UtcNow/Now or DateTimeOffset.UtcNow to TimeProvider/IClock, File.* to IFileSystem or an existing store such as ITextFileStore, and Environment.* to an existing…
Install / Use
npx skills add dotnet/skills --skill migrate-static-to-wrapperInstalls into whichever agent you are using.
SKILL.md
Installable skill definition
Quality Score
Category
Development & EngineeringSupported Platforms
Tags
Our assessment of migrate-static-to-wrapper
migrate-static-to-wrapper scores 87/100 on our quality scale, 888th of 2,398 Development & Engineering skills we index (top 38%).
Its SKILL.md is 20 KB long, well organised into 22 sections with 3 code examples: a thorough specification that gives an agent plenty to work with.
With 5,471 GitHub stars, it is one of the more widely adopted skills in the catalogue.
Maintenance, license and trust
- The repository was last updated 2 days ago, so migrate-static-to-wrapper is actively maintained.
- It is released under the MIT license, a permissive license that allows use, modification and commercial use with attribution.
- Its trust signals score 100/100, with no cautions. These come from repository metadata, not a code audit — read the skill file before letting an agent act on it.
migrate-static-to-wrapper compared with similar skills
All 4 of these similar skills score higher than migrate-static-to-wrapper; compare them before choosing.
| Skill | Score | Stars | Updated | Format |
|---|---|---|---|---|
| migrate-static-to-wrapper (this skill)by dotnet | 87 | 5.5k | 2d ago | SKILL.md |
| ai-job-searchby MadsLorentzen | 100 | 44.0k | 5d ago | CLAUDE.md |
| claude-howtoby luongnv89 | 100 | 41.7k | today | CLAUDE.md |
| algorithmic-artby anthropics | 100 | 177.9k | 4d ago | SKILL.md |
| pptxby anthropics | 100 | 177.9k | 4d ago | SKILL.md |
Frequently asked questions
- How do I install migrate-static-to-wrapper?
- Run
npx skills add dotnet/skills --skill migrate-static-to-wrapper. The install tabs above show the steps for each supported agent. - Which AI agents does migrate-static-to-wrapper work with?
- It is written for Universal, as a SKILL.md file. Other agents that read the same format can often use it too.
- Is migrate-static-to-wrapper safe to use?
- It is MIT-licensed and scores 100/100 on trust signals. Skills are instructions an agent will follow, so read the file before installing it and do not approve commands you do not understand.
- Is migrate-static-to-wrapper still maintained?
- The repository was last updated 2 days ago, so migrate-static-to-wrapper is actively maintained.
Skill content
View source on GitHubname: migrate-static-to-wrapper description: > ALWAYS USE when asked to migrate, replace, or make testable existing C# static calls with a named wrapper or built-in abstraction: DateTime.UtcNow/Now or DateTimeOffset.UtcNow to TimeProvider/IClock, File.* to IFileSystem or an existing store such as ITextFileStore, and Environment.* to an existing reader such as IEnvironmentReader. Covers scoped files/projects, constructor injection, replacing temp-file or process-environment tests with fakes, "already registered" abstractions, and static classes whose callers/signatures must stay unchanged. Preserves DateTimeKind and call count. DO NOT USE for finding statics (detect-static-dependencies), choosing/designing a new wrapper (generate-testability-wrappers), behavior tests with no chosen seam (testability-obstacle), or test-framework migration. license: MIT
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
- Making a
staticutility class testable by adding an ambient seam (Step 3) while its existing call sites keep compiling unchanged - Incremental migration: one project or namespace at a time
- Updating affected tests with fakes when the requested migration names the replacement abstraction
When Not to Use
- No wrapper or abstraction exists yet and one must be designed from scratch (use
generate-testability-wrappersfirst). A built-in abstraction such asTimeProviderorIFileSystemalways counts as existing. - The user wants to detect statics, not migrate them (use
detect-static-dependencies) - Migrating between test frameworks (use the appropriate migration skill)
- The user primarily asks for a deterministic behavior test and has not selected
the production seam (use
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.
Inputs
| Input | Required | Description |
|-------|----------|-------------|
| Static pattern | No | Infer from the request and discovered call sites (e.g., DateTime.UtcNow, File.ReadAllText) |
| Replacement abstraction | No | Infer from the request and existing project abstractions; stop only when no named/existing abstraction is available |
| Scope | No | Infer from the requested file/project/namespace, otherwise discover the narrowest relevant workspace scope |
| Injection strategy | No | constructor (default), primary-constructor, or ambient |
Workflow
Non-negotiable migration boundaries
- Missing abstraction means stop. If the named interface/package is absent and the request only authorizes call-site replacement, do not add a package, invent a local lookalike interface, or edit production code. Report the exact missing prerequisite and the authorization needed to continue.
- One source read stays one replacement read. Do not hoist or coalesce calls, even when sharing a captured timestamp looks cleaner.
- The requested scope is exhaustive and exclusive. Replace every named call in scope and no adjacent member or file.
- Repository-backed requests require repository work. Start by discovering files from the current workspace. Do not claim the repository is unavailable or ask the user for a path or file contents until workspace-relative discovery found no target. Do not say work was implemented unless the diff proves it.
- Discovered workspace files must be completed in this turn when permitted.
Use a host-native shell reader (
sed/catorGet-Content) only after a confirmed reader availability, transport, or path-normalization failure and only after verifying the canonical path remains inside the current workspace. Stop on content-exclusion, permission/policy, workspace-boundary, or unknown read failures. Use a shell edit fallback only for a confirmed editor availability, transport, or path-normalization failure, never for a stale context, concurrent change, permission/policy denial, or path-boundary error. Before fallback, resolve the canonical path inside the current workspace, freshly read the file, and require an anchored replacement with the expected old text and exact match count; abort if either changed. Then re-open the file, inspect the diff, and validate. Do not ask the user to paste a readable discovered file or report a proposed patch as completed work.
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+ orMicrosoft.Bcl.TimeProvideris referenced. ForSystem.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
.csfiles that will be modified. Exclude test projects,obj/,bin/, and generated code. -
Lock and count the member set before editing: Use the exact member named by the user, or infer the smallest unambiguous set from the request and discovered call sites. Record that set, then search every member and capture the file/line inventory. Do not change the set mid-edit or infer counts from a partial read.
Step 2: Plan the migration for each file
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. Preserve a call site whose comment or name marks it as deliberate (e.g. // intentional local time) unless the user explicitly names that site and requests a semantics-preserving migration. 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:
- 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().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.
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 readonlyfield + constructor parameter, matching the existing field naming convention (_camelCaseorm_camelCase)
Static classes: use ambient context (no constructor injection)
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;
Truncated for display — read the full file on GitHub.
Related Skills
ai-job-search
44.0kThe job search that runs on your machine. AI job application framework built on Claude Code: evaluate postings, tailor CVs, write cover letters, prep interviews. Fork it and own it.
claude-howto
41.7kA visual, example-driven guide to Claude Code — from basic concepts to advanced agents, with copy-paste templates that bring immediate value.
algorithmic-art
177.9kCreating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems.
pptx
177.9kUse this skill any time a .pptx or .potx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx or .potx file (even if the extracted content will be used elsewhere, like in an em…
Languages
Trust signals
From repository metadata: license, adoption, age and documentation. Not a code audit — see the Safety scan above for what the skill file itself contains.
