Detect Static Dependencies
Scan a C# codebase for calls to hard-to-test static APIs and produce a ranked report showing which statics appear most frequently, which files are most affected, and which abstractions already exist in the .NET ecosystem to replace them.
When to Use
- Auditing a project's testability before adding unit tests
- Understanding the scope of static coupling in a legacy codebase
- Prioritizing which statics to wrap first (highest-frequency wins)
- Creating a migration plan for incremental testability improvements
Response Guidelines
- Scale the response to the user's request. A question about a specific category (e.g., "find time statics") should focus on that category with file locations and counts, not produce a full report across all categories.
- When the user provides a specific file or directory path, scan only that scope — do not expand to the entire solution unless asked.
- The full structured report format in Step 4 is for comprehensive audit requests. For focused questions, return only the relevant subset (e.g., category summary + affected files for the requested category).
When Not to Use
- The user wants wrappers generated (hand off to
generate-testability-wrappers)
- The user wants mechanical migration done (hand off to
migrate-static-to-wrapper)
- The statics are already behind interfaces or
TimeProvider
- The code is not C# / .NET
Inputs
| Input |
Required |
Description |
| Target path |
Yes |
A file, directory, project (.csproj), or solution (.sln) to scan |
| Exclusion patterns |
No |
Glob patterns to skip (e.g., **/obj/**, **/Migrations/**) |
| Category filter |
No |
Limit to specific categories: time, filesystem, environment, network, console, process |
Workflow
Step 1: Determine scan scope
Resolve the target to a set of .cs files:
- If a
.cs file, scan that single file.
- If a directory, scan all
.cs files recursively (excluding obj/, bin/).
- If a
.csproj, find its directory and scan .cs files within.
- If a
.sln, parse it, find all project directories, and scan .cs files across all projects.
Always exclude obj/, bin/, and any user-specified exclusion patterns.
Step 2: Search for static dependency patterns
Scan each file for calls matching these categories:
Treat pattern matches as candidates, not findings. Before counting an instance call, trace how its
receiver enters the class. A collaborator supplied through a constructor, parameter, property, or
dependency injection (DI) is already a test seam. In particular, an injected HttpClient is
testable with a controlled HttpMessageHandler; do not count its calls or recommend replacing it
merely because the injected type is concrete.
| Category |
Patterns to search for |
Recommended replacement |
| Time |
DateTime.Now, DateTime.UtcNow, DateTime.Today, DateTimeOffset.Now, DateTimeOffset.UtcNow, Task.Delay(, new CancellationTokenSource(TimeSpan |
TimeProvider (.NET 8+) |
| File System |
File.ReadAllText(, File.WriteAllText(, File.Exists(, File.Delete(, File.Copy(, File.Move(, Directory.Exists(, Directory.CreateDirectory(, Directory.GetFiles(, Directory.Delete(, Path.GetTempPath(, and instance members that hit the disk (new FileInfo(...), new DirectoryInfo(...), .LastWriteTimeUtc, new StreamReader(path)) |
IFileSystem (System.IO.Abstractions NuGet) |
| Randomness / identity |
new Random(, Random.Shared, Guid.NewGuid( |
TimeProvider-style seam: inject Random / an IGuidProvider |
| Culture / serialization |
CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture, JsonSerializer.Serialize(, JsonSerializer.Deserialize( |
Pass culture/options explicitly, or inject a serializer abstraction |
| Environment |
Environment.GetEnvironmentVariable(, Environment.SetEnvironmentVariable(, Environment.MachineName, Environment.UserName, Environment.CurrentDirectory, Environment.Exit( |
Custom IEnvironmentProvider |
| Network |
new HttpClient(, .GetAsync(, .PostAsync(, .SendAsync( (confirm the receiver is an HttpClient; exclude calls whose receiver is injected or produced by an injected factory) |
Inject HttpClient (commonly supplied by IHttpClientFactory) |
| Console |
Console.WriteLine(, Console.ReadLine(, Console.Write(, Console.ReadKey( |
IConsole wrapper or ILogger |
| Process |
Process.Start(, Process.GetCurrentProcess(, Process.GetProcessesByName( |
Custom IProcessRunner |
For time calls, inspect use as well as count. Two ambient clock reads in one
logical operation are two call sites and a consistency defect: for example,
separate DateTime.UtcNow reads for CreatedAt and
ExpiresAt = DateTime.UtcNow.AddDays(30) can drift. Recommend one captured
instant. With TimeProvider, retain DateTimeOffset where possible; when the
existing member requires UTC DateTime, use GetUtcNow().UtcDateTime, never
.DateTime, which loses the UTC kind. Treat capturing one instant as an
optional behavior-level follow-up: a mechanical wrapper migration must preserve
the original reads one-for-one unless the user separately approves that
semantic change.
Step 3: Aggregate and rank results
Count each call site across the entire scan scope — including the instance-member call sites covered by the rules below, not only static ones.
Counting rules — inaccurate totals are the main way this report loses to an ad-hoc scan:
- Build one occurrence ledger before writing prose. Give each included call
site exactly one row containing category, exact pattern,
file:line, and
recommended seam. Derive every category, pattern, and per-file count by
grouping that same ledger; never recount independently while writing tables.
- Keep the three count domains separate.
Files scanned includes every
eligible source file; affected files includes only files with ledger rows;
call sites is the number of ledger rows. Never substitute one for another.
- One authoritative total. Every call site you found belongs in the category summary and the grand total. Never park real findings in an "additional observations" section that the totals exclude.
- Classify by what the member touches, not by whether it is
static. Instance members that reach the same untestable resource still count and belong in the matching category (new FileInfo(path).LastWriteTimeUtc → File System; new HttpClient().GetAsync(...) → Network). Say "hidden dependency", not "static", when the member is an instance call.
- Check receiver provenance before counting instance calls. Count a resource access only when the code under test acquires or constructs the dependency itself. Exclude constructor-, parameter-, property-, and DI-injected collaborators from the "needs wrapping" total, including concrete
HttpClient instances.
- Exclude deterministic pure helpers from the "needs wrapping" total.
Path.Combine, Path.GetExtension, Path.GetFileName, and Math.*/string.* statics take no ambient input and are trivially testable. List them, if at all, in a separate "no action needed" note — never as testability blockers.
- Cover every category before reporting — time, file system, environment, network, console, process, randomness (
new Random(), Guid.NewGuid()), culture (CultureInfo.CurrentCulture), and serialization/statics such as JsonSerializer. Omitting a category that is present is an under-count.
- Give
file:line for every occurrence so the user can jump straight to it.
- Reconcile before publishing. The category totals, the top-patterns table, and the per-file table must sum to the same grand total.
- Treat exclusions as a scope decision, not a category. Remove
obj/,
bin/, generated, and user-excluded files before building the ledger. Do not
include their files or call sites in any reported count. State the exclusions
once rather than mixing excluded candidates into the arithmetic.
- Label truncated rankings. In a comprehensive audit, list all distinct
patterns when needed for reconciliation. If the user asked only for a top-N
subset, label it as a subset and do not imply that its rows sum to the grand
total.
Produce a summary with:
- Category summary — total call sites per category (time, filesystem, env, etc.)
- Top patterns — the 10 most frequent individual patterns ranked by count
- Most affected files — files with the highest number of static dependencies
- Existing abstractions available — for each category, note the recommended .NET abstraction:
- Time →
TimeProvider (built-in since .NET 8)
- File system →
System.IO.Abstractions (NuGet package)
- HTTP →
IHttpClientFactory (built-in)
- Environment → custom
IEnvironmentProvider
- Console → custom
IConsole or ILogger
- Process → custom
IProcessRunner
Step 4: Present the report
Format the output as a structured report:
## Static Dependency Report
**Scope**: <project/solution name>
**Files scanned**: <count>
**Total static call sites**: <count>
### Category Summary
| Category | Call Sites | Recommended Abstraction |
|-------------|-----------|------------------------|
| Time | 42 | TimeProvider (.NET 8+) |
| File System | 31 | System.IO.Abstractions |
| Environment | 12 | IEnvironmentProvider |
| ... | ... | ... |
### Top 10 Patterns
| # | Pattern | Count | Files |
|---|---------------------|-------|-------|
| 1 | DateTime.UtcNow | 28 | 14 |
| 2 | File.ReadAllText | 18 | 9 |
| ... |
### Most Affected Files
| File | Static Calls | Categories |
|-------------------------------|-------------|---------------------|
| Services/OrderProcessor.cs | 12 | Time, FileSystem |
| ... |
### Migration Priority
1. **Time** (42 sites) — Use `TimeProvider`, zero NuGet dependencies on .NET 8+
2. **File System** (31 sites) — Use `System.IO.Abstractions` NuGet package
3. ...
Step 5: Suggest next steps
Based on the report, recommend which category to tackle first (highest count, best built-in support). Keep this to a few lines.
Mention generate-testability-wrappers or migrate-static-to-wrapper only when the user's next action clearly needs them — a hand-off note, not a sales pitch. Never end an audit with promotional next-steps that dilute the findings.
Validation
Common Pitfalls
| Pitfall |
Solution |
Scanning obj/ or generated code |
Always exclude obj/, bin/, and *.Designer.cs |
| Counting calls on injected collaborators |
Trace the receiver: an injected HttpClient, TimeProvider, interface, or other caller-supplied dependency already has a seam and needs no replacement |
| Missing statics inside lambdas/LINQ |
Search covers all code within .cs files, including lambdas |
Recommending TimeProvider on < .NET 8 |
Check TargetFramework in .csproj — if < net8.0, recommend NodaTime.IClock or custom ISystemClock |
| Ignoring test projects |
Only scan production code — exclude *.Tests.csproj projects from the scan |
| Under-counting by relegating findings |
Real call sites belong in the category totals, not in a trailing "also noticed" paragraph that the totals ignore |
| Calling an instance member a static |
new FileInfo(p).LastWriteTimeUtc is an instance call but still a hidden file-system dependency — count it under File System and describe it accurately |
Recommending a wrapper for Path.Combine |
Pure, deterministic helpers need no seam; listing them as blockers makes the recommendations wrong |
1---2name: detect-static-dependencies3description: Scan C# source files for hard-to-test static dependencies — DateTime.Now/UtcNow, File.*, Directory.*, Environment.*, HttpClient, Console.*, Process.*, and other untestable statics. Produces a ranked report of static call sites by frequency. USE FOR: find untestable statics, scan for static dependencies, testability audit, identify hard-to-mock code, find DateTime.Now usage, detect static coupling, testability report, static analysis for testability. DO NOT USE FOR: generating wrappers (use generate-testability-wrappers), migrating code (use migrate-static-to-wrapper), general code review, or finding statics that are already behind abstractions.4license: MIT5---67# Detect Static Dependencies89Scan a C# codebase for calls to hard-to-test static APIs and produce a ranked report showing which statics appear most frequently, which files are most affected, and which abstractions already exist in the .NET ecosystem to replace them.1011## When to Use1213- Auditing a project's testability before adding unit tests14- Understanding the scope of static coupling in a legacy codebase15- Prioritizing which statics to wrap first (highest-frequency wins)16- Creating a migration plan for incremental testability improvements1718## Response Guidelines1920- Scale the response to the user's request. A question about a specific category (e.g., "find time statics") should focus on that category with file locations and counts, not produce a full report across all categories.21- When the user provides a specific file or directory path, scan only that scope — do not expand to the entire solution unless asked.22- The full structured report format in Step 4 is for comprehensive audit requests. For focused questions, return only the relevant subset (e.g., category summary + affected files for the requested category).2324## When Not to Use2526- The user wants wrappers generated (hand off to `generate-testability-wrappers`)27- The user wants mechanical migration done (hand off to `migrate-static-to-wrapper`)28- The statics are already behind interfaces or `TimeProvider`29- The code is not C# / .NET3031## Inputs3233| Input | Required | Description |34|-------|----------|-------------|35| Target path | Yes | A file, directory, project (.csproj), or solution (.sln) to scan |36| Exclusion patterns | No | Glob patterns to skip (e.g., `**/obj/**`, `**/Migrations/**`) |37| Category filter | No | Limit to specific categories: `time`, `filesystem`, `environment`, `network`, `console`, `process` |3839## Workflow4041### Step 1: Determine scan scope4243Resolve the target to a set of `.cs` files:44- If a `.cs` file, scan that single file.45- If a directory, scan all `.cs` files recursively (excluding `obj/`, `bin/`).46- If a `.csproj`, find its directory and scan `.cs` files within.47- If a `.sln`, parse it, find all project directories, and scan `.cs` files across all projects.4849Always exclude `obj/`, `bin/`, and any user-specified exclusion patterns.5051### Step 2: Search for static dependency patterns5253Scan each file for calls matching these categories:5455Treat pattern matches as candidates, not findings. Before counting an instance call, trace how its56receiver enters the class. A collaborator supplied through a constructor, parameter, property, or57dependency injection (DI) is already a test seam. In particular, an injected `HttpClient` is58testable with a controlled `HttpMessageHandler`; do not count its calls or recommend replacing it59merely because the injected type is concrete.6061| Category | Patterns to search for | Recommended replacement |62|----------|----------------------|------------------------|63| **Time** | `DateTime.Now`, `DateTime.UtcNow`, `DateTime.Today`, `DateTimeOffset.Now`, `DateTimeOffset.UtcNow`, `Task.Delay(`, `new CancellationTokenSource(TimeSpan` | `TimeProvider` (.NET 8+) |64| **File System** | `File.ReadAllText(`, `File.WriteAllText(`, `File.Exists(`, `File.Delete(`, `File.Copy(`, `File.Move(`, `Directory.Exists(`, `Directory.CreateDirectory(`, `Directory.GetFiles(`, `Directory.Delete(`, `Path.GetTempPath(`, and instance members that hit the disk (`new FileInfo(...)`, `new DirectoryInfo(...)`, `.LastWriteTimeUtc`, `new StreamReader(path)`) | `IFileSystem` (System.IO.Abstractions NuGet) |65| **Randomness / identity** | `new Random(`, `Random.Shared`, `Guid.NewGuid(` | `TimeProvider`-style seam: inject `Random` / an `IGuidProvider` |66| **Culture / serialization** | `CultureInfo.CurrentCulture`, `CultureInfo.CurrentUICulture`, `JsonSerializer.Serialize(`, `JsonSerializer.Deserialize(` | Pass culture/options explicitly, or inject a serializer abstraction |67| **Environment** | `Environment.GetEnvironmentVariable(`, `Environment.SetEnvironmentVariable(`, `Environment.MachineName`, `Environment.UserName`, `Environment.CurrentDirectory`, `Environment.Exit(` | Custom `IEnvironmentProvider` |68| **Network** | `new HttpClient(`, `.GetAsync(`, `.PostAsync(`, `.SendAsync(` (confirm the receiver is an `HttpClient`; exclude calls whose receiver is injected or produced by an injected factory) | Inject `HttpClient` (commonly supplied by `IHttpClientFactory`) |69| **Console** | `Console.WriteLine(`, `Console.ReadLine(`, `Console.Write(`, `Console.ReadKey(` | `IConsole` wrapper or `ILogger` |70| **Process** | `Process.Start(`, `Process.GetCurrentProcess(`, `Process.GetProcessesByName(` | Custom `IProcessRunner` |7172For time calls, inspect use as well as count. Two ambient clock reads in one73logical operation are two call sites and a consistency defect: for example,74separate `DateTime.UtcNow` reads for `CreatedAt` and75`ExpiresAt = DateTime.UtcNow.AddDays(30)` can drift. Recommend one captured76instant. With `TimeProvider`, retain `DateTimeOffset` where possible; when the77existing member requires UTC `DateTime`, use `GetUtcNow().UtcDateTime`, never78`.DateTime`, which loses the UTC kind. Treat capturing one instant as an79optional behavior-level follow-up: a mechanical wrapper migration must preserve80the original reads one-for-one unless the user separately approves that81semantic change.8283### Step 3: Aggregate and rank results8485Count each call site across the entire scan scope — including the instance-member call sites covered by the rules below, not only `static` ones.8687**Counting rules — inaccurate totals are the main way this report loses to an ad-hoc scan:**8889- **Build one occurrence ledger before writing prose.** Give each included call90 site exactly one row containing category, exact pattern, `file:line`, and91 recommended seam. Derive every category, pattern, and per-file count by92 grouping that same ledger; never recount independently while writing tables.93- **Keep the three count domains separate.** `Files scanned` includes every94 eligible source file; `affected files` includes only files with ledger rows;95 `call sites` is the number of ledger rows. Never substitute one for another.96- **One authoritative total.** Every call site you found belongs in the category summary and the grand total. Never park real findings in an "additional observations" section that the totals exclude.97- **Classify by what the member touches, not by whether it is `static`.** Instance members that reach the same untestable resource still count and belong in the matching category (`new FileInfo(path).LastWriteTimeUtc` → File System; `new HttpClient().GetAsync(...)` → Network). Say "hidden dependency", not "static", when the member is an instance call.98- **Check receiver provenance before counting instance calls.** Count a resource access only when the code under test acquires or constructs the dependency itself. Exclude constructor-, parameter-, property-, and DI-injected collaborators from the "needs wrapping" total, including concrete `HttpClient` instances.99- **Exclude deterministic pure helpers from the "needs wrapping" total.** `Path.Combine`, `Path.GetExtension`, `Path.GetFileName`, and `Math.*`/`string.*` statics take no ambient input and are trivially testable. List them, if at all, in a separate "no action needed" note — never as testability blockers.100- **Cover every category before reporting** — time, file system, environment, network, console, process, randomness (`new Random()`, `Guid.NewGuid()`), culture (`CultureInfo.CurrentCulture`), and serialization/statics such as `JsonSerializer`. Omitting a category that is present is an under-count.101- **Give `file:line` for every occurrence** so the user can jump straight to it.102- **Reconcile before publishing.** The category totals, the top-patterns table, and the per-file table must sum to the same grand total.103- **Treat exclusions as a scope decision, not a category.** Remove `obj/`,104 `bin/`, generated, and user-excluded files before building the ledger. Do not105 include their files or call sites in any reported count. State the exclusions106 once rather than mixing excluded candidates into the arithmetic.107- **Label truncated rankings.** In a comprehensive audit, list all distinct108 patterns when needed for reconciliation. If the user asked only for a top-N109 subset, label it as a subset and do not imply that its rows sum to the grand110 total.111112Produce a summary with:1131141. **Category summary** — total call sites per category (time, filesystem, env, etc.)1152. **Top patterns** — the 10 most frequent individual patterns ranked by count1163. **Most affected files** — files with the highest number of static dependencies1174. **Existing abstractions available** — for each category, note the recommended .NET abstraction:118 - Time → `TimeProvider` (built-in since .NET 8)119 - File system → `System.IO.Abstractions` (NuGet package)120 - HTTP → `IHttpClientFactory` (built-in)121 - Environment → custom `IEnvironmentProvider`122 - Console → custom `IConsole` or `ILogger`123 - Process → custom `IProcessRunner`124125### Step 4: Present the report126127Format the output as a structured report:128129```130## Static Dependency Report131132**Scope**: <project/solution name>133**Files scanned**: <count>134**Total static call sites**: <count>135136### Category Summary137| Category | Call Sites | Recommended Abstraction |138|-------------|-----------|------------------------|139| Time | 42 | TimeProvider (.NET 8+) |140| File System | 31 | System.IO.Abstractions |141| Environment | 12 | IEnvironmentProvider |142| ... | ... | ... |143144### Top 10 Patterns145| # | Pattern | Count | Files |146|---|---------------------|-------|-------|147| 1 | DateTime.UtcNow | 28 | 14 |148| 2 | File.ReadAllText | 18 | 9 |149| ... |150151### Most Affected Files152| File | Static Calls | Categories |153|-------------------------------|-------------|---------------------|154| Services/OrderProcessor.cs | 12 | Time, FileSystem |155| ... |156157### Migration Priority1581. **Time** (42 sites) — Use `TimeProvider`, zero NuGet dependencies on .NET 8+1592. **File System** (31 sites) — Use `System.IO.Abstractions` NuGet package1603. ...161```162163### Step 5: Suggest next steps164165Based on the report, recommend which category to tackle first (highest count, best built-in support). Keep this to a few lines.166167Mention `generate-testability-wrappers` or `migrate-static-to-wrapper` only when the user's next action clearly needs them — a hand-off note, not a sales pitch. Never end an audit with promotional next-steps that dilute the findings.168169## Validation170171- [ ] All `.cs` files in scope were scanned (check count)172- [ ] Report includes category totals, top patterns, and affected files173- [ ] Category totals, top patterns, and per-file counts reconcile to the same grand total174- [ ] Files scanned, affected files, and call sites are reported as different quantities175- [ ] Every aggregate was derived from one occurrence ledger rather than independently recounted176- [ ] Every occurrence carries a `file:line` location177- [ ] No findings are held outside the totals in an "additional" section178- [ ] Calls on injected collaborators are excluded from the "needs wrapping" total179- [ ] Deterministic pure helpers (`Path.Combine`, `Math.*`) are not counted as testability blockers180- [ ] Each detected pattern has a recommended replacement listed181- [ ] `obj/` and `bin/` directories were excluded182- [ ] Migration priority is ordered by impact (count × ease of replacement)183184## Common Pitfalls185186| Pitfall | Solution |187|---------|----------|188| Scanning `obj/` or generated code | Always exclude `obj/`, `bin/`, and `*.Designer.cs` |189| Counting calls on injected collaborators | Trace the receiver: an injected `HttpClient`, `TimeProvider`, interface, or other caller-supplied dependency already has a seam and needs no replacement |190| Missing statics inside lambdas/LINQ | Search covers all code within `.cs` files, including lambdas |191| Recommending `TimeProvider` on < .NET 8 | Check `TargetFramework` in `.csproj` — if < net8.0, recommend `NodaTime.IClock` or custom `ISystemClock` |192| Ignoring test projects | Only scan production code — exclude `*.Tests.csproj` projects from the scan |193| Under-counting by relegating findings | Real call sites belong in the category totals, not in a trailing "also noticed" paragraph that the totals ignore |194| Calling an instance member a static | `new FileInfo(p).LastWriteTimeUtc` is an instance call but still a hidden file-system dependency — count it under File System and describe it accurately |195| Recommending a wrapper for `Path.Combine` | Pure, deterministic helpers need no seam; listing them as blockers makes the recommendations wrong |