Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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. USE FOR: generate wrapper for static, create IFileSystem wrapper, wrap DateTime.Now, make a static or a class testable, create abstraction for File.*, ge
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-10 | ✗→✓ | ▲ Improved | 153% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 99% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 156% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 61% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 71% | 0% |
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:
csharpbuilder.Services.AddSingleton(TimeProvider.System);
csharppublic class OrderProcessor(TimeProvider timeProvider) { public bool IsExpired(Order order) => timeProvider.GetUtcNow() > order.ExpiresAt; }
FakeTimeProvider:csharp// 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.
csharpnamespace <Namespace>; /// <summary> /// Abstraction over <static class> for testability. /// </summary> public interface I<WrapperName> { // One method per detected static call <return type> <MethodName>(<parameters>); }
csharpnamespace <Namespace>; /// <summary> /// Default implementation that delegates to <static class>. /// </summary> public sealed class <WrapperName> : I<WrapperName> { public <return type> <MethodName>(<parameters>) => <StaticClass>.<Method>(<arguments>); }
csharp// 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.Abstractionscsharpbuilder.Services.AddSingleton<IFileSystem, FileSystem>();
IFileSystem into classes:csharppublic class ConfigLoader(IFileSystem fileSystem) { public string LoadConfig(string path) => fileSystem.File.ReadAllText(path); }
MockFileSystem:dotnet add <TestProject> package System.IO.Abstractions.TestingHelperscsharpvar 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:
csharppublic 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> |
Other measured skills in the registry, with their headline benchmark lift.