Dotnet performance review
Skill tunahanaliozturk/secure-dotnet-skills/skills/dotnet-performance-review
Use when reviewing .NET code for performance — allocations and GC pressure, async/IO misuse, hot-path LINQ, repeated enumeration, string handling, caching, and serialization.From its SKILL.md
npx -y skills add tunahanaliozturk/secure-dotnet-skills --skill dotnet-performance-reviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
10.1 KB, ~2.2k tokens by cl100k_base, as published. Nobody here has run it
.NET Performance Review
Directs the agent to walk .NET / ASP.NET Core code through concrete performance lenses — allocations, async/IO, enumeration, caching, and serialization — identifying high-impact issues and recommending measurement before any micro-optimization.
When to use
- A request handler, background service, or library method is reported as slow or high-allocation under load.
- A PR adds or changes LINQ queries, serialization, caching, or HTTP client usage in a hot path.
- A profiler or BenchmarkDotNet run has flagged a method as a hotspot and the team needs a structured review.
- Code is being hardened for production throughput before a load test or capacity review.
Process
- Identify hot paths. Find the methods called most often or under the most load — request handlers, inner loops, serialization boundaries, and background job loops. Scope the review to those paths first; premature optimization of cold code wastes time.
- Audit allocations and GC pressure. Walk each hot path for unnecessary heap allocations: string concatenation in loops, boxing of value types, closure capture, LINQ chains materializing intermediate collections, and
paramsarray creation. - Audit async and I/O usage. Check for sync-over-async calls (
.Result/.Wait()),async voidmethods outside event handlers, missed parallelism opportunities (Task.WhenAll), and large result sets that should stream viaIAsyncEnumerable<T>. - Audit enumeration and LINQ. Look for multiple enumeration of the same
IEnumerable<T>, needless.ToList()/.ToArray()materializations used only to call.Count, and LINQ expressions that force client-side evaluation or generate N+1 queries. - Audit caching and object reuse. Check whether expensive repeated computations (serializer options, compiled regexes, HTTP responses) are computed fresh on every request or properly reused via
IMemoryCache,HybridCache, or static initialization. - Prioritize by impact and recommend measuring before optimizing. Rank findings by likely throughput or latency impact. For any finding where the gain is uncertain, require a BenchmarkDotNet micro-benchmark or a profiler trace (dotnet-trace, PerfView, Visual Studio Diagnostic Tools) before investing in the fix. Avoid claiming wins without measurement.
.NET / Azure checks
- String allocations in loops.
string result += item;inside a loop creates a newstringobject on every iteration — O(n²) allocations. Replace withStringBuilderfor imperative concatenation, or use interpolated-string handlers (C# 10+) withStringBuilder.AppendInterpolatedStringHandlerfor mixed format strings. In hot serialization paths, write to anIBufferWriter<byte>instead of astringintermediate. - Boxing of value types. Casting a
struct,int,Guid,DateTime, or enum toobjector a non-generic interface (IComparable,IFormattable) allocates a heap box. Common sources: adding value types toArrayList, passing tostring.Format'sobject[]params, using non-generic collections, or calling virtual methods on interfaces through a boxed value. Prefer generic collections and constrained generics. Span<T>/Memory<T>/ArrayPool<T>for buffers. Methods that slice, parse, or transform byte or char data should operate onSpan<T>(stack-only) orMemory<T>(heap-friendly) rather than allocating sub-arrays. For temporary buffers (e.g., encode/decode scratch space), rent fromArrayPool<T>.Sharedand return in afinallyblock rather than allocatingnew byte[n]on every call.- Needless
.ToList()/.ToArray()materialization. Calling.ToList()only to call.Counton the result, or.ToArray()to pass to a method that acceptsIEnumerable<T>, forces full materialization without benefit. Use.Count()directly onIQueryable<T>to push the count to the database, or acceptIEnumerable<T>at the call site and avoid materializing until necessary. - Multiple enumeration of
IEnumerable<T>. Enumerating the sameIEnumerable<T>more than once (.Count()thenforeach, or two separateWhere/Anycalls on a deferred query) executes the underlying query or iterator twice. If the sequence is deferred (a LINQ query, a yield-return, or an EF CoreIQueryable), materialize it once with.ToList()or.ToArray()and reuse. ForIQueryable<T>, prefer projecting the count and results in a single round-trip where the ORM supports it. - Sync-over-async:
.Result/.Wait(). Calling.Resultor.Wait()on aTaskin an environment with a synchronization context (ASP.NET Core on older hosting, or any code ultimately marshaled back to a captured context) can deadlock. Even where deadlock does not occur, it wastes a thread-pool thread blocking synchronously. Go async all the way:await taskinstead oftask.Result. Note:GetAwaiter().GetResult()carries the same risk. The deadlock is context-dependent — ASP.NET Core's default context does not deadlock the way classic ASP.NET did — but blocking still wastes threads under load. async voidoutside event handlers.async voidmethods swallow exceptions (they are raised on the thread-pool and crash the process or disappear silently). They also cannot be awaited by callers. Useasync Taskfor all async methods except event handlers (+=subscriptions) where the delegate signature requiresvoid.IAsyncEnumerable<T>for streaming results. Methods that produce large result sets (database cursors, external API pages, file streams) should returnIAsyncEnumerable<T>and be consumed withawait foreachrather than buffering into aList<T>and returning. This keeps peak memory bounded and reduces time-to-first-byte latency for the caller.Task.WhenAllfor independent I/O. Sequentialawait call1; await call2; await call3;where the calls are independent serializes I/O unnecessarily. UseTask.WhenAll(call1, call2, call3)(orTask.WhenAllover a projected sequence) to fan out and await all completions concurrently. Be aware of the sharedDbContextconstraint: a singleDbContextis not thread-safe for concurrent operations.IHttpClientFactoryvsnew HttpClient()per call. Constructingnew HttpClient()per request (or per method call) exhausts socket connections — the underlyingSocketsHttpHandleris not reused, so old connections linger inTIME_WAIT. Register typed or named clients viabuilder.Services.AddHttpClient<TClient, TImpl>()and injectIHttpClientFactoryor the typed client; the factory manages handler lifetime and connection pooling.IMemoryCache/HybridCachefor expensive repeated work. Repeated calls to external APIs, database lookups, or CPU-heavy computations that return the same result within a time window should be cached. UseIMemoryCache(in-process) orHybridCache(.NET 9+, two-tier with distributed backing) rather than astatic Dictionaryor per-request recalculation. For high-concurrency scenarios, useGetOrCreateAsyncwith a factory to avoid cache-stampede (multiple threads computing the same value simultaneously).- Reuse
JsonSerializerOptionsand use System.Text.Json source generation.new JsonSerializerOptions { ... }on every serialize/deserialize call causes reflection-based metadata to be compiled on each construction — this is both slow and allocation-heavy. Create a singlestatic readonly JsonSerializerOptionsinstance (or register it viaAddJsonOptionsin ASP.NET Core). For maximum throughput in hot paths, use System.Text.Json source generation ([JsonSerializable]+JsonSerializerContext) to eliminate runtime reflection entirely.
Red flags
| Signal | Why it matters |
|---|---|
.Result or .Wait() in a request handler or service | Blocks a thread-pool thread synchronously; can deadlock in contexts with a synchronization context; wastes throughput under load. Go async all the way. |
new HttpClient() constructed per request or per method call | No connection pooling — each instance opens new TCP connections that linger in TIME_WAIT, exhausting ephemeral ports under moderate traffic. Use IHttpClientFactory. |
string result += item inside a loop | Allocates a new string on every concatenation — O(n²) total allocation. Use StringBuilder or interpolated-string handlers. |
IEnumerable<T> enumerated more than once (e.g., .Count() then foreach) | Executes the underlying query or iterator twice; for EF Core IQueryable<T> this means two round-trips to the database. Materialize once. |
new JsonSerializerOptions(...) inside a method | Triggers reflection-based metadata compilation on every call. Use a static readonly instance or ASP.NET Core's registered options. |
async void on a method that is not a UI/event handler | Exceptions are unobserved and crash or silently disappear; the method cannot be awaited. Return Task instead. |
.ToList() called only to use .Count on the result | Materializes the full sequence needlessly. Call .Count() on IQueryable<T> (pushes to the DB) or .Any() when only presence is needed. |
new byte[size] allocated on every call for a scratch buffer | Creates GC pressure proportional to request rate. Rent from ArrayPool<T>.Shared and return in finally. |
Sequential await of independent I/O calls | Serializes work that could run in parallel. Replace with await Task.WhenAll(...) for independent tasks. |
Missing IMemoryCache / HybridCache for a known hot lookup | Every request recomputes or re-fetches data that does not change per call. Cache with a TTL and a stampede guard. |
Example
See examples/dotnet-performance-review/.
Related skills
- ef-core-review — use for deep EF Core query performance (N+1, tracking, projections).
- resilience-review — use to review timeouts and retry policies on downstream calls that affect throughput.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.