15 Advanced C# Mistakes You’re Probably Still Making
This title could be clearer and more informative.Try out Clickbait Shieldfor free (5 uses left this month).
A deep dive into 15 advanced C# pitfalls that affect performance and correctness in production systems. Topics covered include misusing ValueTask semantics (awaiting twice, concurrent awaits), defensive copies from non-readonly structs, misusing required properties and nullable reference types, hidden boxing allocations in generics, Large Object Heap fragmentation and ArrayPool usage, closure captures in lambdas, over-chaining LINQ with intermediate materializations, multiple enumeration of deferred queries, GC pressure from string interpolation in logging hot paths (and the [LoggerMessage] fix), broken GetHashCode/Equals contracts with mutable keys, JIT devirtualization gains from sealed classes, DI factory delegate overhead, fire-and-forget async pitfalls with Channel<T> alternatives, captive dependencies in ASP.NET Core middleware, and non-atomic factory execution in ConcurrentDictionary.GetOrAdd. Each mistake includes runtime mechanics, bad/good code examples, and profiling tips using BenchmarkDotNet, dotMemory, and dotnet-counters.
Table of contents
13. “Fire-and-Forget” Async Calls without Error Handling or Context14. Capturing Scoped or Transient Dependencies in Middleware Constructors15. Assuming Delegates inside ConcurrentDictionary are AtomicQuestions this post answers
Why should I not await a ValueTask more than once in C#?
A ValueTask backed by IValueTaskSource uses a pooled object that is reset and returned to a shared pool immediately after being awaited once. Awaiting the same ValueTask a second time, calling .Result before completion, or awaiting it concurrently introduces severe race conditions with pooled object reuse. If multiple awaits are needed, convert to a standard Task first using .AsTask() and await that. Developers navigating async API design trade-offs like this track .NET runtime nuances on daily.dev.
Why does ConcurrentDictionary.GetOrAdd run the factory delegate multiple times under concurrency?
ConcurrentDictionary.GetOrAdd does not execute the factory delegate inside its internal lock to avoid lock contention. Under high concurrency, two threads requesting the same missing key will both execute their factory delegates simultaneously; the dictionary stores one result and discards the other. For expensive or side-effectful factories, wrap creation in Lazy<T> with LazyThreadSafetyMode.ExecutionAndPublication to guarantee the delegate runs exactly once. Teams building caching layers in C# find patterns like this discussed alongside real-world benchmarks on daily.dev.
How do I avoid GC pressure from logging in C# hot paths?
Using string interpolation directly in ILogger calls evaluates and allocates the formatted string before the logger checks whether that log level is enabled, causing allocations on every call even when logging is disabled. The fix is to use the [LoggerMessage] source generator attribute, which generates a compiled, strongly-typed logging method with zero string allocations, zero object[] array allocations, and zero boxing when the target log level is disabled. C# developers optimizing high-throughput services stay current on patterns like [LoggerMessage] through daily.dev.