> Generate wrapper interfaces and DI registration for hard-to-test static dependencies in C#, when the abstraction does NOT exist yet. Produces IFileSystem, IEnvironmentProvider, IConsole, IProcessRunner wrappers, or guides first-time adoption of TimeProvider and IHttpClientFactory. With no DI container, produces the ambient context seam instead. make a static or a class testable, create abstraction for File.*, generate DI registration, adopt TimeProvider when it is not registered yet, IHttpClientFactory setup, testability wrapper, how to make statics injectable, adopt System.IO.Abstractions, make code testable without adding a DI framework. call sites or replacing existing DateTime.*/File.* usages once the wrapper is created or already registered in DI (use migrate-static-to-wrapper), general interface design.
npx skills add https://github.com/dotnet/skills --skill generate-testability-wrappers
Generate wrapper interfaces, default implementations, and DI service registration code for untestable static dependencies. For statics that already have .NET built-in abstractions (TimeProvider, IHttpClientFactory), guide adoption of the built-in. For statics without built-in alternatives, generate custom minimal wrappers.
detect-static-dependencies and identifying which statics to wrapTimeProvider (.NET 8+) or System.IO.AbstractionsEnvironment.*, Console.*, or Process.*detect-static-dependencies)migrate-static-to-wrapper)> A project with no DI container, or a user who does not want to add one, is not a reason to skip this skill —
> that is exactly what the ambient context seam in Step 5 is for. Choose the seam over constructor injection in that
> case; do not decline the request and do not propose registering anything in a service collection.
| Input | Required | Description |
|-------|----------|-------------|
| Static category | Yes | Which category: time, filesystem, environment, network, console, process |
| Target framework | Yes | The TargetFramework from .csproj (affects which built-in abstractions exist) |
| DI container | No | Which DI framework: microsoft (default), autofac, none (ambient context) |
| Namespace | No | Target namespace for generated wrapper code |
Based on the category and target framework:
| Category | .NET 8+ | .NET 6-7 | .NET Framework |
|----------|---------|----------|----------------|
| Time | TimeProvider (built-in) | TimeProvider via Microsoft.Bcl.TimeProvider NuGet | Custom ISystemClock |
| File system | System.IO.Abstractions (NuGet) | Same | Same |
| HTTP | IHttpClientFactory (built-in) | Same | Same |
| Environment | Custom IEnvironmentProvider | Same | Same |
| Console | Custom IConsole | Same | Same |
| Process | Custom IProcessRunner | Same | Same |
The table picks *which abstraction*. How it reaches the code under test is a separate axis: constructor
injection when a DI container exists, and the ambient context seam of Step 5 when one does not. Decide that
axis first — check for a host builder, IServiceCollection, or an existing container registration — because a
static class cannot take a constructor and a project without a container has nowhere to register anything. In
that case skip Steps 2–4 and go to Step 5; the abstraction chosen above still applies, it is just reached through
the ambient seam.
No wrapper code needed — guide the user:
builder.Services.AddSingleton(TimeProvider.System);
public class OrderProcessor(TimeProvider timeProvider)
{
public bool IsExpired(Order order)
=> timeProvider.GetUtcNow() > order.ExpiresAt;
}
FakeTimeProvider:// Requires Microsoft.Extensions.TimeProvider.Testing NuGet
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2026, 1, 15, 0, 0, 0, TimeSpan.Zero));
var processor = new OrderProcessor(fakeTime);
fakeTime.Advance(TimeSpan.FromDays(1));
Assert.True(processor.IsExpired(order));
Guide: install Microsoft.Bcl.TimeProvider NuGet. Same API as above.
No wrapper code needed — register typed clients via builder.Services.AddHttpClient<MyService>() and inject HttpClient directly into the class constructor.
For categories without built-in abstractions, follow this template:
Only include methods that were actually detected in the codebase. Do NOT generate a wrapper for every possible member — wrap only what is used.
namespace <Namespace>;
/// <summary>
/// Abstraction over <static class> for testability.
/// </summary>
public interface I<WrapperName>
{
// One method per detected static call
<return type> <MethodName>(<parameters>);
}
namespace <Namespace>;
/// <summary>
/// Default implementation that delegates to <static class>.
/// </summary>
public sealed class <WrapperName> : I<WrapperName>
{
public <return type> <MethodName>(<parameters>)
=> <StaticClass>.<Method>(<arguments>);
}
// In Program.cs or Startup.cs:
builder.Services.AddSingleton<I<WrapperName>, <WrapperName>>();
Prefer the established System.IO.Abstractions NuGet package over custom wrappers:
dotnet add package System.IO.Abstractions
builder.Services.AddSingleton<IFileSystem, FileSystem>();
IFileSystem into classes:public class ConfigLoader(IFileSystem fileSystem)
{
public string LoadConfig(string path)
=> fileSystem.File.ReadAllText(path);
}
MockFileSystem:dotnet add <TestProject> package System.IO.Abstractions.TestingHelpers
var mockFs = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ "/config.json", new MockFileData("{\"key\": \"value\"}") }
});
var loader = new ConfigLoader(mockFs);
Assert.Equal("{\"key\": \"value\"}", loader.LoadConfig("/config.json"));
If the codebase does not use DI (e.g., old console app, library code), offer the ambient context pattern:
public static class Clock
{
private static readonly AsyncLocal<Func<DateTimeOffset>?> s_override = new();
public static DateTimeOffset UtcNow
=> s_override.Value?.Invoke() ?? TimeProvider.System.GetUtcNow();
public static IDisposable Override(DateTimeOffset fixedTime)
{
s_override.Value = () => fixedTime;
return new Scope();
}
private sealed class Scope : IDisposable
{
public void Dispose() => s_override.Value = null;
}
}
Key trade-offs: AsyncLocal<T> ensures parallel tests don't interfere; production cost is one null check per call; the static readonly field is essentially free.
Three properties this pattern must keep, because each has broken a real migration:
IDisposable that restores the previous value, so a test cannot leak a pinned time into the next one. A bare setter, or a manual try/finally at each call site, puts that burden on every test author.AsyncLocal<T>, never [ThreadStatic]. [ThreadStatic] does not flow across await, so the override silently disappears mid-test.DateTime.UtcNow with a local-time source changes the DateTimeKind every existing caller and stored value depends on — pair UtcNow with GetUtcNow(), and Now with GetLocalNow().The same shape works for non-time statics: swap TimeProvider.System.GetUtcNow() for the real static call and keep the override slot, the disposable scope, and the original semantics.
Generate files following the project's existing conventions:
Abstractions/ or Interfaces/ folder, place the interface thereInfrastructure/ or Services/ folder, place the implementation thereAlways generate:
AddSingleton for stateless wrappers, AddTransient for stateful onesTimeProvider is recommended over custom ISystemClockAsyncLocal<T>, a scoped IDisposable that restores the previous value, and trade-off explanationIServiceCollection registration is proposed and the replaced member's semantics (UtcNow vs Now, and its DateTimeKind) are preserved| Pitfall | Solution |
|---------|----------|
| Declining because the project has no DI container | The ambient seam in Step 5 is the answer for that case — offer it instead of asking the user to adopt a container |
| Wrapping ALL members of a static class | Only wrap methods actually called in the codebase |
| Custom time wrapper on .NET 8+ | Use built-in TimeProvider instead |
| Custom file system wrapper | Prefer System.IO.Abstractions NuGet — battle-tested, complete |
| Registering scoped when singleton suffices | Stateless wrappers should be AddSingleton |
| Forgetting test helper packages | Microsoft.Extensions.TimeProvider.Testing for time, System.IO.Abstractions.TestingHelpers for filesystem |
| Ambient context without AsyncLocal | Non-async [ThreadStatic] breaks with async/await — always use AsyncLocal<T> |
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
Automatically creates user-facing changelogs from git commits by analyzing commit history, categorizing changes, and transforming technical commits into clear, customer-friendly release notes. Turns hours of manual changelog writing into minutes of automated generation.
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
React Native and Expo best practices for building performant mobile apps. Use when building React Native components, optimizing list performance, implementing animations, or working with native modules. Triggers on tasks involving React Native, Expo, mobile performance, or native platform APIs.
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling
Use when starting feature work that needs isolation from current workspace or before executing implementation plans - creates isolated git worktrees with smart directory selection and safety verification
Take dotnet/generate-testability-wrappers from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.