Make .NET projects compatible with Native AOT and trimming by systematically resolving IL trim/AOT analyzer warnings.
Migrate Static to Wrapper
Mechanically replace static dependency call sites with wrapper or built-in abstraction calls across a bounded scope (file, project, or namespace). Performs codemod-style bulk replacement of DateTime.UtcNow to TimeProvider.GetUtcNow(), File.ReadAllText to IFileSystem, and similar transformations. Adds constructor injection parameters and updates DI registration. USE FOR: replace DateTime.UtcNow with TimeProvider, replace DateTime.Now with TimeProvider, migrate static calls to wrapper, bulk replace File.* with IFileSystem, codemod static to injectable, add constructor injection for time provider, mechanical migration of statics, refactor DateTime to TimeProvider, swap static for injected dependency, convert static calls to use abstraction, replace statics in a class, migrate one file to TimeProvider, scoped migration, update call sites. DO NOT USE FOR: detecting statics (use detect-static-dependencies), generating wrappers (use generate-testability-wrappers), migrating between test frameworks.
Workflow
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.
- Confirm DI registration exists: Check
Program.csorStartup.csfor the service registration. If missing, add it before proceeding.
- Identify all files in scope: List the
.csfiles that will be modified. Exclude test projects,obj/,bin/, and generated code.
Step 2: Plan the migration for each file
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().DateTime | | Time | DateTime.UtcNow | _timeProvider.GetUtcNow().DateTime | | Time | DateTime.Today | _timeProvider.GetLocalNow().Date | | 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.
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)
Step 4: Replace call sites
Perform each replacement mechanically. For each call site:
- Replace the static call with the wrapper call
- Preserve the surrounding code structure (whitespace, comments, chaining)
- Add required
usingdirectives if not already present
#### Adding using directives
| 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>; |
Step 5: Update affected test files
If test files exist for the migrated classes:
- Update constructor calls — add the new parameter to test class instantiation
- Use test doubles:
- TimeProvider → new FakeTimeProvider() from Microsoft.Extensions.TimeProvider.Testing - IFileSystem → new MockFileSystem() from System.IO.Abstractions.TestingHelpers - Custom wrappers → new Mock<IWrapperName>() or hand-rolled fake
Step 6: Build verification
After all changes in the current scope:
dotnet build <project.csproj>
If the build fails:
- Missing using: Add the required
usingdirective - Missing NuGet package: Run
dotnet add package <name> - Constructor mismatch in tests: Update test instantiation (Step 5)
- Ambiguous call: Fully qualify the wrapper call
Step 7: Report changes
Summarize what was done:
Related skills
Migrate a .NET 10 project or solution to .NET 11 and resolve all breaking changes.
Migrate a .NET 8 project to .NET 9 and resolve all breaking changes.