Claude Cursor GitHub Copilot Skill

analyzing-dotnet-performance

Scans .NET code for ~50 performance anti-patterns across async, memory, strings, collections, LINQ, regex, serialization, and I/O with tiered severity classification. Use when analyzing .NET code for optimization opportunities, reviewing hot paths, or auditing allocation-heavy pa

LLM Mart · 0 points · 16 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download dotnet-skills-plugins_dotnet-diag_skills_analyzing-dotnet-performance-30b30ef.zip · 18 KB
Part of dotnet/skills — 119 skills

Install

skills CLI npx skills add https://github.com/dotnet/skills/tree/main/plugins/dotnet-diag/skills/analyzing-dotnet-performance
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dotnet-skills@llmmart
Git git clone https://github.com/dotnet/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole dotnet/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

.NET Performance Patterns

Scan C#/.NET code for performance anti-patterns and produce prioritized findings with concrete fixes. Patterns sourced from the official .NET performance blog series, distilled to customer-actionable guidance.

When to Use

  • Reviewing C#/.NET code for performance optimization opportunities
  • Auditing hot paths for allocation-heavy or inefficient patterns
  • Systematic scan of a codebase for known anti-patterns before release
  • Second-opinion analysis after manual performance review

When Not to Use

  • Algorithmic complexity analysis — this skill targets API usage patterns, not algorithm design
  • Code not on a hot path with no performance requirements — avoid premature optimization

Inputs

Input Required Description
Source code Yes C# files, code blocks, or repository paths to scan
Hot-path context Recommended Which code paths are performance-critical
Target framework Recommended .NET version (some patterns require .NET 8+)
Scan depth Optional critical-only, standard (default), or comprehensive

Workflow

Step 1: Load Critical Reference

Resolve bundled paths from the directory that contains this SKILL.md, not from the user's workspace. Load this reference file first:

  • references/critical-patterns.md

If a direct read fails, list this skill's references/ directory once and retry only when the listing shows the expected file. Do not use workspace file or text search to locate the skill installation.

Step 2: Detect Code Signals and Select Topic Recipes

Scan the code for signals that indicate which pattern categories to check. Use the ## Detection section from the critical reference when available and the inline recipes in Step 3 for initial signal detection.

After detecting signals, load only the topic-specific references selected by scan depth:

  • critical-only: No additional references (use only critical-patterns.md)
  • standard (default): Load references matching detected signals from this list:
    • references/async-patterns.md — async/Task/ValueTask signals
    • references/memory-and-strings.md — Span/Memory/string allocation signals
    • references/regex-patterns.md — Regex signals
    • references/collections-and-linq.md — Dictionary/List/LINQ signals
    • references/io-and-serialization.md — JsonSerializer/HttpClient/Stream signals
    • references/structural-patterns.md — always loaded (unsealed classes checked regardless)
  • comprehensive: Load all six topic-specific references above

For coverage reporting, the selected references are references/critical-patterns.md plus only the topic-specific references selected above. If any selected reference remains unavailable after retry, use the inline recipes in Step 3 for the missing coverage. Include Reference coverage: reduced; unavailable: <paths>; used inline recipes for missing references. in the final report, with <paths> replaced by the missing relative paths of the selected references only.

Use the ## Detection sections from loaded reference files and the inline recipes in Step 3 for categories whose reference files are unavailable.

Signal in Code Topic
async, await, Task, ValueTask Async patterns
Span<, Memory<, stackalloc, ArrayPool, string.Substring, .Replace(, .ToLower(), += in loops, params Memory & strings
Regex, [GeneratedRegex], Regex.Match, RegexOptions.Compiled Regex patterns
Dictionary<, List<, .ToList(), .Where(, .Select(, LINQ methods, static readonly Dictionary< Collections & LINQ
JsonSerializer, HttpClient, Stream, FileStream I/O & serialization

Always check structural patterns (unsealed classes) regardless of signals.

Scan depth controls scope:

  • critical-only: Only critical patterns (deadlocks, >10x regressions)
  • standard (default): Critical + detected topic patterns
  • comprehensive: All pattern categories

Step 3: Scan and Report

For files under 500 lines, read the entire file first — you'll spot most patterns faster than running individual grep recipes. Use grep to confirm counts and catch patterns you might miss visually.

For each relevant pattern category, run the detection recipes below. Report exact counts, not estimates.

Core scan recipes (run these when reference files aren't available):

# Strings & memory
grep -n '\.IndexOf(\"' FILE                    # Missing StringComparison
grep -n '\.Substring(' FILE                    # Substring allocations
grep -En '\.(StartsWith|EndsWith|Contains)\s*\(' FILE  # Missing StringComparison
grep -n '\.ToLower()\|\.ToUpper()' FILE        # Culture-sensitive + allocation
grep -n '\.Replace(' FILE                      # Chained Replace allocations
grep -n 'params ' FILE                         # params array allocation

# Collections & LINQ
grep -n '\.Select\|\.Where\|\.OrderBy\|\.GroupBy' FILE  # LINQ on hot path
grep -n '\.All\|\.Any' FILE                    # LINQ on string/char
grep -n 'new Dictionary<\|new List<' FILE      # Per-call allocation
grep -n 'static readonly Dictionary<' FILE     # FrozenDictionary candidate

# Regex
grep -n 'RegexOptions.Compiled' FILE           # Compiled regex budget
grep -n 'new Regex(' FILE                      # Per-call regex
grep -n 'GeneratedRegex' FILE                  # Positive: source-gen regex

# Structural
grep -n 'public class \|internal class ' FILE  # Unsealed classes
grep -n 'sealed class' FILE                    # Already sealed
grep -n ': IEquatable' FILE                    # Positive: struct equality

Rules:

  • Run every relevant recipe for the detected pattern categories
  • Emit a scan execution checklist before classifying findings — list each recipe and the hit count
  • A result of 0 hits is valid and valuable (confirms good practice)
  • If reference files were loaded, also run their ## Detection recipes

Verify-the-Inverse Rule: For absence patterns, always count both sides and report the ratio (e.g., "N of M classes are sealed"). The ratio determines severity — 0/185 is systematic, 12/15 is a consistency fix.

Step 3b: Cross-File Consistency Check

If an optimized pattern is found in one file, check whether sibling files (same directory, same interface, same base class) use the un-optimized equivalent. Flag as 🟡 Moderate with the optimized file as evidence.

Step 3c: Compound Allocation Check

After running scan recipes, look for these multi-allocation patterns that single-line recipes miss:

  1. Branched .Replace() chains: Methods that call .Replace() across multiple if/else branches — report total allocation count across all branches, not just per-line.
  2. Cross-method chaining: When a public method delegates to another method that itself allocates intermediates (e.g., A calls B which does 3 regex replaces, then A calls C), report the total chain cost as one finding.
  3. Compound += with embedded allocating calls: Lines like result += $"...{Foo().ToLower()}" are 2+ allocations (interpolation + ToLower + concatenation) — flag the compound cost, not just the .ToLower().
  4. string.Format specificity: Distinguish resource-loaded format strings (not fixable) from compile-time literal format strings (fixable with interpolation). Enumerate the actionable sites.

Step 4: Classify and Prioritize Findings

Assign each finding a severity:

Severity Criteria Action
🔴 Critical Deadlocks, crashes, security vulnerabilities, >10x regression Must fix
🟡 Moderate 2-10x improvement opportunity, best practice for hot paths Should fix on hot paths
ℹ️ Info Pattern applies but code may not be on a hot path Consider if profiling shows impact

Prioritization rules:

  1. If the user identified hot-path code, elevate all findings in that code to their maximum severity
  2. If hot-path context is unknown, report 🔴 Critical findings unconditionally; report 🟡 Moderate findings with a note: "Impactful if this code is on a hot path"
  3. Never suggest micro-optimizations on code that is clearly not performance-sensitive

Scale-based severity escalation: When the same pattern appears across many instances, escalate severity:

  • 1-10 instances of the same anti-pattern → report at the pattern's base severity
  • 11-50 instances → escalate ℹ️ Info patterns to 🟡 Moderate
  • 50+ instances → escalate to 🟡 Moderate with elevated priority; flag as a codebase-wide systematic issue

Always report exact counts (from scan recipes), not estimates or agent summaries.

Step 5: Generate Findings

Keep findings compact. Each finding is one short block — not an essay. Group by severity (🔴 → 🟡 → ℹ️), not by file.

Format per finding:

#### ID. Title (N instances)
**Impact:** one-line impact statement
**Files:** file1.cs:L1, file2.cs:L2, ... (list locations, don't build tables)
**Fix:** one-line description of the change (e.g., "Add `StringComparison.Ordinal` parameter")
**Caveat:** only if non-obvious (version requirement, correctness risk)

Rules for compact output:

  • No ❌/✅ code blocks for trivial fixes (adding a keyword, parameter, or type change). A one-line fix description suffices.
  • Only include code blocks for non-obvious transformations (e.g., replacing a LINQ chain with a foreach loop, or hoisting a closure).
  • File locations as inline comma-separated list, not a table. Use File.cs:L42 format.
  • No explanatory prose beyond the Impact line — the severity icon already conveys urgency.
  • Merge related findings that share the same fix (e.g., all .ToLower() calls go in one finding, not split by file).
  • Positive findings in a bullet list, not a table. One line per pattern: ✅ Pattern — evidence.

End with a summary table and disclaimer:

| Severity | Count | Top Issue |
|----------|-------|-----------|
| 🔴 Critical | N | ... |
| 🟡 Moderate | N | ... |
| ℹ️ Info | N | ... |

> ⚠️ **Disclaimer:** These results are generated by an AI assistant and are non-deterministic. Findings may include false positives, miss real issues, or suggest changes that are incorrect for your specific context. Always verify recommendations with benchmarks and human review before applying changes to production code.

Validation

Before delivering results, verify:

  • All critical patterns were checked (from reference files or inline recipes)
  • Topic-specific recipes run only when matching signals detected
  • Each finding includes a concrete code fix
  • Scan execution checklist is complete (all recipes run)
  • Summary table included at end

Common Pitfalls

Pitfall Correct Approach
Flagging every Dictionary as needing FrozenDictionary Only flag if the dictionary is never mutated after construction
Suggesting Span<T> in async methods Use Memory<T> in async code; Span<T> only in sync hot paths
Reporting LINQ outside hot paths Only flag LINQ in identified hot paths or tight loops; LINQ is acceptable in code that runs infrequently. Since .NET 7, LINQ Min/Max/Sum/Average are vectorized — blanket bans on LINQ are misguided
Suggesting ConfigureAwait(false) in app code Only applicable in library code; not primarily a performance concern
Recommending ValueTask everywhere Only for hot paths with frequent synchronous completion
Flagging new HttpClient() in DI services Check if IHttpClientFactory is already in use
Suggesting [GeneratedRegex] for dynamic patterns Only flag when the pattern string is a compile-time literal
Suggesting CollectionsMarshal.AsSpan broadly Only for ultra-hot paths with benchmarked evidence; adds complexity and fragility
Suggesting unsafe code for micro-optimizations Avoid unsafe except where absolutely necessary — do not recommend it for micro-optimizations that don't matter. Safe alternatives like Span<T>, stackalloc in safe context, and ArrayPool cover the vast majority of performance needs
Files (skills)
  • references
    • async-patterns.md 2.9 KB
      # Async & Concurrency Patterns
      
      ### Don't Expose Async Wrappers for Sync Methods
      🟡 **AVOID** wrapping sync methods with `Task.Run` in libraries | .NET Core+
      
      ❌
      ```csharp
      public Task<int> ComputeHashAsync(byte[] data) =>
          Task.Run(() => ComputeHash(data));
      ```
      ✅
      ```csharp
      public int ComputeHash(byte[] data) { /* CPU-bound work */ }
      // Consumer decides: var hash = await Task.Run(() => lib.ComputeHash(data));
      ```
      
      **Impact: Eliminates unnecessary thread pool queue/dequeue overhead per call.**
      
      ### Don't Expose Sync Wrappers for Async Methods
      🟡 **AVOID** creating sync wrappers that block on async implementations | .NET Core+
      
      ❌
      ```csharp
      public string GetData() => GetDataAsync().Result;
      ```
      ✅
      ```csharp
      public async Task<string> GetDataAsync() { /* ... */ }
      ```
      
      **Impact: Prevents deadlocks and thread pool starvation from hidden sync-over-async blocking.**
      
      ### Use ValueTask for Hot Paths with Frequent Sync Completion
      🟡 **DO** use `ValueTask<T>` on hot paths where sync completion is common | .NET Core 2.1+
      
      ❌
      ```csharp
      public async Task<int> ReadAsync(Memory<byte> buffer)
      {
          if (_bufferedCount > 0)
              return ReadFromBuffer(buffer.Span);
          return await ReadAsyncCore(buffer);
      }
      ```
      ✅
      ```csharp
      public ValueTask<int> ReadAsync(Memory<byte> buffer)
      {
          if (_bufferedCount > 0)
              return new ValueTask<int>(ReadFromBuffer(buffer.Span));
          return new ValueTask<int>(ReadAsyncCore(buffer));
      }
      ```
      
      **Impact: Eliminates Task\<T\> allocation on synchronous completion — the struct stores results inline.**
      
      ### Use Channels for Producer/Consumer
      🟡 **DO** use `System.Threading.Channels` for producer-consumer patterns | .NET Core 3.0+
      
      ❌
      ```csharp
      var queue = new BlockingCollection<WorkItem>();
      var item = queue.Take();
      ```
      ✅
      ```csharp
      var channel = Channel.CreateUnbounded<WorkItem>();
      
      // Producer
      await channel.Writer.WriteAsync(item);
      
      // Consumer
      await foreach (var item in channel.Reader.ReadAllAsync())
          Process(item);
      ```
      
      **Impact: ~25% faster, ~95% fewer GC collections vs manual approaches.**
      
      ### Avoid False Sharing with Thread-Local State
      🟡 **AVOID** adjacent mutable fields written by different threads | .NET 7+
      
      ❌
      ```csharp
      class SharedCounters
      {
          public long Counter1;
          public long Counter2;
      }
      ```
      ✅
      ```csharp
      [StructLayout(LayoutKind.Explicit, Size = 128)]
      struct PaddedCounter
      {
          [FieldOffset(0)] public long Value;
      }
      ```
      
      **Impact: Eliminates cross-core cache invalidation — can improve multi-threaded throughput by 10x+.**
      
      ## Detection
      
      Scan recipes for async anti-patterns. Run these and report exact counts.
      
      ```bash
      # async void methods (correctness issue — crashes on exception)
      grep -rn --include='*.cs' 'async void' --exclude-dir=bin --exclude-dir=obj . | grep -v 'event' | wc -l
      ```
      
      ### Patterns Requiring Manual Review
      
      - **Sync-over-async** (`.Result`, `.Wait()`): `.Result` matches any property named Result — needs type context to confirm it's `Task.Result`
      
    • collections-and-linq.md 7.6 KB
      # Collections & LINQ Patterns
      
      ### Use FrozenDictionary/FrozenSet for Read-Heavy Lookup Tables
      🟡 **DO** use `FrozenDictionary`/`FrozenSet` for collections created once and read many times | .NET 8+
      
      ❌
      ```csharp
      private static readonly Dictionary<string, int> s_statusCodes = new()
      {
          ["OK"] = 200, ["NotFound"] = 404, ["InternalServerError"] = 500
      };
      ```
      ✅
      ```csharp
      private static readonly FrozenDictionary<string, int> s_statusCodes =
          new Dictionary<string, int>
          {
              ["OK"] = 200, ["NotFound"] = 404, ["InternalServerError"] = 500
          }.ToFrozenDictionary();
      ```
      
      **Impact: ~50% faster lookups than Dictionary, ~14x faster than ImmutableDictionary.**
      
      ### Use Dictionary Alternate Lookup for Span-Based Keys
      🟡 **DO** use `GetAlternateLookup<ReadOnlySpan<char>>()` to avoid string allocation on lookups | .NET 9+
      
      ❌
      ```csharp
      string key = headerLine.Substring(0, colonIndex);
      if (s_dict.TryGetValue(key, out int value)) { /* ... */ }
      ```
      ✅
      ```csharp
      var lookup = s_dict.GetAlternateLookup<ReadOnlySpan<char>>();
      ReadOnlySpan<char> key = headerLine.AsSpan(0, colonIndex);
      if (lookup.TryGetValue(key, out int value)) { }
      ```
      
      **Impact: Avoids string allocation per lookup — especially valuable in parser/protocol hot paths.**
      
      ### Use CollectionsMarshal.GetValueRefOrNullRef for Lookup-and-Update
      🟡 **DO** use `CollectionsMarshal.GetValueRefOrAddDefault` for dictionary update patterns | .NET 6+
      
      ❌
      ```csharp
      _counts.TryGetValue(key, out int count);
      _counts[key] = count + 1;
      ```
      ✅
      ```csharp
      ref int count = ref CollectionsMarshal.GetValueRefOrAddDefault(_counts, key, out _);
      count++;
      ```
      
      **Impact: ~48% faster for lookup-and-update patterns (95µs → 49µs).**
      
      ### Use Collection Expressions [] for Zero-Allocation Span Creation
      🟡 **DO** use collection expressions for `Span<T>` targets | C# 12 / .NET 8+
      
      ❌
      ```csharp
      int[] values = new int[] { a, b, c, d };
      ```
      ✅
      ```csharp
      Span<int> values = [a, b, c, d];
      ReadOnlySpan<int> daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
      ```
      
      **Impact: Zero heap allocation for span-targeted collection expressions.**
      
      ### Use EnsureCapacity on List/Stack/Queue Before Bulk Adds
      🟡 **DO** call `EnsureCapacity` before bulk insertions | .NET 6+
      
      ❌
      ```csharp
      var list = new List<int>();
      for (int i = 0; i < 10000; i++)
          list.Add(i);
      ```
      ✅
      ```csharp
      var list = new List<int>();
      list.EnsureCapacity(10000);
      for (int i = 0; i < 10000; i++)
          list.Add(i);
      ```
      
      **Impact: Reduces reallocations and array copies during bulk operations.**
      
      ### Use TryGetNonEnumeratedCount for Pre-Sizing
      🟡 **DO** use `TryGetNonEnumeratedCount` to pre-size destination collections | .NET 6+
      
      ❌
      ```csharp
      var results = new List<int>();
      foreach (var item in source)
          results.Add(Transform(item));
      ```
      ✅
      ```csharp
      var results = source.TryGetNonEnumeratedCount(out int count)
          ? new List<int>(count)
          : new List<int>();
      foreach (var item in source)
          results.Add(Transform(item));
      ```
      
      **Impact: Avoids O(n) enumeration for counting; eliminates resizing allocations.**
      
      ### Hoist Static Data Out of Method Bodies
      🟡 **AVOID** creating collections with static/deterministic data inside method bodies | .NET Core+
      
      ❌
      ```csharp
      public string Convert(long number)
      {
          var groupsMap = new Dictionary<long, Func<long, string>>
          {
              { 1_000_000_000, n => $"{Convert(n)} billion" },
              { 1_000_000, n => $"{Convert(n)} million" },
              { 1_000, n => $"{Convert(n)} thousand" },
          };
      }
      ```
      ✅
      ```csharp
      private static readonly FrozenDictionary<long, Func<long, string>> s_groupsMap =
          new Dictionary<long, Func<long, string>>
          {
              { 1_000_000_000, n => $"{Convert(n)} billion" },
              { 1_000_000, n => $"{Convert(n)} million" },
              { 1_000, n => $"{Convert(n)} thousand" },
          }.ToFrozenDictionary();
      
      public string Convert(long number)
      {
          // ... use s_groupsMap
      }
      ```
      
      **Impact: Eliminates collection + internal storage + closure allocations per call. For a Dictionary with N entries, saves ~N+3 allocations per invocation.**
      
      ### Add Overloads to Avoid params Array Allocation
      🟡 **DO** add 1- and 2-argument overloads for methods that accept `params T[]`, or use `params ReadOnlySpan<T>` on .NET 9+ | .NET Core+
      
      ❌
      ```csharp
      public static string Transform(this string input, params IStringTransformer[] transformers) =>
          transformers.Aggregate(input, (current, t) => t.Transform(current));
      
      "hello".Transform(To.TitleCase);
      ```
      ✅
      ```csharp
      // Option A: Explicit overloads for common arities
      public static string Transform(this string input, IStringTransformer transformer) =>
          transformer.Transform(input);
      
      public static string Transform(this string input, IStringTransformer t1, IStringTransformer t2) =>
          t2.Transform(t1.Transform(input));
      
      public static string Transform(this string input, params IStringTransformer[] transformers) =>
          transformers.Aggregate(input, (current, t) => t.Transform(current));
      
      // Option B (.NET 9+ / C# 13): params ReadOnlySpan<T> — zero heap allocation
      public static string Transform(this string input, params ReadOnlySpan<IStringTransformer> transformers)
      {
          foreach (var t in transformers)
              input = t.Transform(input);
          return input;
      }
      ```
      
      **Impact: Eliminates one array allocation per call for the common 1- and 2-argument cases. `params ReadOnlySpan<T>` eliminates it for all arities.**
      
      ## Detection
      
      Scan recipes for collection and LINQ anti-patterns. Run these and report exact counts.
      
      ```bash
      # Static Dictionary not using FrozenDictionary (read-only after init)
      grep -rn --include='*.cs' 'static readonly Dictionary<' --exclude-dir=bin --exclude-dir=obj . | wc -l
      
      # Static FrozenDictionary (already optimized — verify the inverse)
      grep -rn --include='*.cs' 'static readonly FrozenDictionary<' --exclude-dir=bin --exclude-dir=obj . | wc -l
      
      # Per-call List allocation (inside method bodies, not static/readonly fields)
      grep -rn --include='*.cs' 'new List<' --exclude-dir=bin --exclude-dir=obj . | grep -v 'static\|readonly' | wc -l
      
      # Per-call Dictionary allocation (inside method bodies, not static/readonly fields)
      grep -rn --include='*.cs' 'new Dictionary<' --exclude-dir=bin --exclude-dir=obj . | grep -v 'static\|readonly' | wc -l
      
      # StringComparer.CurrentCulture usage (almost always wrong in library code — use Ordinal)
      grep -rn --include='*.cs' 'StringComparer.CurrentCulture' --exclude-dir=bin --exclude-dir=obj . | wc -l
      
      # LINQ chains in extension/hot-path files (.Select, .Where, .Cast, .Take, .Aggregate)
      grep -rn --include='*.cs' -E '\.(Select|Where|Cast|Take|Aggregate)\(' --exclude-dir=bin --exclude-dir=obj . | wc -l
      ```
      
      For the LINQ chain recipe: any hit in a file whose name ends in `Extensions.cs`, `Formatter.cs`, or implements a method called from a public extension method is a hot-path candidate. Inspect each hit in these files and flag LINQ chains that allocate delegates, enumerators, or intermediate collections on every call. Hits in localization converters or one-time initialization are lower priority.
      
      ### Patterns Requiring Manual Review
      
      - **ContainsKey + indexer double-lookup**: Requires verifying the same key is used in a subsequent indexer access — multi-line/multi-statement context
      - **LINQ on hot paths**: The LINQ chain recipe above catches call sites, but distinguishing hot-path from cold-path requires context. Prioritize hits in `*Extensions.cs` and `*Formatter.cs` files, which are typically called on every user invocation
      - **`new Dictionary/List<` in method bodies vs fields**: The grep heuristic (`grep -v 'static\|readonly'`) catches most cases but may include false positives from field initializers without `static`/`readonly` — spot-check flagged lines
      
    • critical-patterns.md 6.6 KB
      # Critical .NET Performance Anti-Patterns
      
      17 patterns that cause deadlocks, order-of-magnitude regressions, or excessive allocations.
      
      ## Async / Tasks
      
      ### Never Block on Async (Sync-over-Async)
      🔴 **AVOID** | .NET Core+
      
      ❌
      ```csharp
      public string GetData()
          => GetDataAsync().Result;
      ```
      ✅
      ```csharp
      public async Task<string> GetDataAsync()
          => await GetDataInternalAsync();
      ```
      **Impact: Deadlocks or thread pool starvation; wastes threads, destroys scalability.**
      
      ### Never Await a ValueTask Multiple Times
      🔴 **AVOID** | .NET Core 2.1+
      
      ❌
      ```csharp
      ValueTask<int> vt = SomeMethodAsync();
      int a = await vt;
      int b = await vt;
      ```
      ✅
      ```csharp
      int result = await SomeMethodAsync();
      ```
      **Impact: Undefined behavior — silent data corruption or exceptions.**
      
      ## Memory / Allocation
      
      ### Use Span\<T\> / AsSpan Instead of Substring for Slicing
      🔴 **DO** | .NET Core 2.1+
      
      ❌
      ```csharp
      string sub = input.Substring(5, 10);
      ```
      ✅
      ```csharp
      ReadOnlySpan<char> sub = input.AsSpan(5, 10);
      ```
      **Impact: Eliminates per-slice allocations; 2-4x faster via vectorization.**
      
      ### Use ArrayPool\<T\> for Temporary Buffers
      🔴 **DO** | .NET Core+
      
      ❌
      ```csharp
      byte[] buf = new byte[4096];
      ```
      ✅
      ```csharp
      byte[] buf = ArrayPool<byte>.Shared.Rent(4096);
      Process(buf);
      ArrayPool<byte>.Shared.Return(buf);
      ```
      **Impact: Dramatically reduces GC pressure for buffer-heavy workloads.**
      
      ### Avoid stackalloc in Loops
      🔴 **AVOID** | .NET 5+
      
      ❌
      ```csharp
      for (int i = 0; i < 10_000; i++)
          Span<byte> buf = stackalloc byte[1024];
      ```
      ✅
      ```csharp
      Span<byte> buf = stackalloc byte[1024];
      for (int i = 0; i < 10_000; i++) { Process(buf); }
      ```
      **Impact: StackOverflowException — unrecoverable, no catch possible.**
      
      ### Avoid Boxing Value Types
      🔴 **AVOID** | .NET 6+
      
      ❌
      ```csharp
      string s = string.Format("{0}.{1}", major, minor);
      ```
      ✅
      ```csharp
      string s = $"{major}.{minor}";
      ```
      **Impact: When replacing `string.Format` with C# 10+ interpolation, typical improvements are ~40% faster with significantly less allocation. Actual gains vary by call site.**
      
      ## Strings
      
      ### Use StringComparison.Ordinal for Non-Linguistic Comparisons
      🔴 **DO** | .NET Core+
      
      ❌
      ```csharp
      bool found = text.IndexOf("Content-Type") >= 0;
      ```
      ✅
      ```csharp
      bool found = text.Contains("Content-Type", StringComparison.Ordinal);
      ```
      **Impact: 2-3x faster; OrdinalIgnoreCase hash codes ~3.3x faster.**
      
      ### Use AsSpan Instead of Substring
      🔴 **DO** | .NET Core 2.1+
      
      ❌
      ```csharp
      int val = int.Parse(str.Substring(5, 3));
      ```
      ✅
      ```csharp
      int val = int.Parse(str.AsSpan(5, 3));
      ```
      **Impact: Eliminates one string allocation per parse operation.**
      
      ## Regular Expressions
      
      ### Use Source-Generated Regex [GeneratedRegex]
      🔴 **ALWAYS** use `[GeneratedRegex]` for all static regex patterns | .NET 7+
      
      ❌
      ```csharp
      private static readonly Regex s_re =
          new(@"\w+@\w+\.\w+", RegexOptions.Compiled);
      ```
      ✅
      ```csharp
      [GeneratedRegex(@"\w+@\w+\.\w+")]
      private static partial Regex EmailRegex();
      ```
      **Impact: Always beneficial or neutral for static patterns — near-zero startup, better throughput, and required for AOT/trimming scenarios.**
      
      ### Avoid Nested Quantifiers (Catastrophic Backtracking)
      🔴 **AVOID** | .NET Core+
      
      ❌
      ```csharp
      var r = new Regex(@"^(\w+)+$");
      ```
      ✅
      ```csharp
      var r = new Regex(@"^\w+$", RegexOptions.NonBacktracking);
      ```
      **Impact: Can hang process indefinitely on crafted input.**
      
      ### Use TryGetValue Instead of ContainsKey + Indexer
      🔴 **DO** | .NET Core+
      
      ❌
      ```csharp
      if (dict.ContainsKey(key))
          Use(dict[key]);
      ```
      ✅
      ```csharp
      if (dict.TryGetValue(key, out var value))
          Use(value);
      ```
      **Impact: ~2x faster (50% reduction in lookup time).**
      
      ### Avoid LINQ in Hot Paths
      🔴 **AVOID** | .NET Core+
      
      ❌
      ```csharp
      bool found = items.Any(x => x.Name == target);
      ```
      ✅
      ```csharp
      bool found = false;
      foreach (var item in items)
          if (item.Name == target) { found = true; break; }
      ```
      **Impact: Eliminates 1-3 allocations per call; measurable in tight loops.**
      
      ### Don't Iterate IEnumerable Multiple Times
      🔴 **AVOID** | .NET Core+
      
      ❌
      ```csharp
      foreach (Type t in types) { Validate(t); }
      _types = types.ToArray();
      ```
      ✅
      ```csharp
      Type[] arr = types.ToArray();
      foreach (Type t in arr) { Validate(t); }
      _types = arr;
      ```
      **Impact: Halves enumeration cost; prevents bugs from re-executing deferred queries.**
      
      ## JSON Serialization
      
      ### Use System.Text.Json Source Generator
      🔴 **DO** | .NET 6+
      
      ❌
      ```csharp
      string json = JsonSerializer.Serialize(post);
      ```
      ✅
      ```csharp
      [JsonSerializable(typeof(BlogPost))]
      internal partial class AppJsonCtx : JsonSerializerContext { }
      string json = JsonSerializer.Serialize(post, AppJsonCtx.Default.BlogPost);
      ```
      **Impact: 37-44% faster; enables trimming and Native AOT.**
      
      ### Cache JsonSerializerOptions
      🔴 **DO** | .NET 5+
      
      ❌
      ```csharp
      JsonSerializer.Serialize(obj, new JsonSerializerOptions());
      ```
      ✅
      ```csharp
      private static readonly JsonSerializerOptions s_opts = new();
      JsonSerializer.Serialize(obj, s_opts);
      ```
      **Impact: Up to 592x slower without caching (.NET 6); always cache or use defaults.**
      
      ## Networking
      
      ### Reuse HttpClient Instances
      🔴 **DO** | .NET Core 2.1+
      
      ❌
      ```csharp
      using var client = new HttpClient();
      await client.GetStringAsync(url);
      ```
      ✅
      ```csharp
      private static readonly HttpClient s_http = new(new SocketsHttpHandler
      { PooledConnectionLifetime = TimeSpan.FromMinutes(5) });
      await s_http.GetStringAsync(url);
      ```
      **Impact: Prevents socket exhaustion; 6-12x faster concurrent HTTPS.**
      
      ## General
      
      ### Use SearchValues\<T\> for Repeated Set Searches
      🔴 **DO** | .NET 8+
      
      ❌
      ```csharp
      int pos = text.IndexOfAny("ABCDEF".ToCharArray());
      ```
      ✅
      ```csharp
      private static readonly SearchValues<char> s_hex = SearchValues.Create("ABCDEF");
      int pos = text.AsSpan().IndexOfAny(s_hex);
      ```
      **Impact: 2-10x faster for chars; 10-30x faster for multi-string (.NET 9+).**
      
      ## Detection
      
      Scan recipes for critical anti-patterns. Run these and report exact counts of issues found in each case.
      
      ```bash
      # .IndexOf(string) without StringComparison (culture-aware, 2-3x slower)
      grep -rn --include='*.cs' -E '\.IndexOf\("[^"]+"\)' --exclude-dir=bin --exclude-dir=obj . | wc -l
      
      # .Substring( calls (allocates new string — consider AsSpan)
      grep -rn --include='*.cs' '\.Substring(' --exclude-dir=bin --exclude-dir=obj . | wc -l
      
      # .StartsWith/.EndsWith without StringComparison (culture-aware, 2-3x slower)
      grep -rn --include='*.cs' -E '\.(StartsWith|EndsWith)\("[^"]+"\)' --exclude-dir=bin --exclude-dir=obj . | wc -l
      
      # .Contains(string) without StringComparison — NOTE: will also match collection .Contains() calls; filter to string receivers
      grep -rn --include='*.cs' -E '\.Contains\("[^"]+"\)' --exclude-dir=bin --exclude-dir=obj . | wc -l
      ```
      
    • io-and-serialization.md 3.6 KB
      # I/O, Serialization & General Patterns
      
      ### Use HttpCompletionOption.ResponseHeadersRead for Streaming
      🟡 **DO** use `ResponseHeadersRead` when downloading large responses | .NET Core 3.0+
      
      ❌
      ```csharp
      var response = await client.GetAsync(uri);
      ```
      ✅
      ```csharp
      using var response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
      using var stream = await response.Content.ReadAsStreamAsync();
      await stream.CopyToAsync(destinationStream);
      ```
      
      **Impact: ~2x faster for large downloads (10MB+), dramatically reduced memory usage.**
      
      ### Use Async FileStream Operations
      🟡 **DO** use `FileStream` with `useAsync: true` for scalable file I/O | .NET 6+
      
      ❌
      ```csharp
      using var fs = new FileStream(path, FileMode.Open);
      ```
      ✅
      ```csharp
      await using var fs = new FileStream(path, FileMode.Open, FileAccess.Read,
          FileShare.Read, bufferSize: 4096, useAsync: true);
      
      byte[] buffer = new byte[1024];
      while (await fs.ReadAsync(buffer) != 0) { /* process */ }
      ```
      
      **Impact: Up to 3x faster async reads; allocation reduced from megabytes to hundreds of bytes.**
      
      ### Use Memory\<byte\> Overloads for Stream.ReadAsync/WriteAsync
      🟡 **DO** use `Memory<byte>`-based stream overloads instead of `byte[]` overloads | .NET 5+
      
      ❌
      ```csharp
      await stream.ReadAsync(buffer, 0, buffer.Length);
      await stream.WriteAsync(buffer, 0, buffer.Length);
      ```
      ✅
      ```csharp
      await stream.ReadAsync(buffer.AsMemory());
      await stream.WriteAsync(buffer.AsMemory());
      ```
      
      **Impact: Eliminates ~72 KB allocation per 1,000 read/write pairs on NetworkStream.**
      
      ### Use Span-Based TryFormat for Number Formatting
      🟡 **DO** use `TryFormat` to format numbers into `Span<char>` buffers | .NET Core 2.1+
      
      ❌
      ```csharp
      string formatted = value.ToString();
      destination.Write(formatted);
      ```
      ✅
      ```csharp
      Span<char> buffer = stackalloc char[20];
      if (value.TryFormat(buffer, out int charsWritten))
          destination.Write(buffer[..charsWritten]);
      ```
      
      **Impact: Int32.ToString() ~2x faster in .NET Core 2.1, Int32 parsing ~5x faster in .NET Core 3.0.**
      
      ### Use static readonly for Runtime Devirtualization
      🟡 **DO** store implementations in `static readonly` fields for JIT devirtualization | .NET Core 3.0+
      
      ❌
      ```csharp
      private static Base s_impl = new DerivedImpl();
      s_impl.Process();
      ```
      ✅
      ```csharp
      private static readonly Base s_impl = new DerivedImpl();
      s_impl.Process();
      
      private static readonly bool s_feature =
          Environment.GetEnvironmentVariable("Feature") == "1";
      ```
      
      **Impact: Virtual call eliminated entirely — can be inlined to zero overhead. Dead code elimination in tier 1.**
      
      ### Avoid Explicit Static Constructors — Use Field Initializers
      🟡 **AVOID** explicit `static` constructors when field initializers suffice | .NET Core 3.0+
      
      ❌
      ```csharp
      class Foo
      {
          static readonly int s_value;
          static Foo() { s_value = ComputeValue(); }
      }
      ```
      ✅
      ```csharp
      class Foo
      {
          static readonly int s_value = ComputeValue();
      }
      ```
      
      **Impact: Enables better JIT optimization and reduces potential lock overhead on static method access.**
      
      ## Detection
      
      Scan recipes for I/O and serialization anti-patterns. Run these and report exact counts.
      
      ```bash
      # new HttpClient() (socket exhaustion risk)
      grep -rn --include='*.cs' 'new HttpClient(' --exclude-dir=bin --exclude-dir=obj . | wc -l
      
      # new JsonSerializerOptions() not cached (592x slower in .NET 6)
      grep -rn --include='*.cs' 'new JsonSerializerOptions' --exclude-dir=bin --exclude-dir=obj . | grep -v 'static\|readonly' | wc -l
      ```
      
      ### Patterns Requiring Manual Review
      
      - **`JsonSerializer.Serialize/Deserialize` without source-gen context**: Can't determine from grep if a context parameter is passed
      
    • memory-and-strings.md 5.7 KB
      # Memory & String Patterns
      
      ### Use ReadOnlySpan\<byte\> for Constant Byte Data
      🟡 **DO** assign constant byte arrays to `ReadOnlySpan<byte>` | .NET 5+
      
      ❌
      ```csharp
      byte[] data = new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F };
      ```
      ✅
      ```csharp
      ReadOnlySpan<byte> data = [0x48, 0x65, 0x6C, 0x6C, 0x6F];
      ReadOnlySpan<int> primes = [2, 3, 5, 7, 11, 13];
      ```
      
      **Impact: ~100x faster access than static byte[] field, zero allocation.**
      
      ### Use stackalloc for Small Temporary Buffers
      🟡 **DO** use `stackalloc` for small, fixed-size temporary buffers | .NET Core+
      
      ❌
      ```csharp
      char[] buffer = new char[64];
      guid.TryFormat(buffer, out int written);
      ```
      ✅
      ```csharp
      Span<char> buffer = stackalloc char[64];
      guid.TryFormat(buffer, out int written);
      ```
      
      **Impact: Zero heap allocation, no GC pressure, instant alloc/dealloc.**
      
      ### Use Span.TryWrite for Allocation-Free Interpolation
      🟡 **DO** use `MemoryExtensions.TryWrite` to format into `Span<char>` buffers | .NET 6+
      
      ❌
      ```csharp
      string formatted = $"Date: {dt:R}";
      destination.Write(formatted);
      ```
      ✅
      ```csharp
      Span<char> buffer = stackalloc char[64];
      buffer.TryWrite($"Date: {dt:R}", out int charsWritten);
      ```
      
      **Impact: Zero heap allocation for formatting operations.**
      
      ### Use Span.Split() for Zero-Allocation Splitting
      🟡 **DO** use `MemoryExtensions.Split` for allocation-free string splitting | .NET 9+
      
      ❌
      ```csharp
      string[] parts = input.Split(',');
      ```
      ✅
      ```csharp
      foreach (Range range in input.AsSpan().Split(','))
      {
          ReadOnlySpan<char> segment = input.AsSpan(range);
      }
      ```
      
      **Impact: 208 bytes → 0 bytes per split, 2x faster.**
      
      ### Use UTF8 String Literals (u8 suffix)
      🟡 **DO** use the `u8` suffix for compile-time UTF8 `ReadOnlySpan<byte>` | .NET 7+
      
      ❌
      ```csharp
      byte[] header = Encoding.UTF8.GetBytes("Content-Type");
      ```
      ✅
      ```csharp
      ReadOnlySpan<byte> header = "Content-Type"u8;
      ```
      
      **Impact: 17ns → 0.006ns — eliminates runtime transcoding entirely.**
      
      ### Use ReadOnlySpan\<char\> Pattern Matching with switch
      🟡 **DO** use `switch` on `ReadOnlySpan<char>` for allocation-free string matching | C# 11+
      
      ❌
      ```csharp
      switch (attr.Value.Trim()) { case "preserve": /* ... */ break; }
      ```
      ✅
      ```csharp
      switch (attr.Value.AsSpan().Trim())
      {
          case "preserve": return Preserve;
          case "default": return Default;
      }
      ```
      
      **Impact: Eliminates string allocation from Trim() in switch-based dispatch.**
      
      ### Use params ReadOnlySpan\<T\> to Eliminate Array Allocations
      🟡 **DO** add `params ReadOnlySpan<T>` overloads to library methods | C# 13 / .NET 9+
      
      ❌
      ```csharp
      public static void Log(params string[] messages) { /* ... */ }
      Log("Starting", "Processing", "Done");
      ```
      ✅
      ```csharp
      public static void Log(params ReadOnlySpan<string> messages) { /* ... */ }
      Log("Starting", "Processing", "Done");
      ```
      
      **Impact: Eliminates params array allocation. E.g., Path.Join with 5+ segments saves 64 bytes per call.**
      
      ### Avoid Chained String-Returning Operations
      🟡 **AVOID** chains of 3+ string-returning method calls that each allocate intermediates | .NET Core+
      
      **Pattern 1: Chained .Replace() calls**
      
      ❌
      ```csharp
      string result = input.Replace("a", "b").Replace("c", "d").Replace("e", "f");
      ```
      ✅
      ```csharp
      var sb = new StringBuilder(input.Length);
      // single pass replacing all patterns
      ```
      
      **Pattern 2: Chained Regex.Replace() calls**
      
      ❌
      ```csharp
      public static string Underscore(this string input) =>
          Regex3.Replace(Regex2.Replace(Regex1.Replace(input, "$1_$2"), "$1_$2"), "_").ToLower();
      ```
      ✅
      ```csharp
      return string.Create(totalLength, state, (span, s) => { /* write directly */ });
      ```
      
      **Pattern 3: += string concatenation in loops**
      
      ❌
      ```csharp
      string result = "";
      foreach (var part in parts)
          result += separator + part;
      ```
      ✅
      ```csharp
      var sb = new StringBuilder();
      foreach (var part in parts)
          sb.Append(separator).Append(part);
      return sb.ToString();
      ```
      
      **Impact: Eliminates N-1 intermediate string allocations per chain. For `+=` in loops, eliminates O(n²) total allocation.**
      
      ### Cache char.ToString() for Known Character Sets
      🟡 **DO** cache `char.ToString()` results when the set of characters is small and known | .NET Core+
      
      ❌
      ```csharp
      return symbol.ToString();
      
      foreach (var prefix in UnitPrefixes)
          input = input.Replace(prefix.Value.Name, prefix.Key.ToString());
      ```
      ✅
      ```csharp
      private static readonly FrozenDictionary<char, string> s_charStrings =
          new Dictionary<char, string>
          {
              ['k'] = "k", ['M'] = "M", ['G'] = "G",
          }.ToFrozenDictionary();
      
      return s_charStrings[symbol];
      ```
      
      **Impact: Eliminates one string allocation per char.ToString() call. Significant when called in loops or on hot paths.**
      
      ## Detection
      
      Scan recipes for memory and string anti-patterns. Run these and report exact counts.
      
      ```bash
      # .ToLower()/.ToUpper() without culture parameter (allocates + culture-sensitive)
      grep -rn --include='*.cs' -E '\.(ToLower|ToUpper)\(\)' --exclude-dir=bin --exclude-dir=obj . | wc -l
      
      # Chained .Replace( calls (3+ on one line — intermediate string allocations)
      grep -rn --include='*.cs' '\.Replace(.*\.Replace(.*\.Replace(' --exclude-dir=bin --exclude-dir=obj . | wc -l
      
      # params in method signatures (array allocation per call)
      grep -rn --include='*.cs' 'params ' --exclude-dir=bin --exclude-dir=obj . | wc -l
      
      # LINQ on strings — .All/.Any on IEnumerable<char> (replace with foreach loop)
      grep -rn --include='*.cs' -E '\.(All|Any)\(char\.' --exclude-dir=bin --exclude-dir=obj . | wc -l
      ```
      
      ### Patterns Requiring Manual Review
      
      - **Boxing via string.Format**: Can't determine argument types from grep — needs type analysis
      - **`+=` string concatenation in loops**: `+=` matches all types (int, list, event, string) — needs type context to confirm string
      - **`char.ToString()`**: Requires knowing the variable type is `char` — not reliably greppable
      
    • regex-patterns.md 3.2 KB
      # Regex Patterns
      
      ### Choose the Right Regex Engine Mode
      🟡 **DO** use `[GeneratedRegex]` for all static regex patterns, but never remove `NonBacktracking` if present | .NET 7+
      
      ❌
      ```csharp
      var r = new Regex(dynamicPattern, RegexOptions.Compiled);
      ```
      ✅
      ```csharp
      [GeneratedRegex("pattern")]
      private static partial Regex MyRegex();
      
      var safe = new Regex(untrustedPattern, RegexOptions.NonBacktracking);
      
      var oneOff = new Regex("pattern");
      ```
      
      **Impact: Source generator is always beneficial for static patterns. NonBacktracking prevents O(2^N) worst case — never remove it if present.**
      
      ### Use IsMatch When You Only Need a Boolean Result
      🟡 **DO** use `IsMatch` instead of `Match(...).Success` | .NET 7+
      
      ❌
      ```csharp
      bool found = Regex.Match(input, pattern).Success;
      ```
      ✅
      ```csharp
      bool found = Regex.IsMatch(input, pattern);
      ```
      
      **Impact: Avoids Match object allocation; with NonBacktracking, ~3x faster by skipping capture computation.**
      
      ### Use Regex.Count/EnumerateMatches Instead of Matches
      🟡 **DO** use `Count()` and `EnumerateMatches()` for allocation-free match processing | .NET 7+
      
      ❌
      ```csharp
      int count = 0;
      Match m = regex.Match(text);
      while (m.Success) { count++; m = m.NextMatch(); }
      ```
      ✅
      ```csharp
      int count = regex.Count(text);
      
      foreach (ValueMatch m in Regex.EnumerateMatches(text, @"\b\w+\b"))
      {
          ReadOnlySpan<char> word = text.AsSpan(m.Index, m.Length);
      }
      ```
      
      **Impact: ~3x faster than Match/NextMatch with NonBacktracking. Zero allocations for both Count and EnumerateMatches.**
      
      ### Use Span-Based Regex APIs for Allocation-Free Matching
      🟡 **DO** use `ReadOnlySpan<char>` overloads for regex matching on spans | .NET 7+
      
      ❌
      ```csharp
      string sub = largeBuffer.Substring(start, length);
      bool found = Regex.IsMatch(sub, pattern);
      ```
      ✅
      ```csharp
      ReadOnlySpan<char> text = largeBuffer.AsSpan(start, length);
      foreach (ValueMatch m in Regex.EnumerateMatches(text, @"\b\w+\b"))
      {
          ReadOnlySpan<char> word = text.Slice(m.Index, m.Length);
      }
      ```
      
      **Impact: Eliminates string allocations when working with spans — particularly valuable in high-throughput parsing pipelines.**
      
      ## Detection
      
      Scan recipes for regex anti-patterns. Run these and report exact counts.
      
      ```bash
      # Compiled regex count (startup cost budget — compare ratio to GeneratedRegex)
      grep -rn --include='*.cs' 'RegexOptions.Compiled' --exclude-dir=bin --exclude-dir=obj . | wc -l
      
      # GeneratedRegex count (already optimized — verify the inverse)
      grep -rn --include='*.cs' 'GeneratedRegex' --exclude-dir=bin --exclude-dir=obj . | wc -l
      
      # Uncached new Regex() calls (construction cost per call)
      grep -rn --include='*.cs' 'new Regex(' --exclude-dir=bin --exclude-dir=obj . | wc -l
      ```
      
      When `RegexOptions.Compiled` appears inside a class constructor or field initializer of an instantiated class (not a static singleton), count how many instances of that class are created at startup to determine total compiled regex budget. For example, if a `Rule` class compiles a regex in its constructor and 122 rules are registered, that is 122 compiled regexes at startup.
      
      ### Patterns Requiring Manual Review
      
      - **`new Regex(` uncached**: Field assignment may span multiple lines — grep on one line is unreliable. Verify that matched instances are stored in `static readonly` fields or `[GeneratedRegex]`.
      
    • structural-patterns.md 1.6 KB
      # Structural Patterns
      
      Patterns detected by the **absence** of a keyword or interface. These require codebase-wide counting scans, not single-file matching.
      
      ### Seal Classes for Devirtualization
      🟡 **DO** seal all leaf classes (those not subclassed) | .NET Core 3.0+
      
      Sealing lets the JIT devirtualize/inline virtual calls and use pointer comparison for type checks. Every non-abstract, non-static class that is not subclassed should be sealed.
      
      **Detection:** This is an absence pattern — scan for classes that are NOT sealed.
      
      ```bash
      # Count unsealed (non-abstract, non-static) classes
      grep -rn --include='*.cs' -E '^\s*((public|internal|private|protected|file)\s+)?(partial\s+)?class ' --exclude-dir=bin --exclude-dir=obj . | grep -v 'sealed' | grep -v 'abstract' | grep -v 'static' | wc -l
      
      # Count already-sealed classes (verify the inverse)
      grep -rn --include='*.cs' 'sealed class' --exclude-dir=bin --exclude-dir=obj . | wc -l
      ```
      
      **Exclusions:** Do not seal classes that are subclassed elsewhere in the codebase. Identifying base classes requires manual review — grep for `: ClassName` patterns and cross-reference, but expect false positives from interface implementations and generic constraints.
      
      ❌
      ```csharp
      internal class MyHandler : Base
      { public override int Run() => 42; }
      ```
      ✅
      ```csharp
      internal sealed class MyHandler : Base
      { public override int Run() => 42; }
      ```
      
      **Impact: Virtual calls up to 500x faster; type checks ~25x faster. Severity scales with count.**
      
      **Scale-based severity:**
      - 1-10 unsealed leaf classes → ℹ️ Info
      - 11-50 unsealed leaf classes → 🟡 Moderate
      - 50+ unsealed leaf classes → 🟡 Moderate (elevated priority)
      
  • SKILL.md 12.4 KB
    ---
    name: analyzing-dotnet-performance
    description: >-
      Scans .NET code for ~50 performance anti-patterns across async, memory,
      strings, collections, LINQ, regex, serialization, and I/O with tiered
      severity classification. Use when analyzing .NET code for optimization
      opportunities, reviewing hot paths, or auditing allocation-heavy patterns.
    license: MIT
    ---
    
    # .NET Performance Patterns
    
    Scan C#/.NET code for performance anti-patterns and produce prioritized findings with concrete fixes. Patterns sourced from the official .NET performance blog series, distilled to customer-actionable guidance.
    
    ## When to Use
    
    - Reviewing C#/.NET code for performance optimization opportunities
    - Auditing hot paths for allocation-heavy or inefficient patterns
    - Systematic scan of a codebase for known anti-patterns before release
    - Second-opinion analysis after manual performance review
    
    ## When Not to Use
    
    - **Algorithmic complexity analysis** — this skill targets API usage patterns, not algorithm design
    - **Code not on a hot path** with no performance requirements — avoid premature optimization
    
    ## Inputs
    
    | Input | Required | Description |
    |-------|----------|-------------|
    | Source code | Yes | C# files, code blocks, or repository paths to scan |
    | Hot-path context | Recommended | Which code paths are performance-critical |
    | Target framework | Recommended | .NET version (some patterns require .NET 8+) |
    | Scan depth | Optional | `critical-only`, `standard` (default), or `comprehensive` |
    
    ## Workflow
    
    ### Step 1: Load Critical Reference
    
    Resolve bundled paths from the directory that contains this `SKILL.md`, not from the user's workspace. Load this reference file first:
    
    - `references/critical-patterns.md`
    
    If a direct read fails, list this skill's `references/` directory once and retry only when the listing shows the expected file. Do not use workspace file or text search to locate the skill installation.
    
    ### Step 2: Detect Code Signals and Select Topic Recipes
    
    Scan the code for signals that indicate which pattern categories to check. Use the `## Detection` section from the critical reference when available and the inline recipes in Step 3 for initial signal detection.
    
    After detecting signals, load only the topic-specific references selected by scan depth:
    
    - `critical-only`: No additional references (use only `critical-patterns.md`)
    - `standard` (default): Load references matching detected signals from this list:
      - `references/async-patterns.md` — async/Task/ValueTask signals
      - `references/memory-and-strings.md` — Span/Memory/string allocation signals
      - `references/regex-patterns.md` — Regex signals
      - `references/collections-and-linq.md` — Dictionary/List/LINQ signals
      - `references/io-and-serialization.md` — JsonSerializer/HttpClient/Stream signals
      - `references/structural-patterns.md` — always loaded (unsealed classes checked regardless)
    - `comprehensive`: Load all six topic-specific references above
    
    For coverage reporting, the selected references are `references/critical-patterns.md` plus only the topic-specific references selected above. If any selected reference remains unavailable after retry, use the inline recipes in Step 3 for the missing coverage. Include `Reference coverage: reduced; unavailable: <paths>; used inline recipes for missing references.` in the final report, with `<paths>` replaced by the missing relative paths of the selected references only.
    
    Use the `## Detection` sections from loaded reference files and the inline recipes in Step 3 for categories whose reference files are unavailable.
    
    | Signal in Code | Topic |
    |----------------|-------|
    | `async`, `await`, `Task`, `ValueTask` | Async patterns |
    | `Span<`, `Memory<`, `stackalloc`, `ArrayPool`, `string.Substring`, `.Replace(`, `.ToLower()`, `+=` in loops, `params` | Memory & strings |
    | `Regex`, `[GeneratedRegex]`, `Regex.Match`, `RegexOptions.Compiled` | Regex patterns |
    | `Dictionary<`, `List<`, `.ToList()`, `.Where(`, `.Select(`, LINQ methods, `static readonly Dictionary<` | Collections & LINQ |
    | `JsonSerializer`, `HttpClient`, `Stream`, `FileStream` | I/O & serialization |
    
    Always check structural patterns (unsealed classes) regardless of signals.
    
    **Scan depth controls scope:**
    - `critical-only`: Only critical patterns (deadlocks, >10x regressions)
    - `standard` (default): Critical + detected topic patterns
    - `comprehensive`: All pattern categories
    
    ### Step 3: Scan and Report
    
    **For files under 500 lines, read the entire file first** — you'll spot most patterns faster than running individual grep recipes. Use grep to confirm counts and catch patterns you might miss visually.
    
    For each relevant pattern category, run the detection recipes below. Report exact counts, not estimates.
    
    **Core scan recipes** (run these when reference files aren't available):
    ```
    # Strings & memory
    grep -n '\.IndexOf(\"' FILE                    # Missing StringComparison
    grep -n '\.Substring(' FILE                    # Substring allocations
    grep -En '\.(StartsWith|EndsWith|Contains)\s*\(' FILE  # Missing StringComparison
    grep -n '\.ToLower()\|\.ToUpper()' FILE        # Culture-sensitive + allocation
    grep -n '\.Replace(' FILE                      # Chained Replace allocations
    grep -n 'params ' FILE                         # params array allocation
    
    # Collections & LINQ
    grep -n '\.Select\|\.Where\|\.OrderBy\|\.GroupBy' FILE  # LINQ on hot path
    grep -n '\.All\|\.Any' FILE                    # LINQ on string/char
    grep -n 'new Dictionary<\|new List<' FILE      # Per-call allocation
    grep -n 'static readonly Dictionary<' FILE     # FrozenDictionary candidate
    
    # Regex
    grep -n 'RegexOptions.Compiled' FILE           # Compiled regex budget
    grep -n 'new Regex(' FILE                      # Per-call regex
    grep -n 'GeneratedRegex' FILE                  # Positive: source-gen regex
    
    # Structural
    grep -n 'public class \|internal class ' FILE  # Unsealed classes
    grep -n 'sealed class' FILE                    # Already sealed
    grep -n ': IEquatable' FILE                    # Positive: struct equality
    ```
    
    **Rules:**
    - Run every relevant recipe for the detected pattern categories
    - **Emit a scan execution checklist** before classifying findings — list each recipe and the hit count
    - A result of **0 hits** is valid and valuable (confirms good practice)
    - If reference files were loaded, also run their `## Detection` recipes
    
    **Verify-the-Inverse Rule:** For absence patterns, always count both sides and report the ratio (e.g., "N of M classes are sealed"). The ratio determines severity — 0/185 is systematic, 12/15 is a consistency fix.
    
    ### Step 3b: Cross-File Consistency Check
    
    If an optimized pattern is found in one file, check whether sibling files (same directory, same interface, same base class) use the un-optimized equivalent. Flag as 🟡 Moderate with the optimized file as evidence.
    
    ### Step 3c: Compound Allocation Check
    
    After running scan recipes, look for these multi-allocation patterns that single-line recipes miss:
    
    1. **Branched `.Replace()` chains:** Methods that call `.Replace()` across multiple `if/else` branches — report total allocation count across all branches, not just per-line.
    2. **Cross-method chaining:** When a public method delegates to another method that itself allocates intermediates (e.g., A calls B which does 3 regex replaces, then A calls C), report the total chain cost as one finding.
    3. **Compound `+=` with embedded allocating calls:** Lines like `result += $"...{Foo().ToLower()}"` are 2+ allocations (interpolation + ToLower + concatenation) — flag the compound cost, not just the `.ToLower()`.
    4. **`string.Format` specificity:** Distinguish resource-loaded format strings (not fixable) from compile-time literal format strings (fixable with interpolation). Enumerate the actionable sites.
    
    ### Step 4: Classify and Prioritize Findings
    
    Assign each finding a severity:
    
    | Severity | Criteria | Action |
    |----------|----------|--------|
    | 🔴 **Critical** | Deadlocks, crashes, security vulnerabilities, >10x regression | Must fix |
    | 🟡 **Moderate** | 2-10x improvement opportunity, best practice for hot paths | Should fix on hot paths |
    | ℹ️ **Info** | Pattern applies but code may not be on a hot path | Consider if profiling shows impact |
    
    **Prioritization rules:**
    1. If the user identified hot-path code, elevate all findings in that code to their maximum severity
    2. If hot-path context is unknown, report 🔴 Critical findings unconditionally; report 🟡 Moderate findings with a note: _"Impactful if this code is on a hot path"_
    3. Never suggest micro-optimizations on code that is clearly not performance-sensitive
    
    **Scale-based severity escalation:**
    When the same pattern appears across many instances, escalate severity:
    - 1-10 instances of the same anti-pattern → report at the pattern's base severity
    - 11-50 instances → escalate ℹ️ Info patterns to 🟡 Moderate
    - 50+ instances → escalate to 🟡 Moderate with elevated priority; flag as a codebase-wide systematic issue
    
    Always report exact counts (from scan recipes), not estimates or agent summaries.
    
    ### Step 5: Generate Findings
    
    **Keep findings compact.** Each finding is one short block — not an essay. Group by severity (🔴 → 🟡 → ℹ️), not by file.
    
    Format per finding:
    
    ```
    #### ID. Title (N instances)
    **Impact:** one-line impact statement
    **Files:** file1.cs:L1, file2.cs:L2, ... (list locations, don't build tables)
    **Fix:** one-line description of the change (e.g., "Add `StringComparison.Ordinal` parameter")
    **Caveat:** only if non-obvious (version requirement, correctness risk)
    ```
    
    **Rules for compact output:**
    - **No ❌/✅ code blocks** for trivial fixes (adding a keyword, parameter, or type change). A one-line fix description suffices.
    - **Only include code blocks** for non-obvious transformations (e.g., replacing a LINQ chain with a foreach loop, or hoisting a closure).
    - **File locations as inline comma-separated list**, not a table. Use `File.cs:L42` format.
    - **No explanatory prose** beyond the Impact line — the severity icon already conveys urgency.
    - **Merge related findings** that share the same fix (e.g., all `.ToLower()` calls go in one finding, not split by file).
    - **Positive findings** in a bullet list, not a table. One line per pattern: `✅ Pattern — evidence`.
    
    End with a summary table and disclaimer:
    
    ```markdown
    | Severity | Count | Top Issue |
    |----------|-------|-----------|
    | 🔴 Critical | N | ... |
    | 🟡 Moderate | N | ... |
    | ℹ️ Info | N | ... |
    
    > ⚠️ **Disclaimer:** These results are generated by an AI assistant and are non-deterministic. Findings may include false positives, miss real issues, or suggest changes that are incorrect for your specific context. Always verify recommendations with benchmarks and human review before applying changes to production code.
    ```
    
    ## Validation
    
    Before delivering results, verify:
    
    - [ ] All critical patterns were checked (from reference files or inline recipes)
    - [ ] Topic-specific recipes run only when matching signals detected
    - [ ] Each finding includes a concrete code fix
    - [ ] Scan execution checklist is complete (all recipes run)
    - [ ] Summary table included at end
    
    ## Common Pitfalls
    
    | Pitfall | Correct Approach |
    |---------|-----------------|
    | Flagging every `Dictionary` as needing `FrozenDictionary` | Only flag if the dictionary is never mutated after construction |
    | Suggesting `Span<T>` in async methods | Use `Memory<T>` in async code; `Span<T>` only in sync hot paths |
    | Reporting LINQ outside hot paths | Only flag LINQ in identified hot paths or tight loops; LINQ is acceptable in code that runs infrequently. Since .NET 7, LINQ Min/Max/Sum/Average are vectorized — blanket bans on LINQ are misguided |
    | Suggesting `ConfigureAwait(false)` in app code | Only applicable in library code; not primarily a performance concern |
    | Recommending `ValueTask` everywhere | Only for hot paths with frequent synchronous completion |
    | Flagging `new HttpClient()` in DI services | Check if `IHttpClientFactory` is already in use |
    | Suggesting `[GeneratedRegex]` for dynamic patterns | Only flag when the pattern string is a compile-time literal |
    | Suggesting `CollectionsMarshal.AsSpan` broadly | Only for ultra-hot paths with benchmarked evidence; adds complexity and fragility |
    | Suggesting `unsafe` code for micro-optimizations | Avoid `unsafe` except where absolutely necessary — do not recommend it for micro-optimizations that don't matter. Safe alternatives like `Span<T>`, `stackalloc` in safe context, and `ArrayPool` cover the vast majority of performance needs |
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related