Skip to content

Chapter 7 of 7

Collections, Concurrency, Resources, and Diagnostics

The choice of collection profoundly affects performance. Dictionary provides O(1) average-case lookup, insertion, and deletion via a hash table, with TryGetValue preferred over ContainsKey followed by the indexer to avoid double-hashing. SortedDictionary uses a red-black tree for O(log n) operations across the board, while SortedList uses a sorted array with O(log n) lookup but O(n) insert and remove due to shifts. HashSet holds unique items with O(1) Contains, Add, and Remove, while List is indexable and ordered but has O(n) Contains. Array.Length is a property on arrays with O(1) cost, while the LINQ Count() extension walks the sequence unless it is an ICollection. Array.Copy may use a slower path when source and destination overlap, while Array.ConstrainedCopy requires non-overlapping regions with identical element types and is suitable for constrained execution regions. For read-heavy scenarios, frozen collections (System.Collections.Frozen, .NET 8+) and FrozenSet are slow to build but very fast to read, with FrozenSet optimized for lookups over a build-once set; immutable collections in System.Collections.Immutable provide truly immutable trees with structural sharing, while ReadOnlyCollection is a thin wrapper over a mutable collection that can still change underneath.

Concurrency control in C# has several layers. The lock statement is syntactic sugar over Monitor.Enter and Monitor.Exit inside a try/finally, with the locked object ideally a private, static, reference-typed field; locking on this, typeof(...), or strings is a known anti-pattern that can cause deadlocks. Use Monitor directly only when you need TryEnter, Wait, Pulse, or finer control. volatile tells the compiler that a field may be modified by multiple threads, preventing reordering and caching of reads and writes, but it is limited to specific primitive types and reference types. Concurrent collections in System.Collections.Concurrent, including ConcurrentDictionary, ConcurrentQueue, ConcurrentStack, ConcurrentBag, and BlockingCollection, provide thread-safe access without external locking, using lock-free and fine-grained locking internally. For cross-process coordination, Mutex is a single-owner lock that can be named system-wide (often used to ensure only one instance of an app runs), while SemaphoreSlim allows N concurrent holders in-process. AsyncLocal flows values with the async control flow and is appropriate for ambient context like HttpContext, unlike ThreadLocal which is per OS thread and is broken by async continuations.

Resource management follows the IDisposable pattern. A class that owns unmanaged resources should implement IDisposable and follow the dispose pattern: a public Dispose() method that calls Dispose(true) and GC.SuppressFinalize, a protected virtual Dispose(bool disposing) method that frees both managed and unmanaged resources when disposing is true and unmanaged resources only when false, and a finalizer that calls Dispose(false) as a last-resort safety net. Dispose is called deterministically by user code (typically via using), while the finalizer is called by the GC at an indeterminate time and should not allocate or reference other managed objects. Modern code often avoids finalizers by using SafeHandle. The using statement comes in two forms: the classic using (var r = ...) { ... } disposes at the end of the block, while the C# 8 using var r = ... declaration disposes at the end of the enclosing scope, reducing indentation. Throw helpers in .NET 6+ (ArgumentNullException.ThrowIfNull, ArgumentException.ThrowIfNullOrEmpty, ArgumentException.ThrowIfNullOrWhiteSpace) reduce validation boilerplate. Exception.Data is an IDictionary for arbitrary context like request IDs, while InnerException is the conventional wrapped cause; use Data for telemetry and throw new MyException("msg", innerEx) to wrap a cause. Even in managed code, memory leaks arise from references the GC cannot see as unreachable: static fields, unsubscribed event handlers, unbounded caches, and undisposed IDisposable instances. WeakReference lets you hold an object without preventing collection, which is useful in caches and listener patterns, though you must always check TryGetTarget before use.

Reflection, logging, and configuration support the runtime diagnostic surface. Reflection in System.Reflection lets you inspect types and invoke members dynamically, which is powerful for DI containers, ORMs, and serializers but is slow and lacks compile-time checks; source generators and expression trees are modern alternatives for performance-critical paths. The System.Text.Json source generator emits serialization code at compile time, eliminating reflection and enabling AOT and trimmed apps. nameof returns a symbol's name as a string with compile-time checking, ideal for argument validation and INotifyPropertyChanged. The CallerMemberName, CallerFilePath, and CallerLineNumber attributes auto-fill parameters with the calling context. Microsoft.Extensions.Logging.ILogger is the standard logging abstraction, supporting structured logging, levels (Trace through Critical), and providers (Console, Debug, Serilog, Application Insights); inject it via DI rather than calling Console.WriteLine in production code. The built-in DI container in ASP.NET Core supports three lifetimes: Singleton (one per app), Scoped (one per request), and Transient (new each time). IOptions is a singleton that reads configuration once at startup, while IOptionsSnapshot is scoped and re-reads per request, and IOptionsMonitor is a singleton that watches for changes and raises a callback. BackgroundService is the base class for IHostedService that simplifies long-running background work by handling start and stop, with ExecuteAsync receiving a CancellationToken. Lazy defers expensive initialization until first access with thread safety, and LazyInitializer.EnsureInitialized is a memory-efficient alternative for single-field lazy singletons that avoids the wrapper object. Stopwatch provides a high-resolution monotonic counter suitable for benchmarking, while DateTime.UtcNow has 10-15 ms resolution and can jump on system clock changes. DateTimeOffset stores a date plus an explicit offset from UTC, unambiguously identifying a moment, while DateTime with its Kind enum can be ambiguous and is less safe for server-side time-zone math. Environment.NewLine gives the platform-appropriate line separator, but for cross-platform files you should pick one explicitly and document it. The dynamic keyword bypasses compile-time checks via the DLR and is appropriate for COM interop, dynamic JSON, and interop with dynamic languages, while object is statically typed; is null and == null differ when == is overloaded, so prefer is null for reference-type null checks.

All chapters
  1. 1Type System and Language Fundamentals
  2. 2Functions, Delegates, and Events
  3. 3Modern C# Language Features
  4. 4LINQ and Data Querying
  5. 5Asynchronous and Parallel Programming
  6. 6Memory, Performance, and Strings
  7. 7Collections, Concurrency, Resources, and Diagnostics

Drill it

Reading is not remembering. These come from the Csharp Programming deck:

Q

What is LINQ in C#?

LINQ (Language Integrated Query) is a set of features that allows you to write query expressions directly in C# to filter, sort, and transform data from collect...

Q

What is the difference between LINQ query syntax and method syntax?

Query syntax resembles SQL:var q = from x in list where x > 5 select x;Method syntax uses extension methods:var q = list.Where(x => x > 5);Both compile to the s...

Q

What does the LINQ Select method do?

Select projects each element of a sequence into a new form (a transformation/map operation).var names = people.Select(p => p.Name);It returns an IEnumerable<...

Q

What is deferred execution in LINQ?

Deferred execution means a LINQ query is not executed when defined, but only when the results are enumerated (e.g., via foreach, ToList(), or Count()). This all...