{
  "schema": "https://ai-atoms.com/schemas/skill-v1.json",
  "type": "skill",
  "id": "skill/migrate-static-to-wrapper",
  "version": "1.0.1",
  "name": "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, and Environment.* to an existing reader. Covers scoped files/projects, constructor injection, updating 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.",
  "system_prompt_fragment": "# Migrate Static to Wrapper\n\nPerform 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.\n\n## When to Use\n\n- After wrappers have been generated (via `generate-testability-wrappers`) or built-in abstractions identified\n- Migrating `DateTime.UtcNow` → `TimeProvider.GetUtcNow()` across a project\n- Migrating `File.*` → `IFileSystem.File.*` across a namespace\n- Adding constructor injection for the new abstraction to affected classes\n- Incremental migration: one project or namespace at a time\n\n## When Not to Use\n\n- No wrapper or abstraction exists yet (use `generate-testability-wrappers` first)\n- The user wants to detect statics, not migrate them (use `detect-static-dependencies`)\n- The code does not use dependency injection and the user hasn't chosen ambient context\n- Migrating between test frameworks (use the appropriate migration skill)\n\n## Inputs\n\n| Input | Required | Description |\n|-------|----------|-------------|\n| Static pattern | Yes | What to replace (e.g., `DateTime.UtcNow`, `File.ReadAllText`) |\n| Replacement abstraction | Yes | What to use instead (e.g., `TimeProvider`, `IFileSystem`) |\n| Scope | Yes | File path, project (.csproj), namespace, or directory to migrate |\n| Injection strategy | No | `constructor` (default), `primary-constructor`, or `ambient` |\n\n## Workflow\n\n### Step 1: Verify prerequisites\n\nBefore modifying any code:\n\n1. **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.\n\n2. **Confirm DI registration exists**: Check `Program.cs` or `Startup.cs` for the service registration. If missing, add it before proceeding.\n\n3. **Identify all files in scope**: List the `.cs` files that will be modified. Exclude test projects, `obj/`, `bin/`, and generated code.\n\n### Step 2: Plan the migration for each file\n\nFor each file containing the static pattern, determine:\n\n1. **Which class(es) contain the call sites** — identify the class declarations\n2. **Whether the class already has the dependency injected** — check constructors for existing `TimeProvider`, `IFileSystem`, etc. parameters\n3. **The replacement expression** for each call site\n\n#### Replacement mapping\n\n| Category | Original | DI replacement |\n|----------|----------|----------------|\n| Time | `DateTime.Now` | `_timeProvider.GetLocalNow().DateTime` |\n| Time | `DateTime.UtcNow` | `_timeProvider.GetUtcNow().DateTime` |\n| Time | `DateTime.Today` | `_timeProvider.GetLocalNow().Date` |\n| Time | `DateTimeOffset.UtcNow` | `_timeProvider.GetUtcNow()` |\n| File | `File.ReadAllText(path)` | `_fileSystem.File.ReadAllText(path)` |\n| File | `File.WriteAllText(path, text)` | `_fileSystem.File.WriteAllText(path, text)` |\n| File | `File.Exists(path)` | `_fileSystem.File.Exists(path)` |\n| File | `Directory.Exists(path)` | `_fileSystem.Directory.Exists(path)` |\n| Env | `Environment.GetEnvironmentVariable(name)` | `_env.GetEnvironmentVariable(name)` |\n| Console | `Console.WriteLine(msg)` | `_console.WriteLine(msg)` |\n| Process | `Process.Start(info)` | `_processRunner.Start(info)` |\n\nApply the same pattern for other members in each category.\n\n### Step 3: Add constructor injection\n\nAdd the new dependency following the class's existing pattern:\n\n- **Primary constructor** (C# 12+): Add parameter to primary constructor: `public class OrderProcessor(ILogger<OrderProcessor> logger, TimeProvider timeProvider)`\n- **Traditional constructor**: Add `private readonly` field + constructor parameter, matching the existing field naming convention (`_camelCase` or `m_camelCase`)\n\n### Step 4: Replace call sites\n\nPerform each replacement mechanically. For each call site:\n\n1. Replace the static call with the wrapper call\n2. Preserve the surrounding code structure (whitespace, comments, chaining)\n3. Add required `using` directives if not already present\n\n#### Adding using directives\n\n| Abstraction | Using directive |\n|------------|-----------------|\n| `TimeProvider` | None (in `System` namespace) |\n| `IFileSystem` | `using System.IO.Abstractions;` |\n| `IHttpClientFactory` | `using System.Net.Http;` (usually already present) |\n| Custom wrappers | `using <wrapper namespace>;` |\n\n### Step 5: Update affected test files\n\nIf test files exist for the migrated classes:\n\n1. **Update constructor calls** — add the new parameter to test class instantiation\n2. **Use test doubles**:\n   - `TimeProvider` → `new FakeTimeProvider()` from `Microsoft.Extensions.TimeProvider.Testing`\n   - `IFileSystem` → `new MockFileSystem()` from `System.IO.Abstractions.TestingHelpers`\n   - Custom wrappers → `new Mock<IWrapperName>()` or hand-rolled fake\n\n### Step 6: Build verification\n\nAfter all changes in the current scope:\n\n```bash\ndotnet build <project.csproj>\n```\n\nIf the build fails:\n- **Missing using**: Add the required `using` directive\n- **Missing NuGet package**: Run `dotnet add package <name>`\n- **Constructor mismatch in tests**: Update test instantiation (Step 5)\n- **Ambiguous call**: Fully qualify the wrapper call\n\n### Step 7: Report changes\n\nSummarize what was done:\n\n```\n## Migration Summary\n\n**Pattern**: DateTime.UtcNow → TimeProvider.GetUtcNow()\n**Scope**: MyProject/Services/\n\n### Files Modified (production)\n| File | Call Sites Replaced | Injection Added |\n|------|--------------------:|:----------------|\n| OrderProcessor.cs | 3 | Yes (constructor) |\n| NotificationService.cs | 1 | Yes (primary ctor) |\n\n### Files Modified (tests)\n| File | Change |\n|------|--------|\n| OrderProcessorTests.cs | Added FakeTimeProvider parameter |\n\n### Remaining (out of scope)\n- MyProject/Legacy/ — 8 call sites not migrated (different namespace)\n```\n\n## Validation\n\n- [ ] All call sites in scope were replaced (none missed)\n- [ ] Constructor injection added to all affected classes\n- [ ] Field naming follows existing class conventions\n- [ ] Required `using` directives added\n- [ ] Required NuGet packages referenced\n- [ ] Build succeeds after migration\n- [ ] Test files updated with appropriate test doubles\n- [ ] No behavioral changes introduced (wrapper delegates directly to the static)\n\n## Common Pitfalls\n\n| Pitfall | Solution |\n|---------|----------|\n| Replacing statics in test code | Only replace in production code; tests should use fakes/mocks |\n| Breaking static classes | Static classes can't have constructors — use ambient context for these |\n| Missing `FakeTimeProvider` NuGet | Add `Microsoft.Extensions.TimeProvider.Testing` to test project |\n| Replacing in expression-bodied members without updating return type | `DateTime` → `DateTimeOffset` when using `TimeProvider.GetUtcNow()` — verify type compatibility |\n| Migrating too much at once | Stick to the defined scope — one project or namespace per run |\n| Forgetting DI registration | Always verify `Program.cs`/`Startup.cs` has the registration before replacing call sites |",
  "applicable_domains": [
    "code",
    "dotnet",
    "engineering"
  ],
  "invocation": [
    "/migrate-static-to-wrapper"
  ],
  "tags": [
    "dotnet-test",
    "dotnet",
    "csharp",
    "microsoft"
  ],
  "authored_by": "anthropics",
  "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-test/skills/migrate-static-to-wrapper/SKILL.md",
  "lifecycle": "stable",
  "category": "dotnet",
  "provenance": {
    "source": "dotnet/skills",
    "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-test/skills/migrate-static-to-wrapper/SKILL.md",
    "author": "Microsoft / .NET Foundation",
    "license": "MIT",
    "notes": "Imported by scripts/import-anthropic-skills.py."
  }
}