Skip to content
learn

Advanced C# — Mission

I have written C# for years, and most of what I know about its performance is folklore: “structs are faster”, “avoid LINQ”, “always ConfigureAwait(false)”, “FrozenDictionary is the fast one”. Some of it was true in .NET Framework 4.5 and is wrong on .NET 10, where the JIT removes allocations the IL asks for. This course replaces each piece of folklore with something I can look at: the size of an object, the IL the compiler emitted, the state machine behind await, the generation of an array, a BenchmarkDotNet table.

The same goes for the code that moves data between tasks and for ASP.NET Core. I use channels and BackgroundService without having checked what happens when a consumer stops early or a producer throws, and I configure ASP.NET Core by copying what worked last time. The later parts of the course run those questions as programs too.

The measurements run on real code, not toy classes: Guitar Alchemist (GA), a large .NET 10 code base about music theory. Its value objects, its caches, its voicing generator and its hosted services are exactly the kind of code where these questions come up, and the lessons found several places where GA pays for something it didn’t intend to, or hangs where it should fail.

You write C# every day and know the language well: generics, LINQ, async/await, records, pattern matching. You want to know what the compiler, the JIT, the garbage collector and ASP.NET Core do with that code, and to measure before you optimize. If you are starting with C#, begin with the C# for beginners course, which ends where this one starts.

If you also work with Java, parts 2 and 3 end each lesson with an “If you know Spring and Reactor” table, from C# to Java. The Spring Boot, Spring Cloud and Reactor course goes the other way, from ASP.NET Core to Spring; the two courses link to each other instead of explaining the same thing twice.

By the end of this course, I will be able to

Section titled “By the end of this course, I will be able to”
  • predict the size of a value or an object, spot boxing in IL, and tell when the JIT removes it;
  • use ref, in, ref readonly, Span<T> and stackalloc without defensive copies or escaping references;
  • explain generations, the large and pinned object heaps and the GC modes, and read GC.GetGCMemoryInfo;
  • read the state machine the compiler generates for an async method, choose between Task and ValueTask, and avoid deadlocks and lost cancellations;
  • write a BenchmarkDotNet benchmark that measures what I think it measures, and interpret tiered compilation and PGO;
  • choose between Dictionary, FrozenDictionary, SearchValues and plain arithmetic, and vectorize a loop with Vector<T> or TensorPrimitives;
  • connect producers and consumers with channels, TPL Dataflow, Rx.NET or IAsyncEnumerable, and say for each what happens under backpressure, on an error and on cancellation;
  • follow a request through ASP.NET Core, from Kestrel to the endpoint, and choose service lifetimes, options, filters, hosted services, resilience and caching deliberately;
  • observe and test an ASP.NET Core service, and publish it with Native AOT;
  • and, in the last part: expression trees, source generators, Roslyn analyzers and interop.

Parts 1 and 2 measure GA’s own code. Part 3 builds a small scales and chords service on GA’s domain types, the ASP.NET Core counterpart of the scales service in the Spring Boot, Spring Cloud and Reactor course: the same endpoints, so that each lesson can compare the two implementations line by line.

# Lesson Under the hood Measured on GA Spring and Reactor
1 Memory: values, references and spans object layout, boxing in IL, ref/in, ref struct, Span<T>, stackalloc PitchClass, PitchClassSetId.ItemsSpan no value types, erased generics, releasing a DataBuffer
2 The garbage collector generations, LOH and POH, workstation and server GC, DATAS, finalizers, GC.GetGCMemoryInfo allocations of ItemsSpan with [MemoryDiagnoser] G1 and ZGC, Cleaner, JMH’s -prof gc
3 async and await under the hood the generated state machine, ValueTask, SynchronizationContext, ConfigureAwait, cancellation, IAsyncEnumerable Try.OfAsync, LazyWithExpiration assembly and subscription, publishOn, virtual threads
4 Measured performance BenchmarkDotNet, tiered JIT and PGO, SearchValues, FrozenDictionary, Vector<T> PitchClass subtraction, SimdOps.Dot JMH, C1 and C2, the Vector API
5 Generics in depth constraints, static abstract members, generic math, allows ref struct, how the JIT shares generic code GA’s IStaticValueObjectList<TSelf> erasure, and generics without primitives
# Lesson Under the hood Spring and Reactor
6 Channels bounded and unbounded channels, full modes, completion and errors, several producers and consumers, cancellation; GA’s voicing generator and index command onBackpressureBuffer, onBackpressureDrop, BlockingQueue
7 TPL Dataflow blocks and links, parallelism and order, BoundedCapacity, faults that only flow downstream, completion; GA’s Dataflow demo flatMap with concurrency, buffer, publishOn
8 Rx.NET IObservable<T>, cold and hot, operators in virtual time, schedulers, no backpressure, retries; GA’s reactive demo Flux, Sinks, publishOn, StepVerifier.withVirtualTime
9 Choosing a stream IAsyncEnumerable and System.Linq.AsyncEnumerable, the four stream types measured side by side, bridges, throughput, a decision chart Reactor: Mono and Flux, Reactor under the hood
10 Shared state and the thread pool System.Threading.Lock, Interlocked, concurrent collections, Parallel.ForEachAsync, thread pool starvation synchronized, ReentrantLock, virtual threads
# Lesson Under the hood Spring and Reactor
11 Hosting, WebApplication and Kestrel the generic host, the builder, Kestrel’s connection and request limits, graceful shutdown Spring Boot seen from ASP.NET Core, embedded Tomcat and Netty
12 The middleware pipeline Use, Map, Run, ordering, short-circuits, exception handling, endpoint routing Servlet filters, WebFilter
13 Dependency injection and options lifetimes, captive dependencies, scope validation, keyed services, IOptions, IOptionsSnapshot, IOptionsMonitor, validation the Spring container, @ConfigurationProperties
14 Minimal APIs and controllers route handlers and parameter binding, filters, validation, TypedResults, streaming IAsyncEnumerable responses @RestController, WebFlux functional endpoints
15 Hosted services IHostedService, BackgroundService, startup and shutdown order, exceptions, queues with channels; GA’s hosted services @Scheduled, SmartLifecycle
16 Authentication and authorization schemes, handlers, JWT bearer, policies and requirements Spring Security
17 Petri specification oracles for C# pipelines finite lifecycle models, complete reachability, terminal classification, deterministic runtime gates; Channel queues, Dataflow execution/output retention and Rx assumptions kept distinct the same method applies to Reactor only after its capacity and scheduler semantics are modeled explicitly
18 gRPC and SignalR protobuf contracts, streaming calls, hubs, backpressure over the network Spring gRPC, WebSocket, RSocket
19 Resilience, rate limiting and output caching Microsoft.Extensions.Http.Resilience, Polly pipelines, rate limiters, output cache policies Resilience4j, Spring Cloud Circuit Breaker
20 OpenTelemetry and diagnostics in production System.Diagnostics.Metrics, ActivitySource, OpenTelemetry exporters, dotnet-counters, dotnet-trace, dotnet-dump Micrometer, Actuator
21 Testing with WebApplicationFactory the test host, replacing services, authentication in tests, Testcontainers @SpringBootTest, WebTestClient
22 Native AOT and trimming publishing an API with Native AOT, trimming warnings, the request delegate generator, startup and size measured GraalVM native images
# Lesson Under the hood Spring and Reactor
23 Expression trees, reflection and source generators what a lambda compiles to, Expression<T>, the cost of reflection, incremental generators, [GeneratedRegex] annotation processors, Spring’s AOT engine
24 Roslyn analyzers and code fixes syntax and semantic models, writing an analyzer and its tests Error Prone, SpotBugs
25 Interop and unsafe code [LibraryImport], function pointers, Unsafe, MemoryMarshal, pinning JNI, and the foreign function and memory API
# Page Under the hood Measured on GA
1 Five optimisations, proved then measured rotations and PopCount on 12-bit sets, lookup tables, an equivalence proof over the whole input domain, stable sorts as a tie-break, and a benchmark that measured the JIT instead of the code IsClusterFree, IntervalClassVector, ClosestDiatonicKey, ToNormalForm, PrimeForm
2 GA, profiled, then proved and measured dotnet-trace on a real pipeline, 12-bit masks instead of hash sets, boxed enumerators, a 4096-slot cache keyed by the set itself, a LINQ property in a hot loop, and a proof that compares two builds of GA byte for byte CanonicalChordRecognizer, ChordIntervalPattern.TryMatch, IntervalClassVector, OptickIndexReader
3 Petri-net lifecycle oracle one bounded pipeline with explicit success, failure and cancellation; complete reachability and no non-terminal dead marking the Channel failure shapes studied in lessons 6 and 9
4 Plugins without MEF one collectible AssemblyLoadContext per module, AssemblyDependencyResolver, one dependency-injection container, two versions of a library side by side, type identity, and a timer that keeps its module loaded not GA: four small modules written for the appendix
— Journal

Lessons 18 to 25 are planned and not written yet. Lesson 17 is an advanced concurrency lab added after the ASP.NET Core lessons; lessons 6 to 9 were written before lesson 5 and don’t depend on it.

  • The .NET 10 SDK and Git. On Windows, run the course scripts from Git Bash.
  • check.sh restores ilspycmd as a local .NET tool (version 11.0.0.9375, pinned in .config/dotnet-tools.json), fetches the three GA projects the program uses, about 11 MB, and restores System.Reactive 7.0.0 for lesson 8, and Newtonsoft.Json 13.0.3 and 13.0.1 for appendix 4.
  • For the benchmarks, a machine you can keep quiet for a few minutes: close the browser, plug in the laptop.