Skip to content

C# Cheatsheet

Modern object-oriented language for .NET, web, and games.

01

Getting Started

Hello World

C# programs start in Main(). using System; imports the System namespace (Console, Math, etc.). C# 9+ supports top-level statements: a file with just Console.WriteLine("Hello"); is a valid program. The compiler generates the class and Main automatically.

csharp
using System;

class Program {
    static void Main() {
        Console.WriteLine("Hello, World!");
    }
}

Variables & Types

C# is statically typed. var lets the compiler infer the type (still type-safe at compile time). decimal (m suffix) is for financial calculations with exact precision. const is a compile-time constant; readonly is a runtime constant set in the constructor.

csharp
int age = 30;
double pi = 3.14159;
decimal price = 19.99m;
char grade = 'A';
bool isDev = true;
string name = "Alice";
var x = 42;  // implicitly typed (int)
const double TAX = 0.08;

String Interpolation

String interpolation ($"...") embeds expressions in braces. Format specifiers after : control output (F2 for 2 decimals, yyyy-MM-dd for dates). Verbatim strings (@"") treat backslashes literally—useful for file paths and regex. Combine both: $@"...".

csharp
string name = "Alice";
int age = 30;
Console.WriteLine($"Name: {name}, Age: {age}");

// Formatting
double pi = 3.14159;
Console.WriteLine($"Pi: {pi:F2}");  // Pi: 3.14
Console.WriteLine($"Date: {DateTime.Now:yyyy-MM-dd}");

// Verbatim strings (@"") preserve backslashes
string path = @"C:\Users\name";

Input & Output

Console.ReadLine() reads a full line as a string. int.Parse converts but throws on invalid input; int.TryParse is safer—it returns bool and uses an out parameter. Always use TryParse for user input to avoid exceptions from bad data.

csharp
Console.Write("Enter name: ");
string name = Console.ReadLine();
Console.Write("Enter age: ");
int age = int.Parse(Console.ReadLine());

Console.WriteLine($"Hi {name}, you are {age}");

// Safe parsing with TryParse
if (int.TryParse(Console.ReadLine(), out int num)) {
    Console.WriteLine($"Valid: {num}");
} else {
    Console.WriteLine("Invalid number");
}

Nullable Types

int? is a nullable value type (Nullable<int>). The ?. operator (null-conditional) safely accesses members—returns null instead of throwing. The ?? operator (null-coalescing) provides a default for null. C# 8+ nullable reference types warn about potential nulls at compile time.

csharp
int? maybeAge = null;  // nullable int
if (maybeAge.HasValue) {
    Console.WriteLine(maybeAge.Value);
}

string? maybeName = null;  // nullable reference (C# 8+)
int length = maybeName?.Length ?? 0;  // null-conditional + null-coalescing

// ValueOrDefault
int age = maybeAge ?? 0;  // 0 if null
02

Strings

String Methods

C# strings are immutable—methods return new strings rather than modifying the original. Common methods: Length, ToUpper/ToLower, Substring, IndexOf, Replace, Contains, Split. For heavy string manipulation, use StringBuilder to avoid creating many intermediate strings.

csharp
string s = "Hello, World!";
Console.WriteLine(s.Length);        // 13
Console.WriteLine(s.ToUpper());     // HELLO, WORLD!
Console.WriteLine(s.Substring(0, 5)); // Hello
Console.WriteLine(s.IndexOf("World")); // 7
Console.WriteLine(s.Replace("World", "C#"));
Console.WriteLine(s.Contains("Hello")); // True
Console.WriteLine(s.Split(", "));   // ["Hello", "World!"]

StringBuilder

StringBuilder is mutable and efficient for building strings in loops. String concatenation (+) creates a new string each time, so repeated concatenation is O(n^2). StringBuilder amortizes to O(n). Use StringBuilder when you have more than a few concatenations.

csharp
using System.Text;
var sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
    sb.Append("Line ").Append(i).Append("\n");
}
string result = sb.ToString();

// vs string concatenation (creates many temporary strings)
// string result = "";
// for (int i = 0; i < 100; i++) result += "Line " + i;

Formatting & Parsing

string.Format uses {0}, {1} placeholders. Format specifiers: F (fixed), N (number with separators), P (percent), X (hex). Parsing is culture-sensitive—decimal separators differ by locale. Use CultureInfo for explicit control, or TryParse for safety.

csharp
// String formatting
string s1 = string.Format("{0} is {1} years old", "Alice", 30);
string s2 = $"{3.14159:F2}";  // "3.14"
string s3 = $"{1234567:N0}";  // "1,234,567" (thousands separator)

// Parsing
int n = int.Parse("42");
double d = double.Parse("3.14");
bool b = bool.Parse("true");

// Culture-aware parsing
double euro = double.Parse("3,14",
    System.Globalization.CultureInfo.GetCultureInfo("de-DE"));

String Comparison

Use StringComparison.OrdinalIgnoreCase for case-insensitive comparison—it's clearer and faster than ToLower() then ==. Avoid == for culture-sensitive comparisons; use string.Compare with a CultureInfo. For dictionary keys, use StringComparer.OrdinalIgnoreCase.

csharp
string a = "Hello", b = "hello";

// Case-sensitive
bool equal = a == b;  // false
bool equal2 = a.Equals(b);  // false

// Case-insensitive
bool equalCI = a.Equals(b, StringComparison.OrdinalIgnoreCase);  // true
int cmp = string.Compare(a, b, StringComparison.OrdinalIgnoreCase);

// Best practice for comparison
bool same = string.Equals(a, b,
    StringComparison.OrdinalIgnoreCase);

Regular Expressions

Regex (from System.Text.RegularExpressions) provides pattern matching. Use @"" verbatim strings so backslashes don't need double-escaping. For repeated use, compile a Regex instance once and reuse it (RegexOptions.Compiled) for better performance.

csharp
using System.Text.RegularExpressions;

string text = "Phone: 123-456-7890";
var match = Regex.Match(text, @"\d{3}-\d{3}-\d{4}");
if (match.Success) Console.WriteLine(match.Value);  // 123-456-7890

// Replace
string clean = Regex.Replace(text, @"\d", "X");
// "Phone: XXX-XXX-XXXX"

// Extract all matches
var emails = Regex.Matches("[email protected], [email protected]", @"\S+@\S+");
03

Numbers & Math

Numeric Types

C# has fixed-size types: int (32-bit), long (64-bit), double (64-bit float), decimal (128-bit, for financial). Underscores (9_000_000) improve readability. Use decimal for money—it avoids floating-point rounding errors. Suffixes: L (long), f (float), m (decimal).

csharp
int i = 42;
long big = 9_000_000_000L;  // underscores for readability
double d = 3.14159265;
float f = 3.14f;
decimal price = 19.99m;     // exact decimal for money
byte b = 255;

Console.WriteLine(sizeof(int));   // 4
Console.WriteLine(int.MaxValue);  // 2147483647

Math Class

Math provides static methods for common operations. Math.Round uses banker's rounding (rounds to even) by default—use MidpointRounding.AwayFromZero for school rounding. Math.BigMul handles long multiplication to avoid overflow.

csharp
double x = 2.5;
Math.Pow(x, 3);     // 15.625
Math.Sqrt(x);       // 1.581
Math.Abs(-5);       // 5
Math.Floor(3.7);    // 3
Math.Ceiling(3.2);  // 4
Math.Round(3.5);    // 4 (banker's rounding)
Math.Max(3, 7);     // 7
Math.Min(3, 7);     // 3
Math.PI;            // 3.14159...

Random Numbers

Random generates pseudo-random numbers. Create one instance and reuse it (creating many in a loop can produce duplicates due to time-based seeding). For cryptographic randomness, use System.Security.Cryptography.RandomNumberGenerator. C# 8+ provides Random.Shared for thread safety.

csharp
var random = new Random();
int r = random.Next(1, 101);     // 1-100
double d = random.NextDouble();   // 0.0-1.0
byte[] bytes = new byte[4];
random.NextBytes(bytes);

// C# 8+: thread-safe
// var rng = Random.Shared;
// int n = rng.Next(1, 101);

// Array of random items
int[] nums = Enumerable.Range(0, 5)
    .Select(_ => random.Next(1, 100)).ToArray();

Type Conversion

Implicit conversions happen automatically when no data is lost (int to double). Explicit casts (type) are needed when precision may be lost. Convert.ToInt32 handles many types and rounds (unlike cast which truncates). Always prefer TryParse for string-to-number conversion to avoid exceptions.

csharp
// Implicit (safe, no data loss)
int i = 42;
double d = i;  // int -> double

// Explicit cast (may lose data)
double pi = 3.14;
int truncated = (int)pi;  // 3

// Convert class
string s = "42";
int n = Convert.ToInt32(s);
string str = Convert.ToString(42);

// Parse / TryParse
int parsed = int.Parse("100");
bool ok = int.TryParse("abc", out int result);  // false

Integer Overflow & Checked

By default, integer overflow wraps silently (unchecked). The checked block throws OverflowException on overflow—use it for safety-critical code. For truly large numbers, BigInteger (System.Numerics) handles arbitrary-precision integers without overflow.

csharp
int a = int.MaxValue;  // 2147483647
// a++;  // overflow: wraps to -2147483648 (unchecked by default)

checked {
    a++;  // throws OverflowException!
}

// BigInteger for arbitrary precision
using System.Numerics;
var big = BigInteger.Pow(2, 100);
Console.WriteLine(big);  // 1267650...376 (31 digits)
04

Control Flow

If / Else

if/else if/else is standard. The ternary operator (condition ? a : b) is a concise if/else expression. C# requires boolean conditions—unlike C, integers are not implicitly converted to bool. Use braces even for single statements to prevent maintenance bugs.

csharp
int score = 85;
if (score >= 90) {
    Console.WriteLine("A");
} else if (score >= 80) {
    Console.WriteLine("B");
} else {
    Console.WriteLine("C");
}

// Ternary operator
string grade = score >= 60 ? "Pass" : "Fail";

Switch & Pattern Matching

C# 8+ switch expressions (=>) are concise and return values. The _ pattern is the default case. Pattern matching (is, switch) supports type patterns, property patterns, and relational patterns (> 18). This is more powerful than traditional switch and reduces boilerplate.

csharp
// Classic switch
int day = 3;
string name = day switch {
    1 => "Mon", 2 => "Tue", 3 => "Wed",
    6 or 7 => "Weekend",
    _ => "Invalid"  // default
};

// Pattern matching with types
object obj = "hello";
if (obj is string s && s.Length > 3) {
    Console.WriteLine(s);  // "hello"
}

// Property patterns
if (person is { Age: > 18, Name: "Alice" }) {
    // matches if Age > 18 and Name == "Alice"
}

Loops

for is for counted iterations; foreach iterates collections (arrays, lists, IEnumerable); while loops until a condition is false. foreach is read-only—you can't modify the collection during iteration. Use for if you need the index or need to modify elements.

csharp
// for loop
for (int i = 0; i < 5; i++) {
    Console.WriteLine(i);
}

// foreach
string[] fruits = { "apple", "banana", "cherry" };
foreach (string fruit in fruits) {
    Console.WriteLine(fruit);
}

// while
int n = 5;
while (n > 0) Console.WriteLine(n--);

Break, Continue & goto

break exits the nearest loop or switch; continue skips to the next iteration. C# has no labeled break for nested loops—use a flag, extract to a method with return, or use LINQ. goto is valid in switch for fall-through but is otherwise discouraged.

csharp
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    Console.WriteLine(i);  // 0 1 2 4 5 6
}

// goto in switch (rare but valid)
switch (code) {
    case 1:
        DoSomething();
        goto case 2;  // fall through
    case 2:
        DoMore();
        break;
}

Iterators & yield

yield return turns a method into an iterator that produces values lazily—one at a time as requested. This is memory-efficient for large or infinite sequences. The compiler generates a state machine. yield break ends iteration early. LINQ uses iterators extensively.

csharp
// yield return creates an iterator (lazy evaluation)
IEnumerable<int> GetNumbers() {
    for (int i = 0; i < 5; i++) {
        yield return i;
    }
}

foreach (int n in GetNumbers()) {
    Console.WriteLine(n);  // 0 1 2 3 4
}

// Infinite sequence (consumed lazily)
IEnumerable<int> Naturals() {
    int n = 0;
    while (true) yield return n++;
}
05

Methods & Delegates

Method Definition

Methods can be expression-bodied (=>) for one-liners. Default parameters make arguments optional. Named arguments (name:) improve readability for calls with many parameters and allow skipping optional ones. C# doesn't support method overloading with just different return types.

csharp
static int Add(int a, int b) {
    return a + b;
}

// Expression-bodied (C# 6+)
static int Multiply(int a, int b) => a * b;

// Default parameters
static void Greet(string name = "Guest", int times = 1) {
    for (int i = 0; i < times; i++)
        Console.WriteLine($"Hi {name}!");
}

Greet();              // Hi Guest!
Greet("Alice", 3);    // Hi Alice! x3
Greet(times: 2);      // named argument

out, ref & params

out parameters must be assigned by the method (caller doesn't need to initialize). ref parameters must be initialized by the caller and can be modified. params allows a variable number of arguments. C# 7+ allows out var declaration inline: TryParse(s, out var result).

csharp
// out: must be assigned in method (caller doesn't init)
bool TryParse(string s, out int result) {
    result = 0;
    return int.TryParse(s, out result);
}

// ref: caller must initialize
void Increment(ref int n) { n++; }
int x = 5;
Increment(ref x);  // x is now 6

// params: variable arguments
int Sum(params int[] nums) => nums.Sum();
Sum(1, 2, 3);  // 6
Sum();         // 0

Delegates & Events

Delegates are type-safe function pointers. Func<T,TResult> takes inputs and returns a value; Action<T> returns void; Predicate<T> returns bool. Events are delegates with +=/-= subscription and ?.Invoke for safe raising (null-check). Use events for the observer pattern (pub/sub).

csharp
// Delegate type
delegate int MathOp(int a, int b);

MathOp add = (a, b) => a + b;
MathOp mul = (a, b) => a * b;
Console.WriteLine(add(3, 4));  // 7

// Built-in delegate types
Func<int, int, int> divide = (a, b) => a / b;
Action<string> log = msg => Console.WriteLine(msg);
Predicate<int> isEven = n => n % 2 == 0;

// Events
event Action<string> OnMessage;
OnMessage += msg => Console.WriteLine($"Got: {msg}");
OnMessage?.Invoke("Hello");

Lambda Expressions

Lambdas are anonymous methods using =>. They capture variables from the enclosing scope (closures). Captured variables are evaluated when the lambda executes, not when it's created—beware of this in loops. Use lambdas extensively with LINQ, events, and callbacks.

csharp
// Lambda with explicit types
Func<int, int> square = (int x) => x * x;

// Type inference
Func<int, int, int> add = (a, b) => a + b;

// Statement lambda
Action<string> log = msg => {
    Console.WriteLine($"[{DateTime.Now}] {msg}");
};

// Capturing variables (closures)
int factor = 3;
Func<int, int> multiply = x => x * factor;
Console.WriteLine(multiply(5));  // 15

Extension Methods

Extension methods add methods to existing types without modifying them. The this keyword before the first parameter marks it as an extension method. They must be in a static class. LINQ is entirely implemented as extension methods on IEnumerable. This is a powerful way to add utility methods.

csharp
static class StringExtensions {
    public static int WordCount(this string s) {
        return s.Split(new[] { ' ' },
            StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

string text = "Hello World C#";
Console.WriteLine(text.WordCount());  // 3

// LINQ is built on extension methods
var evens = nums.Where(n => n % 2 == 0);
06

Collections

List<T>

List<T> is a dynamic array (like vector in C++ or ArrayList in Java). Add/Remove are O(1) amortized / O(n). Count gives the number of elements (not capacity). Find/FindAll use predicates. For frequent insertions/deletions in the middle, LinkedList<T> may be better.

csharp
using System.Collections.Generic;
var nums = new List<int> { 1, 2, 3 };
nums.Add(4);
nums.AddRange(new[] { 5, 6 });
nums.Remove(3);
nums.RemoveAt(0);
Console.WriteLine(nums.Count);  // 4
Console.WriteLine(nums.Contains(4));  // true

// Find
int first = nums.Find(n => n > 3);
List<int> all = nums.FindAll(n => n > 3);

Dictionary<TKey, TValue>

Dictionary is a hash table with O(1) average lookup. TryGetValue is safer than indexer (which throws KeyNotFoundException). For case-insensitive string keys, pass StringComparer.OrdinalIgnoreCase to the constructor. Dictionary does not preserve insertion order (use OrderedDictionary if needed).

csharp
var ages = new Dictionary<string, int> {
    ["Alice"] = 30,
    ["Bob"] = 25
};
ages["Charlie"] = 35;

// Safe access
if (ages.TryGetValue("Alice", out int age)) {
    Console.WriteLine($"Alice: {age}");
}

// Iterate
foreach (var kv in ages) {
    Console.WriteLine($"{kv.Key}: {kv.Value}");
}

// Case-insensitive keys
var dict = new Dictionary<string, int>(
    StringComparer.OrdinalIgnoreCase);

HashSet<T> & SortedSet<T>

HashSet<T> stores unique elements with O(1) lookup—use for deduplication and membership testing. It supports set operations (Union, Intersect, Except). SortedSet<T> (red-black tree) keeps elements sorted with O(log n) operations. Use HashSet when order doesn't matter, SortedSet when it does.

csharp
var set = new HashSet<int> { 1, 2, 3 };
set.Add(2);  // duplicate, ignored
set.Add(4);
Console.WriteLine(set.Count);  // 4
Console.WriteLine(set.Contains(3));  // true

// Set operations
var other = new HashSet<int> { 3, 4, 5 };
set.IntersectWith(other);  // set: {3, 4}
set.UnionWith(other);      // set: {3, 4, 5}

// SortedSet keeps elements sorted
var sorted = new SortedSet<int> { 3, 1, 2 };  // {1, 2, 3}

Queue<T> & Stack<T>

Queue<T> is FIFO (Enqueue/Dequeue)—use for task scheduling, BFS. Stack<T> is LIFO (Push/Pop)—use for undo/redo, DFS, expression evaluation. Both are O(1) for their core operations. For concurrent scenarios, use ConcurrentQueue and ConcurrentStack from System.Collections.Concurrent.

csharp
// Queue: FIFO (first in, first out)
var queue = new Queue<string>();
queue.Enqueue("first");
queue.Enqueue("second");
string next = queue.Dequeue();  // "first"
string peek = queue.Peek();     // "second"

// Stack: LIFO (last in, first out)
var stack = new Stack<int>();
stack.Push(1);
stack.Push(2);
int top = stack.Pop();   // 2
int peek = stack.Peek(); // 1

Arrays

Arrays are fixed-size. Array.Sort sorts in place. Multidimensional arrays ([,]) are rectangular (uniform columns). Jagged arrays ([][]) are arrays of arrays (rows can have different lengths). Use List<T> for dynamic sizing; use arrays for fixed-size, performance-critical data.

csharp
int[] nums = { 1, 2, 3, 4, 5 };
Console.WriteLine(nums.Length);  // 5
Array.Sort(nums);
Array.Reverse(nums);
int found = Array.IndexOf(nums, 3);
int[] copy = new int[5];
Array.Copy(nums, copy, 5);

// Multidimensional
int[,] grid = { { 1, 2 }, { 3, 4 } };
Console.WriteLine(grid[1, 0]);  // 3

// Jagged array (array of arrays)
int[][] jagged = { new[] { 1, 2 }, new[] { 3, 4, 5 } };
07

Classes & OOP

Class & Properties

Auto-properties ({ get; set; }) auto-generate backing fields. Expression-bodied properties (=>) compute on access. C# 9+ init-only properties ({ get; init; }) are settable only during construction, enabling immutability. Object initializers: new Person { Name = "X" }.

csharp
class Person {
    // Auto-properties (C# 3+)
    public string Name { get; set; }
    public int Age { get; set; }

    // Read-only property
    public bool IsAdult => Age >= 18;

    // Constructor
    public Person(string name, int age) {
        Name = name;
        Age = age;
    }
}

var p = new Person("Alice", 30);
Console.WriteLine(p.IsAdult);  // true

Inheritance & Virtual

virtual marks a method for overriding; override in subclasses provides the new implementation. C# requires explicit override (unlike Java). sealed prevents a class from being inherited or a method from being overridden. All classes implicitly inherit from object (System.Object).

csharp
class Animal {
    public virtual void Speak() {
        Console.WriteLine("...");
    }
}

class Dog : Animal {
    public override void Speak() {
        Console.WriteLine("Woof");
    }
}

Animal a = new Dog();
a.Speak();  // Woof (polymorphism)

// sealed prevents further inheritance
sealed class Puppy : Dog { }

Interfaces

Interfaces define contracts without implementation. A class can implement multiple interfaces (unlike single class inheritance). C# 8+ allows default interface methods. Use interfaces for polymorphism and dependency injection. Naming convention: prefix with I (IShape, ILogger).

csharp
interface IShape {
    double Area();  // interface member
    double Perimeter { get; }
}

class Circle : IShape {
    public double Radius { get; set; }
    public double Area() => Math.PI * Radius * Radius;
    public double Perimeter => 2 * Math.PI * Radius;
}

// Default interface methods (C# 8+)
interface ILogger {
    void Log(string msg);
    void Error(string msg) => Log($"ERROR: {msg}");  // default
}

Abstract Classes

Abstract classes cannot be instantiated and can have both abstract (must override) and concrete (virtual) members. Use abstract classes when there's shared implementation; use interfaces for pure contracts. A class can inherit only one abstract class but implement many interfaces.

csharp
abstract class Shape {
    public abstract double Area();  // must be overridden
    public virtual void Describe() {
        Console.WriteLine($"Area: {Area()}");
    }
}

class Rectangle : Shape {
    public double Width { get; set; }
    public double Height { get; set; }
    public override double Area() => Width * Height;
}

// Cannot instantiate abstract class
// var s = new Shape();  // ERROR
var r = new Rectangle { Width = 3, Height = 4 };

Records & Structs

Records (C# 9+) provide value-based equality, immutability, and concise syntax—ideal for DTOs and data models. The with expression creates a copy with modified properties. Structs are value types (copied on assignment, stack-allocated)—use for small, lightweight data. Classes are reference types (heap-allocated).

csharp
// Record (C# 9+): value-based equality, immutability
record Point(int X, int Y);

var p1 = new Point(3, 4);
var p2 = new Point(3, 4);
Console.WriteLine(p1 == p2);  // true (value equality)

// with expression (non-destructive mutation)
var p3 = p1 with { X = 10 };

// Struct: value type (stack-allocated)
struct Vector {
    public double X, Y;
    public Vector(double x, double y) { X = x; Y = y; }
}
08

LINQ

Where & Select

LINQ (Language Integrated Query) provides SQL-like queries on collections. Where filters, Select transforms (map). LINQ is lazy—queries execute only when enumerated (e.g., via ToList()). This enables efficient chaining without intermediate collections. Method syntax (above) is most common; query syntax is also available.

csharp
using System.Linq;
int[] nums = { 1, 2, 3, 4, 5, 6 };

// Filter
var evens = nums.Where(n => n % 2 == 0);  // 2, 4, 6

// Map/Transform
var squares = nums.Select(n => n * n);  // 1, 4, 9, 16, 25, 36

// Chain operations
var result = nums
    .Where(n => n > 2)
    .Select(n => n * 10)
    .ToList();  // 30, 40, 50, 60

Ordering & Grouping

OrderBy/OrderByDescending sort; ThenBy adds secondary sort criteria. GroupBy clusters elements by a key, returning IGrouping<key, element> groups. Each group has a Key and is itself an IEnumerable of its members. This replaces manual loops and dictionaries for grouping.

csharp
var people = new[] {
    new { Name = "Alice", Age = 30 },
    new { Name = "Bob", Age = 25 },
    new { Name = "Charlie", Age = 30 },
};

// Order
var sorted = people.OrderBy(p => p.Age).ThenBy(p => p.Name);
var desc = people.OrderByDescending(p => p.Age);

// Group
var grouped = people.GroupBy(p => p.Age);
foreach (var g in grouped) {
    Console.WriteLine($"Age {g.Key}: {g.Count()} people");
}
// Age 25: 1 people
// Age 30: 2 people

Aggregation

LINQ aggregation methods (Sum, Average, Min, Max, Count) compute single values from collections. Aggregate is the most general—it's a fold/reduce that applies a function cumulatively. These throw on empty sequences; use the *OrDefault variants or check for emptiness first.

csharp
int[] nums = { 1, 2, 3, 4, 5 };

Console.WriteLine(nums.Sum());    // 15
Console.WriteLine(nums.Average()); // 3
Console.WriteLine(nums.Min());    // 1
Console.WriteLine(nums.Max());    // 5
Console.WriteLine(nums.Count());  // 5

// Count with predicate
int evens = nums.Count(n => n % 2 == 0);  // 2

// Aggregate (fold/reduce)
int product = nums.Aggregate((a, b) => a * b);  // 120
string joined = nums.Aggregate("", (s, n) => s + n);  // "12345"

First, Single & ElementAt

First returns the first element (throws if empty); FirstOrDefault returns default (0 for int, null for reference types) if not found. Single requires exactly one element (throws otherwise)—use for validation. Any/All return bool without enumerating the entire collection (short-circuit).

csharp
int[] nums = { 1, 2, 3, 4, 5 };

int first = nums.First();           // 1
int firstEven = nums.First(n => n % 2 == 0);  // 2
int firstOrDef = nums.FirstOrDefault(n => n > 10);  // 0 (default)

int single = new[] { 42 }.Single();  // 42
// nums.Single();  // throws! more than one element

int third = nums.ElementAt(2);  // 3 (0-indexed)

bool any = nums.Any(n => n > 3);  // true
bool all = nums.All(n => n > 0);  // true

Join & Zip

Join performs an inner join (like SQL) matching elements by key. Zip pairs elements from two sequences by position. Both produce new sequences. LINQ also supports GroupJoin (left join with grouping). These are powerful for combining related data from multiple sources.

csharp
var users = new[] {
    new { Id = 1, Name = "Alice" },
    new { Id = 2, Name = "Bob" },
};
var orders = new[] {
    new { UserId = 1, Item = "Book" },
    new { Id = 2, Item = "Pen" },
};

// Join (inner join)
var joined = users.Join(orders,
    u => u.Id, o => o.UserId,
    (u, o) => new { u.Name, o.Item });
// { Alice, Book }

// Zip (pair elements by position)
var names = new[] { "Alice", "Bob" };
var ages = new[] { 30, 25 };
var pairs = names.Zip(ages, (n, a) => $"{n}: {a}");
// "Alice: 30", "Bob: 25"
09

Error Handling

Try / Catch / Finally

Catch specific exceptions first, then the general Exception last (most specific to least). finally always runs—use it for cleanup (closing files, releasing resources). Avoid catch (Exception) for control flow; catch only what you can handle. Use throw; (not throw e;) to preserve the stack trace.

csharp
try {
    int x = 10;
    int y = 0;
    int z = x / y;  // throws DivideByZeroException
} catch (DivideByZeroException e) {
    Console.WriteLine($"Error: {e.Message}");
} catch (Exception e) {
    Console.WriteLine($"Unexpected: {e.Message}");
} finally {
    // always runs, even with return/throw
    Console.WriteLine("Cleanup");
}

Custom Exceptions

Derive custom exceptions from Exception (or a more specific base). Add context fields that help debugging. Always call the base constructor with the message. By convention, exception class names end with Exception. Use custom exceptions for domain-specific error conditions that callers can handle.

csharp
class ValidationError : Exception {
    public string Field { get; }
    public ValidationError(string field, string msg)
        : base(msg) {
        Field = field;
    }
}

throw new ValidationError("email", "Invalid format");

try {
    // ...
} catch (ValidationError e) {
    Console.WriteLine($"{e.Field}: {e.Message}");
}

using & IDisposable

using ensures Dispose() is called even if an exception occurs—this is C#'s RAII equivalent for resource management. The using declaration (C# 8+) is cleaner—Dispose is called at end of scope. Implement IDisposable for classes that hold unmanaged resources (file handles, database connections).

csharp
// using statement (C# 1+)
using (var file = new StreamReader("data.txt")) {
    string content = file.ReadToEnd();
}  // file.Dispose() called automatically

// using declaration (C# 8+)
using var file2 = new StreamReader("data.txt");
string content2 = file2.ReadToEnd();
// Dispose called at end of scope

// Implementing IDisposable
class Resource : IDisposable {
    public void Dispose() {
        // release unmanaged resources
        GC.SuppressFinalize(this);
    }
}

Null Handling

C# 8+ nullable reference types (string?) enable compile-time null safety. ?. returns null instead of throwing NullReferenceException. ?? provides a fallback. The throw expression (?? throw) is concise for validation. Enable <Nullable>enable</Nullable> in .csproj for project-wide null checking.

csharp
string? name = null;

// Null-conditional operator
int? length = name?.Length;  // null (no exception)

// Null-coalescing operator
string display = name ?? "Unknown";  // "Unknown"

// Throw if null
string required = name ?? throw new ArgumentNullException(nameof(name));

// Pattern matching null check
if (name is null) { /* ... */ }
if (name is { Length: > 5 }) { /* not null and length > 5 */ }

Exception Filters

Exception filters (when) add conditions to catch blocks—the exception is only caught if the filter is true. This is more powerful than catching and re-throwing because it preserves the stack trace. The logging pattern (when returns false) lets you observe exceptions without handling them.

csharp
try {
    // ...
} catch (HttpRequestException e) when (e.StatusCode == 404) {
    Console.WriteLine("Not found");
} catch (HttpRequestException e) when (e.StatusCode >= 500) {
    Console.WriteLine("Server error");
} catch (HttpRequestException e) {
    Console.WriteLine($"HTTP error: {e.Message}");
}

// Log without catching
catch (Exception e) when (LogError(e)) {
    // never enters if LogError returns false
}
private bool LogError(Exception e) { /* log */ return false; }
10

File I/O & Async

File Class (Quick I/O)

File provides static methods for quick one-shot file operations—simple but loads the entire file into memory. For large files, use StreamReader/StreamWriter to process line by line. File.Exists checks existence but there's a TOCTOU race—always handle exceptions from file operations.

csharp
using System.IO;

// Read all text at once
string content = File.ReadAllText("input.txt");
string[] lines = File.ReadAllLines("input.txt");

// Write
File.WriteAllText("output.txt", "Hello\n");
File.WriteAllLines("output.txt", new[] { "Line 1", "Line 2" });

// Append
File.AppendAllText("log.txt", $"[{DateTime.Now}] Event\n");

// File info
bool exists = File.Exists("data.txt");
var info = new FileInfo("data.txt");
Console.WriteLine(info.Length);  // bytes

Streams (StreamReader/Writer)

StreamReader/StreamWriter process files line by line without loading everything into memory—use for large files. The using statement ensures the file is closed even if an exception occurs. StreamWriter buffers data; call Flush() to write immediately, or let Dispose handle it.

csharp
using System.IO;

// Reading line by line
using var reader = new StreamReader("input.txt");
string? line;
while ((line = reader.ReadLine()) != null) {
    Console.WriteLine(line);
}

// Writing
using var writer = new StreamWriter("output.txt");
writer.WriteLine("First line");
writer.WriteLine("Second line");
writer.Flush();  // optional, using disposes and flushes

async / await

async/await enables non-blocking asynchronous operations. The method returns Task<T> (or Task for void). await suspends the method without blocking the thread—the thread is freed for other work. This is essential for I/O-bound operations (files, network, database) in UI and web applications.

csharp
async Task<string> ReadFileAsync(string path) {
    using var reader = new StreamReader(path);
    return await reader.ReadToEndAsync();
}

// Calling async method
string content = await ReadFileAsync("data.txt");
Console.WriteLine(content.Length);

// Async doesn't block the calling thread
// The thread is freed while waiting for I/O

Task & Parallel

Task.Run schedules work on the thread pool. Task.WhenAll awaits multiple tasks concurrently (don't use async/await for CPU-bound work—use Task.Run). Parallel.For is for CPU-bound parallel loops. PLINQ (AsParallel) parallelizes LINQ queries. Use async for I/O, Parallel/PLINQ for CPU.

csharp
// Run work on a thread pool thread
var task = Task.Run(() => HeavyComputation());
int result = await task;

// Run multiple tasks concurrently
var t1 = Task.Run(() => DownloadAsync("url1"));
var t2 = Task.Run(() => DownloadAsync("url2"));
await Task.WhenAll(t1, t2);  // wait for both

// Parallel.For for CPU-bound parallelism
Parallel.For(0, 100, i => {
    Process(i);  // runs on multiple threads
});

// PLINQ
var results = nums.AsParallel()
    .Where(n => IsPrime(n))
    .ToArray();

JSON Serialization

System.Text.Json is the modern, high-performance JSON serializer (replaces Newtonsoft.Json for most cases). Serialize/Deserialize handle object<->JSON. Use JsonSerializerOptions for formatting (indented, camelCase). For async, use the stream-based methods to avoid loading large JSON into memory.

csharp
using System.Text.Json;

var person = new { Name = "Alice", Age = 30 };

// Serialize
string json = JsonSerializer.Serialize(person);
// {"Name":"Alice","Age":30}

// Deserialize
Person p = JsonSerializer.Deserialize<Person>(json);

// With options
var options = new JsonSerializerOptions {
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string pretty = JsonSerializer.Serialize(person, options);

// Async (for streams)
using var stream = File.OpenRead("data.json");
var data = await JsonSerializer.DeserializeAsync<Person>(stream);
11

LINQ Deep Dive

Query vs Method Syntax

LINQ has two equivalent syntaxes. Query syntax is SQL-like and more readable for complex joins/grouping. Method syntax (fluent) is more common, supports all operators, and chains naturally. They compile to the same IL. Use query syntax for complex multi-clause queries; method syntax for simple chains. Both are lazy-evaluated (deferred execution).

csharp
List<int> nums = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Query syntax (SQL-like)
var evens = from n in nums
            where n % 2 == 0
            orderby n descending
            select n * 2;

// Method syntax (fluent) — equivalent
var evens2 = nums.Where(n => n % 2 == 0)
                  .OrderByDescending(n => n)
                  .Select(n => n * 2);

// Query syntax supports: from, where, orderby, select, group, join, let
// Method syntax supports everything query does, plus more

Deferred vs Immediate Execution

LINQ uses deferred execution—Where/Select/OrderBy just build a query; it runs when enumerated. This means results reflect the source's state at enumeration time, not at query creation. ToList/ToArray/Count/etc. force immediate execution, capturing a snapshot. Be careful: enumerating a deferred query twice runs it twice. Cache with ToList if you need stable results.

csharp
var nums = new List<int> { 1, 2, 3 };

// Deferred: query not run until enumerated
var query = nums.Where(n => n > 1);
nums.Add(4);  // query sees this!
Console.WriteLine(query.Count());  // 3 (2, 3, 4)

// Immediate: forces execution now
var list = nums.Where(n => n > 1).ToList();  // runs now
nums.Add(5);
Console.WriteLine(list.Count);  // still 3 (snapshot)

// Operators that force immediate execution:
// ToList, ToArray, ToDictionary, ToLookup,
// Count, Sum, Min, Max, Average, First, Last, Any, All

Grouping and Joining

GroupBy clusters elements by a key, returning IGrouping<key, element> sequences. Join performs inner joins (matching keys). Group join (join...into) creates hierarchical results and enables left outer joins via DefaultIfEmpty(). These operations are powerful for data analysis. For large datasets, consider using ToLookup for repeated lookups (it's a pre-computed grouping).

csharp
var students = new[] {
    new { Name = "Alice", Grade = "A", Dept = "CS" },
    new { Name = "Bob", Grade = "B", Dept = "Math" },
    new { Name = "Carol", Grade = "A", Dept = "CS" },
};

// Group by department
var byDept = students.GroupBy(s => s.Dept);
foreach (var g in byDept) {
    Console.WriteLine(`{g.Key}: {g.Count()}`);
    // CS: 2, Math: 1
}

// Inner join
var courses = new[] { new { Dept = "CS", Course = "Algo" } };
var joined = from s in students
             join c in courses on s.Dept equals c.Dept
             select new { s.Name, c.Course };

// Group join (left outer join with DefaultIfEmpty)
var groupJoin = from s in students
                join c in courses on s.Dept equals c.Dept into sc
                from c in sc.DefaultIfEmpty()
                select new { s.Name, Course = c?.Course };

Aggregation and Quantifiers

LINQ provides standard aggregations (Sum, Min, Max, Average, Count) and a generic Aggregate for custom reductions (like JavaScript reduce). Quantifiers (Any, All, Contains) return booleans and short-circuit—Any stops at the first match, All stops at the first non-match. Use Any() (not Count() > 0) to check for existence—it's more efficient and readable.

csharp
var nums = new[] { 1, 2, 3, 4, 5 };

// Aggregations
int sum = nums.Sum();              // 15
double avg = nums.Average();       // 3.0
int min = nums.Min();              // 1
int max = nums.Max();              // 5
int count = nums.Count();          // 5

// Custom aggregation (like reduce/fold)
int product = nums.Aggregate((a, b) => a * b);  // 120
int withSeed = nums.Aggregate(10, (a, b) => a + b);  // 25

// Quantifiers (return bool)
bool anyEven = nums.Any(n => n % 2 == 0);  // true
bool allPos = nums.All(n => n > 0);        // true
bool contains3 = nums.Contains(3);         // true

// LongCount for > int.MaxValue elements
long big = largeList.LongCount();

IEnumerable and IQueryable

IEnumerable<T> is for in-memory collections (LINQ to Objects)—uses delegates, executes locally. IQueryable<T> is for remote sources (Entity Framework, LINQ to SQL)—uses expression trees, translates to the source's query language (SQL). IQueryable composes server-side queries efficiently. Switch to IEnumerable with AsEnumerable() when you need client-side logic that can't be translated.

csharp
// IEnumerable<T>: in-memory, LINQ to Objects
List<int> nums = new() { 1, 2, 3 };
var q1 = nums.Where(n => n > 1);  // executes in memory

// IQueryable<T>: translates to query language (SQL)
// Expression trees, not delegates
using var db = new MyDbContext();
var q2 = db.Users.Where(u => u.Age > 18);  // generates SQL
// SELECT * FROM Users WHERE Age > 18

// IQueryable composes queries that execute server-side
var query = db.Users
    .Where(u => u.Active)
    .OrderBy(u => u.Name)
    .Take(10);
// All translated to one SQL query

// Force client-side with AsEnumerable()
var clientSide = db.Users.AsEnumerable()
    .Where(u => ExpensiveCheck(u));
12

Async/Await Deep Dive

Task and Task<T>

Task represents an async operation without a result; Task<T> returns T. ValueTask<T> (C# 7) avoids heap allocation when the result is often available synchronously (caching scenarios)—but can only be awaited once. Use Task.FromResult for synchronous results in async APIs. Prefer Task over void for async methods (void is only for event handlers).

csharp
// Task: represents an async operation (no result)
async Task DoWorkAsync() {
    await Task.Delay(1000);
    Console.WriteLine("Done");
}

// Task<T>: async operation returning T
async Task<int> GetCountAsync() {
    await Task.Delay(500);
    return 42;
}

// ValueTask<T>: lightweight, avoids allocation
ValueTask<int> GetCachedAsync() {
    if (_cache != null) return new ValueTask<int>(_cache);
    return new ValueTask<int>(LoadFromDbAsync());
}

// Converting to Task
Task t = Task.Run(() => Console.WriteLine("hi"));
Task<int> t2 = Task.FromResult(42);  // already complete
Task completed = Task.CompletedTask;

CancellationToken

CancellationToken enables cooperative cancellation. Pass it to async methods and check ThrowIfCancellationRequested() in loops. The caller cancels via CancellationTokenSource.Cancel(); the method decides when/how to respond. Always accept a CancellationToken parameter in async APIs (especially library code). Use CancellationTokenSource with a TimeSpan for timeouts. Cancellation throws OperationCanceledException.

csharp
async Task DownloadAsync(string url, CancellationToken ct) {
    using var client = new HttpClient();
    // Pass token to async operations
    var data = await client.GetByteArrayAsync(url, ct);
    // Check manually in loops
    foreach (var chunk in data) {
        ct.ThrowIfCancellationRequested();
        Process(chunk);
    }
}

// Usage with timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try {
    await DownloadAsync("https://example.com", cts.Token);
} catch (OperationCanceledException) {
    Console.WriteLine("Timed out or cancelled");
}

// Cooperative cancellation: caller cancels, method checks
cts.Cancel();  // request cancellation

Task.WhenAll vs WhenAny

Task.WhenAll awaits multiple tasks concurrently and continues when all complete (parallelism). Task.WhenAny continues when the first task completes (racing/redundancy). WhenAll throws only the first exception by default; inspect each task's IsFaulted/Exception to see all failures. For fire-and-forget, use Task.Run and handle exceptions inside to avoid unobserved exceptions.

csharp
// WhenAll: wait for ALL to complete (like Promise.all)
var tasks = new[] {
    FetchAsync("url1"),
    FetchAsync("url2"),
    FetchAsync("url3"),
};
string[] results = await Task.WhenAll(tasks);
// Continues when all 3 done; throws AggregateException if any fails

// WhenAny: wait for FIRST to complete (race)
var racing = new[] { PrimaryAsync(), FallbackAsync() };
Task<string> firstDone = await Task.WhenAny(racing);
string result = await firstDone;

// WhenAll with exception handling
try {
    await Task.WhenAll(tasks);
} catch (Exception ex) {
    // Only first exception is thrown; access all via Task.Exception
    foreach (var inner in tasks.Where(t => t.IsFaulted))
        Console.WriteLine(inner.Exception);
}

Async Streams (IAsyncEnumerable)

IAsyncEnumerable<T> (C# 8) is an async stream—yields items as they become available, with awaits between yields. Consumed with await foreach. Perfect for streaming large datasets or real-time data without buffering everything in memory. The [EnumeratorCancellation] attribute ensures the cancellation token flows correctly when the stream is consumed with WithCancellation.

csharp
// C# 8: async iterator (like async generator)
async IAsyncEnumerable<int> GenerateAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    for (int i = 0; i < 10; i++) {
        await Task.Delay(100, ct);
        yield return i;  // async yield
    }
}

// Consume with await foreach
await foreach (int n in GenerateAsync().WithCancellation(ct)) {
    Console.WriteLine(n);
}

// Use case: streaming data from a database or API
async IAsyncEnumerable<Record> StreamRecordsAsync() {
    await using var reader = await cmd.ExecuteReaderAsync();
    while (await reader.ReadAsync())
        yield return MapRecord(reader);
}

Async Pitfalls (Deadlocks)

The #1 async pitfall: calling .Result or .Wait() on a Task can deadlock when a SynchronizationContext is present (UI apps, legacy ASP.NET). The await captures the context; .Result blocks the thread that the continuation needs. Fix: make methods async all the way up, or use ConfigureAwait(false) in library code. async void is dangerous—exceptions can't be caught and it's not awaitable. Use async Task instead.

csharp
// BAD: .Result blocks (can deadlock in UI/ASP.NET classic)
public string BadMethod() {
    string data = GetDataAsync().Result;  // DEADLOCK risk!
    return data;
}

// GOOD: async all the way
public async Task<string> GoodMethodAsync() {
    string data = await GetDataAsync();
    return data;
}

// Why deadlock: in UI/ASP.NET context, await captures
// SynchronizationContext. .Result blocks the thread,
// but the continuation needs that same thread → deadlock.

// Fix if you MUST block (last resort):
string data = Task.Run(GetDataAsync).Result;  // offloads
// Or ConfigureAwait(false) in library code:
await GetDataAsync().ConfigureAwait(false);

// async void is EVIL (except event handlers)
async void Bad() { await Task.Delay(1); throw new Exception(); }
// Exception can't be caught by caller!
13

Events & Delegates

Delegate Basics

Delegates are type-safe function pointers. Custom delegates (delegate int MathOp(int,int)) are largely replaced by built-in Action (void return) and Func<T> (returns T). Delegates are multicast—they can hold multiple methods (combined with +). When invoked, all methods run. The return value of a multicast delegate is the last method's result. Use Action/Func for most modern code.

csharp
// Declare a delegate type
public delegate int MathOp(int a, int b);

// Methods matching the signature
static int Add(int a, int b) => a + b;
static int Mul(int a, int b) => a * b;

MathOp op = Add;
Console.WriteLine(op(2, 3));  // 5
op = Mul;
Console.WriteLine(op(2, 3));  // 6

// Multicast: combine delegates
MathOp multi = (MathOp)Add + Mul;
// Note: returns last result (Add's), but both run
// (return value of multicast is the last one's)

// Built-in: Action (void), Func<T> (returns T)
Action<string> log = s => Console.WriteLine(s);
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = n => n % 2 == 0;  // returns bool

Events (Publisher/Subscriber)

Events are a restricted form of delegates—only the declaring class can invoke them, but external code can subscribe/unsubscribe (+=/-=). Use EventHandler<TArgs> for the standard pattern. Always use the null-conditional operator (Click?.Invoke) since an event with no subscribers is null. Always unsubscribe to prevent memory leaks (the event holds a strong reference to the handler).

csharp
public class Button {
    // Event: restricted multicast delegate
    public event EventHandler<EventArgs> Click;

    public void SimulateClick() {
        // Null-conditional invoke pattern (thread-safe)
        Click?.Invoke(this, EventArgs.Empty);
    }
}

// Custom event args
public class SearchEventArgs : EventArgs {
    public string Query { get; init; }
}

// Subscribe
var btn = new Button();
btn.Click += (sender, e) => Console.WriteLine("Clicked!");
btn.Click += HandlerMethod;  // method group

// Unsubscribe (MUST to avoid memory leaks)
btn.Click -= HandlerMethod;

void HandlerMethod(object sender, EventArgs e) { /* ... */ }

INotifyPropertyChanged (Data Binding)

INotifyPropertyChanged is the standard pattern for data binding in WPF, MAUI, and WinForms. When a property changes, raise PropertyChanged so bound UI updates. [CallerMemberName] (C# 5) automatically passes the calling property's name—no magic strings. The check (if != value) prevents unnecessary notifications. ViewModels use this heavily; source generators (CommunityToolkit.Mvvm) can auto-generate this boilerplate.

csharp
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class ViewModel : INotifyPropertyChanged {
    private string _name;
    public string Name {
        get => _name;
        set {
            if (_name != value) {
                _name = value;
                OnPropertyChanged();  // raises event
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    // CallerMemberName auto-fills the property name
    protected void OnPropertyChanged([CallerMemberName] string prop = null) {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
    }
}

// WPF/MAUI bindings automatically update UI when Name changes

Event vs Delegate

The key difference: a public delegate field can be invoked, reassigned, or cleared by anyone. An event restricts external access to only += (subscribe) and -= (unsubscribe)—only the declaring class can invoke or clear it. This encapsulation is why events are the standard for the observer pattern. Events also generate thread-safe add/remove accessors and integrate with designer tooling.

csharp
// Public delegate: anyone can invoke or clear
public delegate void Notify();
public class Bad {
    public Notify OnDone;  // field-like, public
}
// External code can do:
//   bad.OnDone();        // invoke (BAD: not the owner)
//   bad.OnDone = null;   // clear all subscribers (BAD)

// Event: only owner can invoke/clear
public class Good {
    public event Notify OnDone;  // restricted
    public void Complete() {
        OnDone?.Invoke();  // OK: owner invokes
    }
}
// External code can ONLY += or -= (subscribe/unsubscribe)
// Cannot invoke or clear from outside

// Event = delegate + access restriction
// Under the hood: event generates add/remove accessors

Weak Events (Memory Leak Prevention)

Events hold strong references to subscribers, causing memory leaks if the subscriber should be garbage collected but the publisher lives on. Solutions: explicitly unsubscribe (best), use WeakEventManager (WPF), or implement a weak event pattern with WeakReference. This is a common source of leaks in long-running apps (servers, desktop apps). Always unsubscribe in Dispose or when the subscriber's lifetime ends.

csharp
// Problem: event holds strong ref to subscriber → leak
// if subscriber should be GC'd but publisher lives long

// Solution 1: WeakEventManager (WPF)
WeakEventManager<Publisher, EventArgs>.AddHandler(pub, "Event", Handler);

// Solution 2: WeakReference in custom pattern
public class WeakEvent<TArgs> where TArgs : EventArgs {
    private List<WeakReference<EventHandler<TArgs>>> _handlers = new();
    public void Subscribe(EventHandler<TArgs> h) =>
        _handlers.Add(new WeakReference<EventHandler<TArgs>>(h));
    public void Raise(object sender, TArgs args) {
        _handlers.RemoveAll(wr => !wr.TryGetTarget(out _));
        foreach (var wr in _handlers) {
            if (wr.TryGetTarget(out var h)) h(sender, args);
        }
    }
}

// Best practice: unsubscribe (-=) when done, or use
// IDisposable pattern with weak references for long-lived publishers
14

Reflection

Type Information

Reflection inspects type metadata at runtime. typeof(T) gets a Type at compile time; obj.GetType() gets it at runtime. Type provides Name, IsClass, IsValueType, and methods to enumerate members (GetMethods, GetProperties, GetFields). BindingFlags control what's returned (NonPublic, Instance, Static, Public). Reflection is powerful but slow—cache results when possible.

csharp
using System;

Type t = typeof(string);
// Or: Type t = "hello".GetType();

Console.WriteLine(t.Name);          // String
Console.WriteLine(t.FullName);      // System.String
Console.WriteLine(t.IsClass);       // true
Console.WriteLine(t.IsValueType);   // false
Console.WriteLine(t.IsAbstract);    // false

// Get methods
foreach (var m in t.GetMethods())
    Console.WriteLine(m.Name);

// Get properties
var props = t.GetProperties();

// Get fields
var fields = t.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

// Check if assignable
Type ienum = typeof(IEnumerable<int>);
Console.WriteLine(ienum.IsAssignableFrom(t));  // false

Instantiating and Invoking

Activator.CreateInstance creates objects dynamically. MethodInfo.Invoke calls methods via reflection (slow due to argument boxing and security checks). For repeated calls, CreateDelegate is much faster—creates a strongly-typed delegate that bypasses reflection overhead. Reflection breaks compile-time type safety and is slower than direct calls; use it for frameworks (serialization, DI, ORMs) and caching scenarios.

csharp
using System.Reflection;

// Create instance
Type t = typeof(StringBuilder);
object obj = Activator.CreateInstance(t);
// Or with args: Activator.CreateInstance(t, "initial")

// Invoke method
MethodInfo method = t.GetMethod("Append", new[] { typeof(string) });
object result = method.Invoke(obj, new object[] { "hello" });

// Get/set property
PropertyInfo prop = t.GetProperty("Length");
int len = (int)prop.GetValue(obj);

// Get/set field
FieldInfo field = t.GetField("_field", BindingFlags.NonPublic | BindingFlags.Instance);
field.SetValue(obj, 42);

// Create delegate from MethodInfo (faster than Invoke)
var appendDel = (Func<StringBuilder, string, StringBuilder>)
    method.CreateDelegate(typeof(Func<StringBuilder, string, StringBuilder>));

Attributes

Attributes attach metadata to code elements (classes, methods, properties). Define with [AttributeUsage(...)] specifying valid targets. Retrieve via GetCustomAttributes<T>() (generic, modern) or Attribute.GetCustomAttribute. Built-in attributes: [Obsolete], [Serializable], [DllImport], [Conditional]. Frameworks use attributes extensively: [HttpGet] (ASP.NET), [JsonProperty] (JSON), [Table] (EF Core).

csharp
// Define an attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class AuthorAttribute : Attribute {
    public string Name { get; }
    public AuthorAttribute(string name) => Name = name;
}

// Apply
[Author("Alice")]
[Author("Bob")]
public class Widget {
    [Author("Charlie")]
    public void DoWork() { }
}

// Read via reflection
var attrs = typeof(Widget).GetCustomAttributes<AuthorAttribute>();
foreach (var a in attrs) Console.WriteLine(a.Name);  // Alice, Bob

// Check if attribute is applied
bool hasAuthor = Attribute.IsDefined(typeof(Widget), typeof(AuthorAttribute));

Emit and Dynamic Code Generation

Reflection.Emit generates IL at runtime for maximum performance in dynamic scenarios (expression compilation, serializers, mock frameworks). You build types/methods and emit IL opcodes directly. This is advanced—use expression trees (Expression.Compile) for simpler dynamic code. Emit is used by DLR (dynamic languages), ORMs, and serialization libraries to generate fast typed code at runtime.

csharp
using System.Reflection;
using System.Reflection.Emit;

// Create a dynamic assembly/module/type
var asmName = new AssemblyName("Dynamic");
var asm = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
var mod = asm.DefineDynamicModule("Main");
var type = mod.DefineType("Calculator", TypeAttributes.Public);

// Add a method
var method = type.DefineMethod("Add",
    MethodAttributes.Public | MethodAttributes.Static,
    typeof(int), new[] { typeof(int), typeof(int) });

// Emit IL
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);  // load first arg
il.Emit(OpCodes.Ldarg_1);  // load second arg
il.Emit(OpCodes.Add);      // add
il.Emit(OpCodes.Ret);      // return

Type created = type.CreateType();
int result = (int)created.GetMethod("Add").Invoke(null, new object[] { 3, 4 });  // 7

Expression Trees

Expression trees represent code as data (a tree of Expression nodes). They're the foundation of IQueryable (LINQ providers translate them to SQL, etc.). Compile() turns an expression into a runnable delegate. You can build expressions manually for dynamic code generation (more readable than Reflection.Emit). Expression trees enable ORMs, rule engines, and dynamic LINQ.

csharp
using System.Linq.Expressions;

// Expression<Func<T>>: code as data (tree), not executable
Expression<Func<int, int, int>> expr = (a, b) => a + b + 1;

// Inspect the tree
Console.WriteLine(expr.Body.NodeType);  // Add
Console.WriteLine(expr.Body.Type);      // int

// Compile to a delegate (fast, runs in memory)
Func<int, int, int> compiled = expr.Compile();
int result = compiled(2, 3);  // 6

// EF Core translates expressions to SQL
Expression<Func<User, bool>> filter = u => u.Age > 18;
// → WHERE Age > 18

// Build expressions manually (advanced)
ParameterExpression x = Expression.Parameter(typeof(int), "x");
Expression body = Expression.Multiply(x, Expression.Constant(2));
var lambda = Expression.Lambda<Func<int, int>>(body, x);
Func<int, int> doubler = lambda.Compile();
15

Extension Methods & Patterns

Extension Methods

Extension methods let you 'add' methods to existing types without modifying them. Defined in a static class with 'this' on the first parameter. They're syntactic sugar—the compiler rewrites email.IsEmail() to StringExtensions.IsEmail(email). LINQ is entirely extension methods on IEnumerable. Use extensions to add utility methods to sealed classes, interfaces, or third-party types. Bring them into scope with using.

csharp
public static class StringExtensions {
    // Must be static, in static class, first param with 'this'
    public static bool IsEmail(this string s) =>
        s.Contains('@') && s.Contains('.');

    public static string Repeat(this string s, int n) =>
        string.Concat(Enumerable.Repeat(s, n));

    // Chainable
    public static string Truncate(this string s, int max) =>
        s.Length <= max ? s : s[..max];
}

// Usage: appears as instance method
string email = "[email protected]";
bool valid = email.IsEmail();
string repeated = "ab".Repeat(3);  // "ababab"
string short_ = "hello world".Truncate(5);  // "hello"

// LINQ is built on extension methods (Enumerable class)
var evens = nums.Where(n => n % 2 == 0);  // extension

Fluent Interfaces

Fluent interfaces return 'this' to enable method chaining, producing readable code that reads like a sentence. Common in builders (QueryBuilder), configuration (ASP.NET, EF), and DSLs. Extension methods can add fluent methods to primitive types (5.Seconds()). Keep methods pure (return new instance) for immutable fluent APIs, or mutate and return this for builders.

csharp
public class QueryBuilder {
    private string _table = "";
    private string _where = "";
    private int _limit;

    public QueryBuilder From(string table) { _table = table; return this; }
    public QueryBuilder Where(string clause) { _where = clause; return this; }
    public QueryBuilder Limit(int n) { _limit = n; return this; }

    public string Build() =>
        $"SELECT * FROM {_table} WHERE {_where} LIMIT {_limit}";
}

// Fluent chaining
var sql = new QueryBuilder()
    .From("users")
    .Where("age > 18")
    .Limit(10)
    .Build();

// Extension methods enable fluent APIs on existing types
public static class IntExtensions {
    public static TimeSpan Seconds(this int n) => TimeSpan.FromSeconds(n);
    public static TimeSpan Minutes(this int n) => TimeSpan.FromMinutes(n);
}
var delay = 5.Seconds();  // TimeSpan
var timeout = 2.Minutes();

Iterator Methods (yield)

yield return turns a method into an iterator—the compiler generates a state machine. Execution is lazy: code runs only as items are pulled. This enables infinite sequences and efficient streaming (process one item at a time without buffering). yield break ends iteration. The method must return IEnumerable<T> or IEnumerator<T>. LINQ operators are built on yield. Re-enumerating re-runs the method.

csharp
// yield return: lazy iterator (state machine generated)
public static IEnumerable<int> Evens(int max) {
    for (int i = 0; i <= max; i += 2)
        yield return i;
}

// Lazy: nothing runs until enumerated
var evens = Evens(100);  // no execution yet
foreach (var e in evens) { /* runs here */ }

// yield break: end iteration
public static IEnumerable<T> TakeWhile<T>(IEnumerable<T> src, Func<T, bool> pred) {
    foreach (var item in src) {
        if (!pred(item)) yield break;
        yield return item;
    }
}

// Infinite sequence (lazy, safe)
public static IEnumerable<int> Naturals() {
    int n = 0;
    while (true) yield return n++;
}
var first10 = Naturals().Take(10);  // 0..9

Dependency Injection

Dependency Injection (DI) passes dependencies via constructors rather than creating them internally. Benefits: testability (swap implementations in tests), loose coupling, single responsibility. .NET's built-in DI container (Microsoft.Extensions.DependencyInjection) handles registration and lifetime (AddTransient, AddScoped, AddSingleton). Always depend on interfaces, not concrete classes, for flexibility.

csharp
// Interface for the dependency
public interface IEmailSender {
    Task SendAsync(string to, string body);
}

// Implementation
public class SmtpEmailSender : IEmailSender {
    public async Task SendAsync(string to, string body) { /* SMTP */ }
}

// Consumer: depends on abstraction, not concretion
public class UserService {
    private readonly IEmailSender _email;
    public UserService(IEmailSender email) => _email = email;  // inject

    public async Task RegisterAsync(string email) {
        // ... save user ...
        await _email.SendAsync(email, "Welcome!");
    }
}

// Register in DI container (ASP.NET Core)
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<UserService>();

// Resolve
var userService = app.Services.GetRequiredService<UserService>();

Factory and Builder Patterns

Factory pattern centralizes object creation, hiding concrete classes from callers (useful when creation is complex or type is chosen at runtime). Builder pattern constructs complex objects step-by-step with fluent methods, avoiding 'telescoping constructors' (many parameters). Both improve readability and maintainability. C# records with 'with' expressions often replace builders for immutable data.

csharp
// Factory: create objects without specifying exact class
public interface IShape { void Draw(); }
public class Circle : IShape { public void Draw() => Console.WriteLine("○"); }
public class Square : IShape { public void Draw() => Console.WriteLine("□"); }

public static class ShapeFactory {
    public static IShape Create(string type) => type switch {
        "circle" => new Circle(),
        "square" => new Square(),
        _ => throw new ArgumentException()
    };
}

// Builder: step-by-step construction of complex objects
public class Pizza {
    public string Size { get; set; }
    public bool Cheese { get; set; }
    public bool Pepperoni { get; set; }
}
public class PizzaBuilder {
    private readonly Pizza _p = new();
    public PizzaBuilder WithSize(string s) { _p.Size = s; return this; }
    public PizzaBuilder AddCheese() { _p.Cheese = true; return this; }
    public PizzaBuilder AddPepperoni() { _p.Pepperoni = true; return this; }
    public Pizza Build() => _p;
}

var pizza = new PizzaBuilder()
    .WithSize("large").AddCheese().AddPepperoni().Build();
16

Pattern Matching (C# 7-10+)

Type Patterns and is

Pattern matching (C# 7+) combines type checking and variable binding in one step: 'o is string s' checks and assigns. Property patterns ({ Length: > 3 }) match object properties. C# 9 adds 'not', 'and', 'or' combinators. Switch expressions (C# 8) return values and use patterns—much cleaner than if-else chains. The _ is the discard pattern (default case).

csharp
object o = "hello";

// Type pattern (C# 7)
if (o is string s) {
    Console.WriteLine(s.Length);  // s is string here
}

// Combined with null check
if (o is string { Length: > 3 } longStr) {
    Console.WriteLine(longStr);  // string with length > 3
}

// Negation and conjunction (C# 9)
if (o is not null) { /* ... */ }
if (o is string and not null) { /* ... */ }

// Switch on type
string Describe(object o) => o switch {
    int i => $"Integer: {i}",
    string s => $"String: {s}",
    double d => $"Double: {d}",
    null => "null",
    _ => "Unknown"
};

Property and Positional Patterns

Property patterns match object properties: { Prop: pattern }. Positional patterns deconstruct via the Deconstruct method (records have this automatically). Patterns compose: you can nest them and combine with relational operators (<, >, etc.). C# 10 allows nested property shorthand (Name.Length instead of Name: { Length: }). Pattern matching makes complex conditional logic declarative and readable.

csharp
public record Point(int X, int Y);
public record Person(string Name, int Age);

// Property pattern
string Describe(Person p) => p switch {
    { Age: < 18 } => "Minor",
    { Age: >= 65 } => "Senior",
    { Name.Length: > 10 } => "Long name",
    { } => "Adult"  // non-null Person
};

// Positional pattern (deconstruction)
string Quadrant(Point p) => p switch {
    (0, 0) => "Origin",
    ( > 0, > 0) => "Q1",
    ( < 0, > 0) => "Q2",
    ( < 0, < 0) => "Q3",
    ( > 0, < 0) => "Q4",
    _ => "On axis"
};

// Nested patterns
bool IsValid(Person p) => p is { Name: { Length: > 0 }, Age: > 0 };
// C# 10: nested property shorthand
bool IsValid2(Person p) => p is { Name.Length: > 0, Age: > 0 };

Relational and Logical Patterns (C# 9)

C# 9 introduced relational patterns (<, >, <=, >=) and logical combinators (and, or, not) for patterns. These make switch expressions expressive for range checks and category logic. 'or' combines patterns (matches if either); 'and' requires both; 'not' negates. Parentheses group for precedence. This replaces verbose if-else chains with declarative, exhaustive (compiler-checked) logic.

csharp
// Relational patterns: <, >, <=, >=
string Classify(int n) => n switch {
    < 0 => "Negative",
    0 => "Zero",
    > 0 and < 10 => "Small positive",
    >= 10 and < 100 => "Medium",
    >= 100 => "Large"
};

// Logical patterns: and, or, not
bool IsWeekend(DayOfWeek d) => d is DayOfWeek.Saturday or DayOfWeek.Sunday;

// Combined with type patterns
decimal GetDiscount(object customer) => customer switch {
    Student => 0.2m,
    Senior or Veteran => 0.15m,
    not null => 0.0m,
    null => throw new ArgumentNullException()
};

// Parenthesized patterns for grouping
bool IsValid(int n) => n is (> 0 and < 10) or (> 100 and < 200);

List Patterns (C# 11)

List patterns (C# 11) match the shape of arrays and lists: empty [], single element [x], exact [a, b], or with slices [first, .., last]. The slice pattern (..) captures zero or more middle elements into a variable. Combined with var and guards (when), this enables expressive matching on sequences. Useful for parsing, validation, and algorithm implementation.

csharp
// List patterns match array/list shapes
int[] nums = { 1, 2, 3, 4, 5 };

string Describe(int[] arr) => arr switch {
    [] => "Empty",
    [single] => $"Single: {single}",
    [first, second] => $"Pair: {first}, {second}",
    [first, .., last] => $"First: {first}, Last: {last}",  // slice
    [.., var last] => $"Last: {last}",
    _ => "Other"
};

// Slice pattern (..) captures middle elements
if (nums is [var first, .. var middle, var last]) {
    Console.WriteLine(`First {first}, middle count {middle.Length}, last {last}`);
}

// With guards
string Classify(int[] arr) => arr switch {
    [var a, var b] when a == b => "Equal pair",
    [var a, var b] => "Unequal pair",
    _ => "Other"
};

Switch Expressions

Switch expressions (C# 8) are expressions that return values, unlike switch statements. They use pattern matching (=>) and are more concise. Tuple switching ((a, b) switch) handles multiple values. The compiler checks exhaustiveness for enums (warns if a case is missing). Always include a default (_) unless you want a runtime exception. Switch expressions are preferred over statements for value-returning logic.

csharp
// Traditional switch statement (verbose)
string Old(int n) {
    switch (n) {
        case 1: return "one";
        case 2: return "two";
        default: return "many";
    }
}

// Switch expression (C# 8): expression, returns value
string New(int n) => n switch {
    1 => "one",
    2 => "two",
    _ => "many"
};

// With multiple values per arm
string Describe(int day, bool holiday) => (day, holiday) switch {
    (6 or 7, _) => "Weekend",
    (_, true) => "Holiday",
    (1, false) => "Monday",
    _ => "Weekday"
};

// Exhaustiveness: compiler warns if not all cases covered
// (especially with enums)
17

Records & Immutability

Record Basics (C# 9)

Records (C# 9) are reference types with value-based equality—two records are equal if their data is equal (unlike classes, which use reference equality). The compiler generates Equals, GetHashCode, ToString, and Deconstruct automatically. The 'with' expression creates a copy with modified properties (non-destructive mutation). Records are ideal for DTOs, value objects, and immutable data models.

csharp
// Record: value-equality, immutable, concise
public record Person(string Name, int Age);

// Create
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);

// Value equality (auto-generated)
Console.WriteLine(p1 == p2);  // true (compares properties)
Console.WriteLine(p1.Equals(p2));  // true

// ToString (auto-generated, readable)
Console.WriteLine(p1);  // Person { Name = Alice, Age = 30 }

// Deconstruction
var (name, age) = p1;

// Non-destructive mutation (with-expression)
var older = p1 with { Age = 31 };
// p1 is unchanged, older is a copy with Age = 31

Record Structs (C# 10)

C# 10 added record structs (value types with record features) and readonly record structs (immutable value types). Choose: record class (reference type, for larger/shared data), record struct (value type, for small data, avoids heap allocation), readonly record struct (immutable value type, safest). Record structs have value equality and 'with' support like record classes. Use readonly record struct for small immutable values like Point, Money.

csharp
// record class (default, reference type)
public record PersonRef(string Name, int Age);

// record struct (C# 10): value type with record features
public record struct Point(int X, int Y);

// readonly record struct: fully immutable value type
public readonly record struct Money(decimal Amount, string Currency);

// Mutable record struct (allowed but discouraged)
public record struct Counter(int Value) {
    public int Value { get; set; } = Value;
}

var p = new Point(1, 2);
var p2 = p with { X = 5 };  // copy with X=5
Console.WriteLine(p == p2);  // false

// Records can have additional members
public record Person(string Name) {
    public int Age { get; init; }  // extra init-only prop
    public string DisplayName => Name.ToUpper();
}

Init-only Setters

init-only setters (C# 9) allow setting during object initialization but not afterward—immutability after construction without constructor boilerplate. 'required' (C# 11) forces the caller to set a property in the initializer (compile-time check). Records use init setters by default. This enables immutable object patterns with object-initializer syntax, which is more readable than constructor parameters.

csharp
public class Config {
    // init: settable only during construction (object initializer)
    public string Name { get; init; } = "";
    public int Timeout { get; init; } = 30;
}

var c = new Config { Name = "MyApp", Timeout = 60 };
// c.Name = "Other";  // ERROR: init-only after construction

// Required properties (C# 11): must be set in initializer
public class User {
    public required string Email { get; init; }
    public string? DisplayName { get; init; }
}

// Must set Email, DisplayName optional
var u = new User { Email = "[email protected]" };

// Records use init by default (positional parameters)
public record Point(int X, int Y);
// Equivalent to: public int X { get; init; }
//                public int Y { get; init; }

Record Inheritance

Records support inheritance: a derived record includes base record parameters in its constructor. The 'with' expression preserves the runtime type (creating a Dog from a Dog, not an Animal). Equality checks the runtime type—two records are equal only if they're the same type with equal data. This makes records work correctly in polymorphic collections, unlike naive value equality.

csharp
// Base record
public record Animal(string Name);

// Derived record: must include base parameters
public record Dog(string Name, string Breed) : Animal(Name);

// Derived can add to base
public record Teacher(string Name, string Subject) : Person(Name);

var d = new Dog("Rex", "Labrador");
Console.WriteLine(d);  // Dog { Name = Rex, Breed = Labrador }

// with preserves the runtime type
var d2 = d with { Name = "Buddy" };
Console.WriteLine(d2.GetType().Name);  // Dog (not Animal)

// Value equality across hierarchy
Animal a = new Dog("Rex", "Lab");
Animal b = new Dog("Rex", "Lab");
Console.WriteLine(a == b);  // true

// Different types are not equal even with same data
Animal c = new Animal("Rex");
Console.WriteLine(a == c);  // false (different runtime types)

Records vs Classes vs Structs

Classes use reference equality (== compares references); records use value equality (compares data); structs use value equality by default but are value types (copied on assignment). Use records for immutable data with value semantics (DTOs, value objects, messages). Use classes for mutable entities with identity (User, Order) and inheritance hierarchies. Use structs for small (≤16 bytes) values that should be copied (Point, Color, Money).

csharp
// Class: reference type, reference equality
public class PersonClass {
    public string Name { get; set; }
    public PersonClass(string name) => Name = name;
}
var c1 = new PersonClass("Alice");
var c2 = new PersonClass("Alice");
Console.WriteLine(c1 == c2);  // false (different references)

// Record: reference type, value equality
public record PersonRec(string Name);
var r1 = new PersonRec("Alice");
var r2 = new PersonRec("Alice");
Console.WriteLine(r1 == r2);  // true (value equality)

// Struct: value type, value equality (manual)
public struct PointStruct {
    public int X, Y;
    public PointStruct(int x, int y) { X = x; Y = y; }
}
var s1 = new PointStruct(1, 2);
var s2 = new PointStruct(1, 2);
Console.WriteLine(s1.Equals(s2));  // true (default struct equality)

// Choose:
// - record: immutable data with value equality (DTOs, value objects)
// - class: mutable, reference semantics, inheritance (entities, services)
// - struct: small, value type, avoid heap allocation (Point, Color)
18

Pattern Matching Deep

Property Patterns

Property patterns match against object properties inline. The switch expression returns a value directly. Combined patterns (and/or) create flexible conditions. null and _ (discard) handle edge cases. This replaces verbose if-else chains with declarative, readable code.

csharp
static decimal CalcDiscount(Customer c) => c switch
{
    { IsPremium: true, Orders.Count: > 100 } => 0.20m,
    { IsPremium: true } => 0.10m,
    { Orders.Count: > 50 } => 0.05m,
    { Country: "US" or "CA" } => 0.03m,
    null => 0m,
    _ => 0m
};

List Patterns

List patterns (C# 11) match arrays and indexable sequences. [] matches empty, [single] matches one element, [first, .. rest] uses slice pattern to capture remaining elements. Useful for parsing commands, validating sequences, and recursive algorithms.

csharp
int Sum(int[] n) => n switch
{
    [] => 0,
    [int single] => single,
    [int first, int second] => first + second,
    [int first, .. int[] rest] => first + Sum(rest),
};

// Matching array shapes
var arr = new[] { 1, 2, 3, 4 };
// [1, ..] matches array starting with 1
// [.., 4] matches array ending with 4
// [1, 2, .., 4] matches 1,2,anything,4

Tuple Patterns

Tuple patterns match multiple values simultaneously. Each case tests a tuple of values. Combined with relational patterns (>, <, >=) and logical patterns (and, or, not), this creates powerful multi-dimensional matching without nested if statements.

csharp
static string Classify(int t, bool rain) => (t, rain) switch
{
    (< 0, _) => "Freezing",
    (>= 0 and < 15, true) => "Cold and wet",
    (>= 0 and < 15, false) => "Cold",
    (>= 15 and < 25, _) => "Mild",
    (>= 25, true) => "Hot and wet",
    (>= 25, false) => "Hot",
};

Type Patterns & Guards

Type patterns match runtime types. The when clause adds additional guards. var matches anything (including null). Order matters: more specific patterns must come before general ones. This is the idiomatic way to do type-based dispatch in modern C#.

csharp
object Process(object input) => input switch
{
    int i when i > 0 => $"Positive: {i}",
    int i => $"Non-positive: {i}",
    string s when s.Length > 10 => s.Substring(0, 10),
    string s => s,
    IList list => $"List with {list.Count} items",
    var x => x?.ToString() ?? "null"
};

Nested Patterns

Patterns nest arbitrarily deep. var inside a pattern captures the matched value for use in guards or the expression body. This example matches Point properties inside Line records. Nested patterns are powerful for validating complex object graphs concisely.

csharp
record Point(int X, int Y);
record Line(Point Start, Point End);

string Describe(Line line) => line switch
{
    Line({ X: 0, Y: 0 }, { X: 0, Y: 0 }) => "Point at origin",
    Line({ X: var x1, Y: var y1 }, { X: var x2, Y: var y2 })
        when x1 == x2 && y1 == y2 => "Degenerate line",
    Line({ X: var x1 }, { X: var x2 }) when x1 == x2 => "Vertical",
    Line({ Y: var y1 }, { Y: var y2 }) when y1 == y2 => "Horizontal",
    _ => "Diagonal line"
};
19

Records & With Expressions

Record Value Equality

Records provide value-based equality by default: two instances with the same data are equal. The compiler generates Equals, GetHashCode, ToString, and == operator. Records are reference types but designed for immutability. Use them for DTOs, value objects, and data that should compare by content.

csharp
public record Person(string Name, int Age);

var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
Console.WriteLine(p1 == p2);  // True (value equality)
Console.WriteLine(p1.GetHashCode() == p2.GetHashCode()); // True
Console.WriteLine(p1);  // Person { Name = Alice, Age = 30 }

With Expressions

The with expression creates a copy of a record with modified properties. The original remains unchanged (non-destructive mutation). This is the idiomatic way to update immutable data. Under the hood, the compiler uses a protected copy constructor and init-only setters.

csharp
var alice = new Person("Alice", 30);
var alice31 = alice with { Age = 31 };
var aliceMarried = alice with { Name = "Alice Smith" };

// Original is unchanged (immutable)
Console.WriteLine(alice.Age);      // 30
Console.WriteLine(alice31.Age);    // 31
Console.WriteLine(aliceMarried.Name); // Alice Smith

Positional vs Init-Only

Positional records use primary constructor syntax and generate deconstruct methods. Init-only records use object initializer syntax, allowing default values and required modifiers. Choose positional for simple value types; init-only for complex records with optional or computed properties.

csharp
// Positional record (primary constructor)
public record Point(int X, int Y);

// Init-only record (more flexible properties)
public record Employee
{
    public required string Name { get; init; }
    public decimal Salary { get; init; }
    public string Dept { get; init; } = "General";
}

var emp = new Employee { Name = "Bob", Salary = 50000 };
var promoted = emp with { Salary = 60000 };

Record Structs & Inheritance

Record structs are value types (copied on assignment) and cannot inherit from other record structs. Reference-type records support inheritance. readonly record struct prevents mutation. Use record struct for small immutable values; record class for larger objects that benefit from reference semantics and inheritance.

csharp
// Record struct (value type)
public readonly record struct Money(decimal Amount, string Currency);

// Record inheritance (reference type only)
public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);

var dog = new Dog("Rex", "Labrador");
Console.WriteLine(dog is Animal);  // True
var copied = dog with { Breed = "Poodle" };

Deconstruct & Matching

Positional records auto-generate a Deconstruct method, enabling tuple deconstruction. This integrates seamlessly with pattern matching: you can match record properties positionally or by name. Deconstruction works in var patterns, tuple patterns, and switch expressions.

csharp
var person = new Person("Alice", 30);
var (name, age) = person;  // Deconstruct
Console.WriteLine($"{name} is {age}");

// Works with pattern matching
string Greet(Person p) => p switch
{
    ("Alice", _) => "Hi Alice!",
    (_, < 18) => "Hello young one",
    var (n, a) => $"Hello {n}, age {a}"
};
20

Source Generators

Basic Source Generator

Source generators run at compile time and add C# source files to the compilation. IIncrementalGenerator is the modern API (C# 9+). RegisterPostInitializationOutput adds static code without analyzing user code. Generators are read-only: they can add files but cannot modify existing code.

csharp
[Generator]
public class HelloGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(ctx =>
        {
            ctx.AddSource("Hello.g.cs", """
                namespace Generated;
                public static class Hello
                {
                    public static string World => "Hello!";
                }
                """);
        });
    }
}

Generating from Attributes

ForAttributeWithMetadataName finds types marked with a specific attribute. The pipeline: syntax provider filters nodes, transform extracts data, Collect batches results, RegisterSourceOutput emits code. This incremental pipeline caches results and only regenerates when inputs change, keeping builds fast.

csharp
// Find types marked with an attribute
context.SyntaxProvider.ForAttributeWithMetadataName(
    "MyApp.AutoToStringAttribute",
    predicate: (node, _) => node is ClassDeclarationSyntax,
    transform: (ctx, _) => (ClassDeclarationSyntax)ctx.TargetNode)
    .Collect()
    .RegisterSourceOutput((spc, classes) =>
    {
        foreach (var cls in classes)
            GenerateToString(spc, cls);
    });

StringBuilder Output

Generators produce source as strings. StringBuilder is efficient for multi-line output. The target class must be partial for the generator to add members. Generated files appear in the IDE under Dependencies > Analyzers. Use .g.cs suffix by convention to distinguish generated files.

csharp
void GenToString(SourceProductionContext ctx, ClassDeclarationSyntax cls)
{
    var sb = new StringBuilder();
    sb.AppendLine($"namespace {cls.ContainingNamespace};");
    sb.AppendLine($"public partial class {cls.Identifier}");
    sb.AppendLine("{");
    sb.AppendLine("  public override string ToString()");
    sb.AppendLine("  {");
    var props = cls.Members.OfType<PropertyDeclarationSyntax>();
    sb.AppendLine($"    return $"{cls.Identifier}: " +
      string.Join(", ", props.Select(p => $"{p.Identifier}={{{p.Identifier}}}")) + "";");
    sb.AppendLine("  }");
    sb.AppendLine("}");
    ctx.AddSource($"{cls.Identifier}_ToString.g.cs", sb.ToString());
}

JsonSerializerContext

System.Text.Json includes a built-in source generator that pre-compiles serialization logic, eliminating runtime reflection. This improves performance and supports AOT compilation (Native AOT). Declare types with JsonSerializable, then use the generated context for serialize/deserialize calls.

csharp
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    WriteIndented = true)]
[JsonSerializable(typeof(List<Person>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
public partial class AppJsonContext : JsonSerializerContext { }

// Usage (no runtime reflection):
var json = JsonSerializer.Serialize(people, AppJsonContext.Default.ListPerson);
var data = JsonSerializer.Deserialize(json, AppJsonContext.Default.ListPerson);

Common Use Cases

Popular generator use cases: DTO generation, DI registration, strongly-typed configuration, AutoMapper profiles, logging templates, and JSON serialization. Libraries like MediatR, AutoMapper, and Microsoft.Extensions.Logging use generators to reduce boilerplate and improve AOT compatibility.

csharp
// 1. DTO generation from interfaces
public interface IUserDto { string Name { get; } int Age { get; } }
// Generator creates: public class UserDto : IUserDto { ... }

// 2. Dependency injection registration
[RegisterService(ServiceLifetime.Scoped)]
public class MyService : IService { }
// Generator creates: services.AddScoped<MyService>();

// 3. Strongly-typed configuration
[ConfigurationSection("App:Db")]
public class DbSettings { public string Conn { get; set; } }
// Generator creates: services.Configure<DbSettings>(...)
21

Span & Memory

Span Basics

Span<T> provides a type-safe, memory-safe view over contiguous memory without copying. It can wrap arrays, stackalloc memory, unmanaged memory, or strings. Span is a ref struct (stack-only), so it cannot be boxed, stored in fields, or captured by lambdas. Use it for high-performance slicing and parsing.

csharp
// Span<T> is a stack-only view over contiguous memory
Span<int> numbers = stackalloc int[] { 1, 2, 3, 4, 5 };

// From array
int[] arr = { 10, 20, 30, 40, 50 };
Span<int> slice = arr.AsSpan(1, 3);  // [20, 30, 40]
slice[0] = 99;  // Modifies original array

// From string
ReadOnlySpan<char> text = "Hello, World".AsSpan();
ReadOnlySpan<char> hello = text.Slice(0, 5);  // "Hello"

Memory for Async

Memory<T> is the heap-safe counterpart to Span<T>. It can be stored in fields, captured by lambdas, and used across await boundaries. Convert to Span<T> via .Span when doing actual work. Use Memory for async APIs and Span for synchronous hot paths.

csharp
// Memory<T> can live on the heap and cross async boundaries
async Task ProcessAsync(Memory<byte> buffer)
{
    await Task.Delay(100);
    Span<byte> span = buffer.Span;
    for (int i = 0; i < span.Length; i++)
        span[i] = (byte)(span[i] * 2);
}

byte[] data = new byte[1024];
await ProcessAsync(data);  // Implicit conversion to Memory<byte>

Avoiding Allocations

Span eliminates intermediate string allocations during parsing and slicing. For hot paths processing large inputs (HTTP parsing, JSON, log files), this can reduce GC pressure dramatically. int.TryParse and other primitives have Span overloads. Only call ToString() when you actually need a string.

csharp
// Traditional (allocates new string)
string Extract(string input, int start, int len)
    => input.Substring(start, len);

// Zero-allocation with Span
ReadOnlySpan<char> ExtractFast(ReadOnlySpan<char> input, int start, int len)
    => input.Slice(start, len);

// Parse without allocating substrings
bool TryParseInt(ReadOnlySpan<char> span, out int result)
    => int.TryParse(span, out result);

Stackalloc & Unmanaged

stackalloc allocates on the call stack (auto-freed, no GC pressure) but is limited to small sizes. For large buffers, use NativeMemory.Alloc/Free (C# 7+) or Marshal.AllocHGlobal. Always wrap unmanaged memory in a Span and free it in a finally block to prevent leaks.

csharp
// Stack allocation (small, fast, auto-freed)
Span<int> buffer = stackalloc int[128];
for (int i = 0; i < buffer.Length; i++)
    buffer[i] = i;

// Unmanaged memory (large buffers, manual lifetime)
unsafe
{
    int* ptr = (int*)NativeMemory.Alloc(1024, sizeof(int));
    try
    {
        Span<int> span = new Span<int>(ptr, 1024);
        span.Fill(42);
    }
    finally
    {
        NativeMemory.Free(ptr);
    }
}

Span-Friendly APIs

Design APIs to accept Span<T> or ReadOnlySpan<T> for buffers, avoiding array allocations. The caller decides whether to use stackalloc, an array, or unmanaged memory. Span.TryWrite (C# 10+) formats directly into a span without allocating a string, useful for logging and serialization.

csharp
// Writing to a Span buffer
int WriteValues(Span<int> dest)
{
    int count = 0;
    foreach (var v in new[] { 1, 2, 3 })
    {
        if (count >= dest.Length) break;
        dest[count++] = v;
    }
    return count;
}

// Span-based string formatting (C# 10+)
Span<char> dest = stackalloc char[64];
bool ok = dest.TryWrite($"Value: {42:N2}", out int charsWritten);
Console.WriteLine(dest.Slice(0, charsWritten).ToString());
22

Interpolated Strings Deep

Format Specifiers

String interpolation ($"...") embeds expressions in braces. Format specifiers after a colon control output: C for currency, F for fixed decimals, X for hex, D for digits with padding, P for percent, yyyy-MM-dd for dates. The compiler translates this to string.Format or FormattableString.

csharp
string name = "Alice";
int age = 30;
decimal price = 19.99m;
DateTime now = DateTime.Now;

Console.WriteLine($"Name: {name}, Age: {age}");
Console.WriteLine($"Price: {price:C}");  // $19.99
Console.WriteLine($"Date: {now:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"Hex: {255:X}");  // FF
Console.WriteLine($"Padded: {age:D5}");  // 00030
Console.WriteLine($"Percent: {0.85:P0}");  // 85%

Raw String Interpolation

Verbatim interpolated strings ($@) preserve backslashes literally and allow multi-line content. Raw string interpolation (C# 11) uses triple quotes and eliminates all escaping: no need to escape braces or quotes. The number of $ signs controls how many braces are needed for interpolation.

csharp
// Verbatim interpolated ($@)
string path = $@"C:\Users\{name}\Documents";
string json = $@"{{
  ""name"": ""{name}"",
  ""age"": {age}
}}";

// Raw string interpolation (C# 11)
string query = $$"""
    SELECT * FROM Users
    WHERE Name = '{{name}}'
    AND Age > {{age - 5}}
    """;
// $$ means {{ }} for interpolation, no escaping needed

FormattableString & Culture

FormattableString preserves the interpolation structure, allowing deferred formatting with specific cultures. This is critical for libraries that should not depend on the current culture. FormattableString.Invariant forces invariant culture, preventing locale-specific issues in serialization and logging.

csharp
// FormattableString captures the format for deferred execution
FormattableString msg = $"Hello {name}, you are {age} years old";

// Explicit culture (avoid CurrentCulture issues in libraries)
string german = msg.ToString(CultureInfo.GetCultureInfo("de-DE"));
string invariant = msg.ToString(CultureInfo.InvariantCulture);

// Invariant by default
string formatted = FormattableString.Invariant($"Total: {price}");

Interpolated String Handlers

Interpolated string handlers (C# 10) let the compiler pass interpolation components directly to a method, avoiding string allocation when the result is not needed. This is how high-performance logging works: if the log level is disabled, no string is built. The handler receives literals and formatted values separately.

csharp
// Custom handler (C# 10+) for conditional allocation
[InterpolatedStringHandler]
public ref struct LogStringHandler
{
    public LogStringHandler(int literal, int hole, Logger logger, out bool enabled)
    {
        enabled = logger.IsEnabled;
        // Skip allocation if logging is disabled
    }
    public void AppendLiteral(string s) { /* ... */ }
    public void AppendFormatted<T>(T value) { /* ... */ }
}

// Usage: no string allocation if log level is off
logger.Info($"Processing {item} with {count} items");

Span-Based Formatting

Span.TryWrite formats interpolated strings directly into a stack-allocated buffer, avoiding heap allocation entirely. string.Create with an interpolated string and culture gives you culture-aware formatting without intermediate allocations. These are essential techniques for hot paths in high-throughput applications.

csharp
// Format directly into a buffer (C# 10+)
Span<char> buffer = stackalloc char[256];

if (buffer.TryWrite($"User: {name}, Age: {age}", out int chars))
{
    var result = buffer[..chars];
    Console.WriteLine(result);
}

// Concatenation with DefaultInterpolatedStringHandler
string built = string.Create(CultureInfo.InvariantCulture, $"Total={price}");
23

File-Scoped Namespaces

File-Scoped Syntax

File-scoped namespaces (C# 10) declare the namespace for the entire file with a semicolon instead of braces. This removes one level of indentation, making code flatter and easier to read. Only one file-scoped namespace per file is allowed. All types in the file belong to that namespace.

csharp
// Traditional (adds a level of indentation)
namespace MyApp.Services
{
    public class UserService
    {
        public void Process() { }
    }
}

// File-scoped (C# 10+, no indentation)
namespace MyApp.Services;

public class UserService
{
    public void Process() { }
}

Global Usings

Global usings (C# 10) declared in one file apply to all files in the project. Combine with file-scoped namespaces for clean, minimal boilerplate. The .NET SDK projects auto-generate global usings for common namespaces (ImplicitUsings). Order: global usings, then file usings, then namespace.

csharp
// Global usings (apply to all files in the project)
global using System.Collections.Generic;
global using System.Linq;

// File-scoped namespace with usings
using System.Text.Json;
using MyApp.Models;

namespace MyApp.Api;

public class UserController
{
    // Can use List, LINQ, Json, Models without per-file usings
}

Nested Namespaces

File-scoped namespaces use dotted names for hierarchy: MyApp.Services.Authentication is equivalent to nested namespace blocks. Multiple files can share the same namespace. You cannot nest types in a different namespace within the same file; use separate files instead.

csharp
// File-scoped: flat name with dots
namespace MyApp.Services.Authentication;

public class TokenService { }

// Multiple files can use the same namespace
// File 1: namespace MyApp.Services;
// File 2: namespace MyApp.Services;  // Same namespace

Project Configuration

ImplicitUsings in the project file auto-generates global usings for common System namespaces. Combined with file-scoped namespaces, this eliminates most boilerplate. Nullable enables nullable reference types. These are the default for new .NET 6+ projects.

csharp
<!-- .csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

<!-- ImplicitUsings generates:
     global using System;
     global using System.Collections.Generic;
     global using System.IO;
     global using System.Linq;
     global using System.Net.Http;
     global using System.Threading;
     global using System.Threading.Tasks;
-->

Migration & Best Practices

Use .editorconfig to enforce file-scoped namespaces (IDE0161). The IDE provides a one-click migration from traditional to file-scoped. Best practice: one public type per file, with the file name matching the type name. This keeps files small and navigation easy.

csharp
// .editorconfig to enforce file-scoped namespaces
[*.{cs,vb}]
dotnet_diagnostic.IDE0161.severity = error  // Prefer file-scoped

// Migration: use the IDE fixer to convert
// Right-click > Quick Actions > Convert to file-scoped namespace

// Best practice: one type per file
// UserService.cs
namespace MyApp.Services;
public class UserService { }
24

Required Members

Required Properties

The required modifier (C# 11) forces callers to initialize properties during object creation. The compiler emits CS9035 if a required member is not set. This replaces constructor validation for mandatory properties. Works with init-only setters and object initializers, providing compile-time guarantees.

csharp
public class User
{
    public required Guid Id { get; init; }
    public required string Name { get; init; }
    public required string Email { get; init; }
    public string? Phone { get; init; }  // Optional
}

// Compiler enforces initialization
var user = new User
{
    Id = Guid.NewGuid(),
    Name = "Alice",
    Email = "[email protected]"
};

// Error CS9035: Required member 'Name' must be set
// var bad = new User { Id = Guid.NewGuid() };

SetsRequiredMembers

The SetsRequiredMembers attribute tells the compiler that a constructor initializes all required members, allowing callers to use the constructor without an object initializer. This bridges the gap between required properties and traditional constructors. The attribute does not verify at runtime; it is a compile-time promise.

csharp
public class Configuration
{
    public required string ConnectionString { get; init; }
    public required int Port { get; init; }

    [SetsRequiredMembers]
    public Configuration(string conn, int port)
    {
        ConnectionString = conn;
        Port = port;
    }
}

// Both work: constructor or initializer
var c1 = new Configuration("localhost", 5432);
var c2 = new Configuration
{
    ConnectionString = "localhost",
    Port = 5432
};

Required with Records

Required members work with records. For positional records, required properties are declared in the body, not the primary constructor. The with expression preserves required values automatically. Default values (like OpenedAt) make properties optional without the required modifier.

csharp
public record Account
{
    public required string AccountNumber { get; init; }
    public required decimal Balance { get; init; }
    public DateTime OpenedAt { get; init; } = DateTime.UtcNow;
}

// Positional record with required
public record Customer(string Name)
{
    public required string Email { get; init; }
}

var cust = new Customer("Alice") { Email = "[email protected]" };

Init vs Required vs Set

init makes a property settable only during construction (immutable after). required forces initialization. Together (required + init) they create mandatory immutable properties. required + set allows post-construction modification. Choose based on whether the value should change after creation.

csharp
// Init-only: can be set in initializer, but not required
public class Settings
{
    public string Name { get; init; } = "Default";
}

// Required + init: must be set, immutable after
public class SecureSettings
{
    public required string ApiKey { get; init; }
}

// Required + set: must be set, but can be modified later
public class MutableConfig
{
    public required string Environment { get; set; }
}

JSON Serialization

System.Text.Json respects the required modifier: deserialization throws JsonException if a required property is missing from JSON. This provides runtime validation matching the compile-time contract. Combine with init for immutable DTOs that are guaranteed to have all mandatory fields populated.

csharp
using System.Text.Json;

public class ApiResponse
{
    public required string Status { get; init; }
    public required int Code { get; init; }
    public string? Message { get; init; }
}

// Deserialization enforces required at runtime
var json = """{"Status":"OK","Code":200}""";
var response = JsonSerializer.Deserialize<ApiResponse>(json);

// Throws JsonException if required member is missing
// var bad = JsonSerializer.Deserialize<ApiResponse>("""{"Code":200}""");
25

Collection Expressions

Collection Expression Syntax

Collection expressions (C# 12) provide a uniform syntax [..] for initializing any collection type. The compiler infers the type and uses the appropriate builder. This replaces verbose new List<int> { 1, 2, 3 } syntax. Works with arrays, List<T>, Span<T>, IEnumerable<T>, and custom collections with a CollectionBuilder attribute.

csharp
// C# 12 collection expressions
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];
Span<int> span = [10, 20, 30];
IEnumerable<int> seq = [1, 2, 3];

// Empty collections
int[] empty = [];
List<int> emptyList = [];

// Works with any collection type that has a builder
HashSet<string> set = ["a", "b", "c"];

Spread Operator

The spread operator (..) flattens a collection into the enclosing collection expression. This replaces Concat, AddRange, and manual loops. You can mix spreads with individual elements. The spread works with any IEnumerable<T>, making it easy to combine multiple data sources concisely.

csharp
int[] a = [1, 2, 3];
int[] b = [4, 5, 6];

// Spread elements with ..
int[] combined = [..a, ..b, 7, 8];  // [1,2,3,4,5,6,7,8]

// Mix spreads and literals
List<int> result = [0, ..a, 100, ..b];

// Spread any IEnumerable
List<int> evens = [.. Enumerable.Range(0, 10).Where(x => x % 2 == 0)];

Return & Parameter Patterns

Collection expressions work in return statements, arguments, and assignments. The compiler converts to the target type automatically. Conditional spreads (ternary with ..) let you optionally include elements. This makes building collections from multiple optional sources clean and readable.

csharp
// Return collection expressions
List<int> GetNumbers() => [1, 2, 3];
int[] GetArray() => [10, 20];

// Pass as arguments
void Process(IEnumerable<int> data) { }
Process([1, 2, 3]);

// Conditional spread
bool includeExtra = true;
var items = [
    1, 2, 3,
    .. includeExtra ? [4, 5] : []
];

Custom Collection Builders

The CollectionBuilder attribute connects a collection type to a factory method, enabling collection expression support. The Create method receives a ReadOnlySpan<T> of elements. This allows third-party collections to participate in the uniform [..] syntax. The builder pattern keeps initialization efficient.

csharp
[CollectionBuilder(typeof(MyCollection), "Create")]
public class MyCollection<T> : IEnumerable<T>
{
    private readonly List<T> _items = new();
    public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();

    public static MyCollection<T> Create(ReadOnlySpan<T> items)
    {
        var col = new MyCollection<T>();
        foreach (var item in items) col._items.Add(item);
        return col;
    }
}

// Now usable with collection expressions
MyCollection<int> custom = [1, 2, 3, 4];

Params Collections

C# 12 allows params with any collection type, not just arrays. Collection expressions can be passed to params methods. When the target is Span<T>, the compiler may use stack allocation for small collections, avoiding heap allocation entirely. This makes variadic methods both ergonomic and efficient.

csharp
// C# 12: params with collection expressions
void Log(params string[] messages)
{
    foreach (var m in messages) Console.WriteLine(m);
}

// Traditional params call
Log("Error", "Warning", "Info");

// Collection expression call (C# 12)
Log(["Error", "Warning", "Info"]);

// params with Span for zero-allocation
void ProcessFast(params Span<int> values) { }
ProcessFast([1, 2, 3]);  // May use stackalloc internally

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.