Dotnet performance review
Skill tunahanaliozturk/secure-dotnet-skills/skills/dotnet-performance-review
Aegis — 12 judgment-style agent skills for secure, production-grade .NET on Azure (security, design, performance, concurrency, observability). Works with Claude Code, Codex, Cursor, Gemini.
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.
What its author says it does
Copied from the file, not written here
Use when reviewing .NET code for performance — allocations and GC pressure, async/IO misuse, hot-path LINQ, repeated enumeration, string handling, caching, and serialization.
SKILL.md
10.1 KB, 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.