C# Handbook

C# is a statically typed, multi-paradigm language developed by Microsoft as the primary language of the .NET platform. First released in 2000, it has evolved rapidly — each version adds features that rival the most expressive modern languages: LINQ (2007), async/await (2012), records and pattern matching (2020+), and nullable reference types. It runs on Windows, macOS, and Linux via .NET, and powers everything from Unity games to Azure microservices to desktop WPF applications.

Pick C# when

  • You are building on the Microsoft / Azure ecosystem — C# and .NET are first-class citizens. ASP.NET Core, Azure Functions, SignalR, Entity Framework, and Blazor are best-in-class tooling.
  • You are making a Unity game — Unity's scripting API is C# and the ecosystem (Asset Store, documentation, tutorials) is built around it.
  • You need enterprise-grade OOP with a rich type system — generics with variance, interfaces, records, sealed hierarchies, pattern matching, and source generators give you expressive, safe domain modelling.
  • You want async I/O without the complexity of Node.js — async/await in C# is deeply integrated: ASP.NET Core pipelines are async end-to-end, and the TPL handles CPU parallelism cleanly.
  • Cross-platform desktop apps — .NET MAUI and Avalonia let you target Windows, macOS, iOS, and Android from a single C# codebase.

Think twice before choosing C# when

  • You are outside the .NET ecosystem — interoperability with non-Microsoft infrastructure (AWS Lambda with custom runtimes, embedded Linux, WASM) is possible but requires more effort than Go or Python.
  • Startup time is critical — .NET has historically had slow cold-start times. .NET 8 Native AOT helps, but for CLI tools and short-lived lambdas, Go or Python still start faster.
  • You need a very small footprint — the .NET runtime is not tiny. For embedded or edge targets, C, Rust, or MicroPython are better choices.
  • Your team prefers open ecosystems with no corporate alignment — C# is open source but Microsoft drives the roadmap. Go, Rust, and Python have more community-neutral governance.

C# vs. its closest alternatives

  • C# vs Java — both run on a managed runtime. C# has evolved faster: records, top-level statements, nullable refs, pattern matching, and LINQ are all superior to Java equivalents. Java wins on cross-platform portability and legacy ecosystem breadth.
  • C# vs Python — Python is faster to prototype but C# catches far more errors at compile time and is 10–100× faster at runtime. Use Python for data science and scripts; C# for production services where type safety and throughput matter.
  • C# vs Go — Go is simpler, starts faster, and has a smaller runtime. C# has richer type system features and better tooling for complex domain models. Go wins on ops simplicity; C# wins on developer ergonomics in large codebases.

Resources

Topics

Variables & Types

csharp
// Primitive types
int     age   = 30;
long    big   = 9_000_000_000L;
float   f     = 3.14f;
double  d     = 3.14159265;
decimal money = 9.99m;         // exact decimal
char    c     = 'A';
bool    ok    = true;
string  name  = 'Alice';
object  obj   = 42;            // all types inherit object

// Type inference
var count = 0;
var items = new List<string>();

// const and readonly
const double PI = 3.14159;
readonly DateTime CreatedAt = DateTime.UtcNow;

// Nullable value types
int? maybe = null;
if (maybe.HasValue) Console.WriteLine(maybe.Value);
int value = maybe ?? -1;   // null coalescing

// Nullable reference types (C# 8+ — enable in .csproj)
string? nullable = null;
string nonNull = nullable ?? 'default';

Strings

csharp
string name = 'Alice';

// Interpolation
string msg = 
#39;Hello, {name}! Age: {30 + 1}'; // Verbatim string (no escape processing) string path = @'C:\Users\Alice\Documents'; // Raw string literals (C# 11) string json = """ { 'name': 'Alice' } """; // Common methods name.ToUpper(); name.Trim(); name.Contains('li'); name.StartsWith('Al'); name.Replace('l', 'L'); name.Split(','); string.Join(', ', arr); name.Substring(1, 3); // 'lic' name.Length; // 5 // Safe parse int.TryParse('42', out int n); // StringBuilder (efficient for many concatenations) var sb = new System.Text.StringBuilder(); sb.Append('Hello'); sb.AppendLine(name); string result = sb.ToString();

Control Flow

csharp
int x = 42;

// if / else if / else
if (x > 100) Console.WriteLine('big');
else if (x > 10) Console.WriteLine('medium');
else Console.WriteLine('small');

// Ternary
string label = x % 2 == 0 ? 'even' : 'odd';

// switch expression (C# 8+)
string size = x switch {
    > 100 => 'big',
    > 10  => 'medium',
    _     => 'small'
};

// for / foreach / while
for (int i = 0; i < 5; i++) { /* ... */ }

int[] nums = { 1, 2, 3, 4, 5 };
foreach (int n in nums) { Console.WriteLine(n); }

int count = 5;
while (count-- > 0) { /* ... */ }

Methods & Functions

csharp
// Expression-bodied method
public int Add(int a, int b) => a + b;

// Default arguments
public static string Greet(string name, string greeting = 'Hello')
    => 
#39;{greeting}, {name}!'; // Named and optional arguments Greet(greeting: 'Hi', name: 'Alice'); // out parameter bool TryDivide(int a, int b, out int result) { if (b == 0) { result = 0; return false; } result = a / b; return true; } if (TryDivide(10, 2, out int val)) Console.WriteLine(val); // params int Sum(params int[] nums) => nums.Sum(); // Lambda Func<int, int> square = x => x * x; Func<int, int, int> add = (a, b) => a + b; Action<string> print = msg => Console.WriteLine(msg); Predicate<int> isPositive = n => n > 0;

Classes & OOP

csharp
public class Animal {
    public string Name { get; init; }   // init-only property (C# 9)
    public string Sound { get; private set; }

    public Animal(string name, string sound) {
        Name = name;
        Sound = sound;
    }

    public virtual string Speak() => 
#39;{Name} says {Sound}'; public override string ToString() =>
#39;Animal({Name})'; } public class Dog : Animal { public string Breed { get; } public Dog(string name, string breed) : base(name, 'Woof') { Breed = breed; } public override string Speak() => base.Speak() + '!'; } // Abstract class public abstract class Shape { public abstract double Area(); public string Describe() =>
#39;Shape with area {Area():F2}'; } // Struct (value type — stack allocated, copied by value) public struct Point { public double X { get; } public double Y { get; } public Point(double x, double y) { X = x; Y = y; } public double Distance() => Math.Sqrt(X * X + Y * Y); }

Interfaces & Generics

csharp
// Generic interface with constraints
public interface IRepository<T> where T : class {
    Task<T?> FindByIdAsync(int id);
    Task<IEnumerable<T>> FindAllAsync();
    Task SaveAsync(T entity);
    Task DeleteAsync(int id);
}

// Enum
public enum Status { Pending, Active, Inactive }
Status s = Status.Active;
int val = (int)s;             // 1
string name2 = s.ToString();  // 'Active'
Enum.TryParse('Pending', out Status parsed);

LINQ

csharp
using System.Linq;

var nums = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Method syntax
var result = nums
    .Where(n => n % 2 == 0)
    .OrderByDescending(n => n)
    .Select(n => n * n)
    .ToArray();

// Query syntax
var evens = from n in nums
            where n % 2 == 0
            orderby n descending
            select n * n;

// Aggregates
nums.Sum();        // 55
nums.Average();    // 5.5
nums.Min();        // 1
nums.Max();        // 10
nums.Count(n => n > 5);   // 5

// First / Any / All
nums.First(n => n > 3);
nums.FirstOrDefault(n => n > 100);   // 0 (default int)
nums.Any(n => n > 9);                // true
nums.All(n => n > 0);                // true

// Grouping
var grouped = nums.GroupBy(n => n % 2 == 0 ? 'even' : 'odd');
foreach (var g in grouped) {
    Console.WriteLine(
#39;{g.Key}: {string.Join(',', g)}'); }

Async / Await

csharp
using System.Threading.Tasks;
using System.Net.Http;

// async / await
public async Task<string> FetchDataAsync(string url) {
    using var client = new HttpClient();
    var response = await client.GetAsync(url);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync();
}

// Parallel awaits
var tasks = urls.Select(url => FetchDataAsync(url));
var results = await Task.WhenAll(tasks);

// First to complete
var first = await Task.WhenAny(tasks);

// CancellationToken
public async Task LongRunningAsync(CancellationToken ct) {
    for (int i = 0; i < 100; i++) {
        ct.ThrowIfCancellationRequested();
        await Task.Delay(100, ct);
    }
}

// IAsyncEnumerable (C# 8+)
public async IAsyncEnumerable<int> GenerateAsync() {
    for (int i = 0; i < 10; i++) {
        await Task.Delay(100);
        yield return i;
    }
}

await foreach (var item in GenerateAsync()) {
    Console.WriteLine(item);
}

Records & Pattern Matching

csharp
// Record — immutable, value equality, auto ToString
public record Person(string Name, int Age);

var alice = new Person('Alice', 30);
var older = alice with { Age = 31 };   // non-destructive mutation
Console.WriteLine(alice == older);     // false

// Record struct (C# 10)
public record struct Temperature(double Celsius) {
    public double Fahrenheit => Celsius * 9/5 + 32;
}

// Pattern matching — switch expression
string Describe(object o) => o switch {
    int i when i < 0     => 'negative int',
    int i                => 
#39;positive int: {i}', string { Length: 0 } => 'empty string', string s =>
#39;string: {s}', null => 'null', _ => 'unknown' }; // Property pattern record Point(double X, double Y); if (new Point(3, 4) is Point { X: > 0, Y: > 0 }) { Console.WriteLine('First quadrant'); } // List pattern (C# 11) int[] arr = { 1, 2, 3 }; if (arr is [1, 2, ..]) Console.WriteLine('starts with 1, 2');

Exceptions

csharp
try {
    int result = 10 / 0;
} catch (DivideByZeroException ex) {
    Console.WriteLine(
#39;Error: {ex.Message}'); } catch (Exception ex) when (ex.Message.Contains('network')) { Console.WriteLine('Network error'); // exception filter } catch (Exception ex) { Console.WriteLine(
#39;Error: {ex.Message}'); throw; // rethrow — preserves original stack trace } finally { Console.WriteLine('Always runs'); } // Custom exception public class AppException : Exception { public int Code { get; } public AppException(string message, int code) : base(message) => Code = code; } // using statement — automatic IDisposable cleanup using var stream = File.OpenRead('file.txt'); using var reader = new StreamReader(stream); string content = reader.ReadToEnd(); // stream and reader are disposed here

Delegates & Events

csharp
// Delegate type declaration
public delegate int Transform(int value);

// Built-in generic delegates
Action<string> log    = msg => Console.WriteLine(
#39;[LOG] {msg}'); Func<int, int> square = x => x * x; Predicate<int> isEven = n => n % 2 == 0; // Multicast delegate — all subscribers called in order Action<string> notify = s => Console.WriteLine(
#39;Handler1: {s}'); notify += s => Console.WriteLine(
#39;Handler2: {s}'); notify('event fired'); // both handlers run // Custom EventArgs public class ValueChangedEventArgs : EventArgs { public int OldValue { get; } public int NewValue { get; } public ValueChangedEventArgs(int oldVal, int newVal) { OldValue = oldVal; NewValue = newVal; } } // Class with event public class Counter { private int _value; public event EventHandler<ValueChangedEventArgs>? ValueChanged; public int Value { get => _value; set { ValueChanged?.Invoke(this, new ValueChangedEventArgs(_value, value)); _value = value; } } } // Subscribe / unsubscribe var counter = new Counter(); EventHandler<ValueChangedEventArgs> handler = (_, e) => Console.WriteLine(
#39;{e.OldValue} -> {e.NewValue}'); counter.ValueChanged += handler; counter.Value = 10; // fires: 0 -> 10 counter.ValueChanged -= handler; // unsubscribe // Lambda stored as typed delegate Transform doubleIt = x => x * 2; int res = doubleIt(5); // 10

Extension Methods

csharp
// Extension methods live in a static class
public static class StringExtensions {
    // 'this' marks the type being extended
    public static bool IsPalindrome(this string s) {
        s = new string(s.ToLower().Where(char.IsLetter).ToArray());
        return s == new string(s.Reverse().ToArray());
    }

    public static string Truncate(this string s, int maxLen, string suffix = '...') =>
        s.Length <= maxLen ? s : s[..maxLen] + suffix;
}

// Extending IEnumerable<T>
public static class EnumerableExtensions {
    public static IEnumerable<T> WhereNotNull<T>(
        this IEnumerable<T?> source) where T : class
        => source.Where(x => x is not null)!;

    public static IEnumerable<(int Index, T Item)> Indexed<T>(
        this IEnumerable<T> source)
        => source.Select((item, i) => (i, item));
}

// Usage
bool palindrome  = 'racecar'.IsPalindrome();      // true
string truncated = 'Hello World'.Truncate(5);     // 'Hello...'

var names = new[] { 'Alice', null, 'Bob', null };
foreach (var (i, name) in names.WhereNotNull().Indexed()) {
    Console.WriteLine(
#39;{i}: {name}'); } // Fluent builder pattern via extension methods public static class BuilderExtensions { public static StringBuilder AppendLineIf( this StringBuilder sb, bool condition, string text) => condition ? sb.AppendLine(text) : sb; } string msg = new StringBuilder() .AppendLine('Hello') .AppendLineIf(true, 'World') .AppendLineIf(false, 'Ignored') .ToString();

Span<T> & Memory<T>

csharp
// Span<T> — stack-only, zero-allocation slice over contiguous memory
int[] array = { 1, 2, 3, 4, 5 };
Span<int> span  = array.AsSpan();
Span<int> slice = span[1..4];   // { 2, 3, 4 } — no copy

// Slicing strings without allocation
ReadOnlySpan<char> text = 'Hello, World!'.AsSpan();
ReadOnlySpan<char> word = text[..5];   // 'Hello' — no string allocation

// Parsing without allocations
ReadOnlySpan<char> csv = '42,100,7'.AsSpan();
while (csv.Length > 0) {
    int comma = csv.IndexOf(',');
    var part  = comma < 0 ? csv : csv[..comma];
    int.TryParse(part, out int num);
    Console.WriteLine(num);
    csv = comma < 0 ? ReadOnlySpan<char>.Empty : csv[(comma + 1)..];
}

// stackalloc — allocate on stack, wrap in Span
Span<byte> buffer = stackalloc byte[64];
buffer.Fill(0);

// Memory<T> — heap-safe, works across async boundaries
Memory<int> memory = new Memory<int>(array, 1, 3);
await ProcessAsync(memory);   // safe to pass to async methods

static async Task ProcessAsync(Memory<int> mem) {
    await Task.Yield();
    Span<int> s = mem.Span;   // access Span inside a sync scope
    foreach (var item in s) Console.WriteLine(item);
}

// MemoryMarshal — reinterpret raw memory
ReadOnlySpan<byte> bytes = stackalloc byte[] { 0x01, 0x00, 0x00, 0x00 };
int value = System.Runtime.InteropServices.MemoryMarshal.Read<int>(bytes);

Advanced Collections

csharp
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Collections.Immutable;

// Interface hierarchy
IEnumerable<int>  seq  = new List<int> { 1, 2, 3 };   // read-only iteration
ICollection<int>  coll = new List<int> { 1, 2, 3 };   // Count + Add/Remove
IList<int>        list = new List<int> { 1, 2, 3 };   // index access

// LinkedList — O(1) insert/remove at a known node
var linked = new LinkedList<string>(new[] { 'alpha', 'beta', 'gamma' });
linked.AddFirst('zeta');
linked.Remove('beta');

// SortedDictionary — keys always sorted (Red-Black tree, O(log n))
var sorted = new SortedDictionary<string, int> {
    ['banana'] = 2, ['apple'] = 5, ['cherry'] = 1
};

// HashSet — set operations
var a = new HashSet<int> { 1, 2, 3, 4 };
var b = new HashSet<int> { 3, 4, 5, 6 };
a.IntersectWith(b);   // a == { 3, 4 }
a.UnionWith(b);       // a == { 3, 4, 5, 6 }

// ConcurrentDictionary — thread-safe without external locking
var cc = new ConcurrentDictionary<string, int>();
cc.AddOrUpdate('hits', 1, (k, old) => old + 1);
int val = cc.GetOrAdd('missing', _ => 42);

// ImmutableList — returns new instance on each mutation
ImmutableList<int> imm  = ImmutableList.Create(1, 2, 3);
ImmutableList<int> imm2 = imm.Add(4);   // original unchanged

// ArrayPool — reuse large buffers to reduce GC pressure
var pool   = System.Buffers.ArrayPool<byte>.Shared;
byte[] buf = pool.Rent(1024);
try { /* use buf */ } finally { pool.Return(buf); }

// Custom IEqualityComparer
public class CaseInsensitiveComparer : IEqualityComparer<string> {
    public bool Equals(string? x, string? y) =>
        string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
    public int GetHashCode(string obj) =>
        obj.ToLowerInvariant().GetHashCode();
}
var dict = new Dictionary<string, int>(new CaseInsensitiveComparer());

Reflection & Attributes

csharp
using System;
using System.Reflection;

// Custom attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuditAttribute : Attribute {
    public string Action { get; }
    public AuditAttribute(string action) => Action = action;
}

[Audit('UserLogin')]
public class AuthService {
    [Audit('GetUser')]
    public string GetUser(int id) => 
#39;User-{id}'; [Obsolete('Use GetUser instead')] public string FetchUser(int id) => GetUser(id); } // Inspect type at runtime Type t = typeof(AuthService); Console.WriteLine(t.FullName); foreach (var prop in t.GetProperties()) Console.WriteLine(
#39;{prop.Name}: {prop.PropertyType.Name}'); foreach (var m in t.GetMethods(BindingFlags.Public | BindingFlags.Instance)) Console.WriteLine(
#39;{m.Name}({m.GetParameters().Length} params)'); // Read custom attribute var attr = t.GetCustomAttribute<AuditAttribute>(); Console.WriteLine(attr?.Action); // 'UserLogin' // Activator.CreateInstance — instantiate by type object instance = Activator.CreateInstance(typeof(AuthService))!; // MethodInfo.Invoke — dynamic method invocation MethodInfo? method = t.GetMethod('GetUser'); object? result = method?.Invoke(instance, new object[] { 42 }); Console.WriteLine(result); // 'User-42' // CallerMemberName — inject caller name at compile time (no reflection cost) public static void Log(string msg, [System.Runtime.CompilerServices.CallerMemberName] string caller = '') => Console.WriteLine(
#39;[{caller}] {msg}'); Log('started'); // [MyMethod] started

Source Generators & Modern C#

csharp
// partial class — split across files; enables source generators
public partial class OrderService {
    partial void OnOrderCreated(int id);   // declared in generated file
}
public partial class OrderService {
    public void CreateOrder(int id) {
        // business logic...
        OnOrderCreated(id);   // calls generated partial method if present
    }
}

// required properties (C# 11) — must be set at object construction
public class Config {
    public required string Host   { get; init; }
    public required int    Port   { get; init; }
    public          string Scheme { get; init; } = 'https';
}
var cfg = new Config { Host = 'localhost', Port = 5432 };

// file-scoped type (C# 11) — invisible outside its compilation unit
file class InternalHelper {
    public static int Compute(int x) => x * 2;
}

// ref struct — stack-only, cannot be boxed or heap-allocated
public ref struct StackBuffer {
    private Span<byte> _data;
    public StackBuffer(Span<byte> data) => _data = data;
    public int Length => _data.Length;
}

// Raw string literals (C# 11) — no escape sequences needed
string json = """
    {
        'name': 'Alice',
        'age': 30
    }
    """;

// checked / unchecked — control arithmetic overflow behavior
int max = int.MaxValue;
unchecked { int overflow = max + 1; }   // wraps silently
checked   { int overflow = max + 1; }   // throws OverflowException

// GeneratedRegex (C# 11) — compile-time regex, AOT-friendly
[System.Text.RegularExpressions.GeneratedRegex(@'d{4}-d{2}-d{2}')]
private static partial System.Text.RegularExpressions.Regex DatePattern();
bool isDate = DatePattern().IsMatch('2024-01-15');   // true

// System.Text.Json source generation — faster serialization, AOT-safe
[System.Text.Json.Serialization.JsonSerializable(typeof(Config))]
internal partial class ConfigJsonContext
    : System.Text.Json.Serialization.JsonSerializerContext { }

Unsafe Code & Fixed Buffers

The unsafe keyword enables pointer arithmetic, fixed pinning, and stack allocation with stackalloc. For most high-performance scenarios, Span<T> and Memory<T> cover the same ground without unsafe.

csharp
// Enable: <AllowUnsafeBlocks>true</AllowUnsafeBlocks> in .csproj
// Compile: dotnet build -p:AllowUnsafeBlocks=true

unsafe void PointerBasics() {
    int x = 42;
    int* p = &x;
    Console.WriteLine(*p);   // 42
    *p = 100;
    Console.WriteLine(x);    // 100

    // Pointer arithmetic
    int[] arr = { 1, 2, 3 };
    fixed (int* ptr = arr) {
        for (int i = 0; i < arr.Length; i++)
            Console.Write(*(ptr + i) + ' ');
    }
}

// fixed — pin a managed object so GC won't move it
unsafe void CopyBytes(byte[] src, byte[] dst) {
    fixed (byte* s = src, d = dst) {
        Buffer.MemoryCopy(s, d, dst.Length, src.Length);
    }
}

// Stackalloc — allocate on the stack (no GC, no heap)
unsafe void StackBuffer() {
    int* buf = stackalloc int[64];
    for (int i = 0; i < 64; i++) buf[i] = i * i;
    Console.WriteLine(buf[7]);   // 49
}

// Span<T> + stackalloc (safe equivalent — no unsafe needed)
Span<int> span = stackalloc int[64];
span.Fill(0);
span[3] = 99;

Channels & IAsyncEnumerable

Channel<T> provides a thread-safe, back-pressured async queue for producer-consumer pipelines. IAsyncEnumerable<T> combined with await foreach enables streaming async sequences with cancellation support.

csharp
using System.Threading.Channels;
using System.Runtime.CompilerServices;

// Channel<T> — async producer/consumer pipeline
var ch = Channel.CreateBounded<int>(capacity: 10);

// Producer
async Task ProduceAsync() {
    for (int i = 0; i < 100; i++) {
        await ch.Writer.WriteAsync(i);
    }
    ch.Writer.Complete();
}

// Consumer
async Task ConsumeAsync() {
    await foreach (int item in ch.Reader.ReadAllAsync()) {
        Console.WriteLine(item);
    }
}

await Task.WhenAll(ProduceAsync(), ConsumeAsync());

// IAsyncEnumerable<T> — async iteration (C# 8)
async IAsyncEnumerable<int> StreamNumbers(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    for (int i = 0; i < 10; i++) {
        await Task.Delay(100, ct);
        yield return i;
    }
}

await foreach (int n in StreamNumbers().WithCancellation(CancellationToken.None)) {
    Console.WriteLine(n);
}

// Unbounded channel (drop-in for high-throughput)
var unbounded = Channel.CreateUnbounded<string>();

Expression Trees

Expression trees represent code as inspectable data structures — instead of compiling to IL, the lambda is stored as an AST. LINQ providers (Entity Framework, LINQ-to-SQL) use this to translate C# expressions into SQL queries at runtime.

csharp
using System.Linq.Expressions;

// Expression tree — code as data, inspectable at runtime
// The lambda (x => x * x) compiles to an expression tree, not IL
Expression<Func<int, int>> expr = x => x * x;

// Inspect the tree
var body     = (BinaryExpression)expr.Body;
var left     = (ParameterExpression)body.Left;
Console.WriteLine(body.NodeType);   // Multiply
Console.WriteLine(left.Name);       // x

// Compile and invoke
Func<int, int> fn = expr.Compile();
Console.WriteLine(fn(5));           // 25

// Build an expression tree manually
ParameterExpression param = Expression.Parameter(typeof(int), 'n');
Expression body2 = Expression.Add(param, Expression.Constant(1));
var lambda = Expression.Lambda<Func<int, int>>(body2, param);
Console.WriteLine(lambda.Compile()(10));  // 11

// Practical use: build a dynamic WHERE predicate for LINQ-to-SQL/EF
Expression<Func<Product, bool>> BuildFilter(string field, string value) {
    var param = Expression.Parameter(typeof(Product), 'p');
    var member = Expression.Property(param, field);
    var constant = Expression.Constant(value);
    var equals = Expression.Equal(member, constant);
    return Expression.Lambda<Func<Product, bool>>(equals, param);
}
// dbContext.Products.Where(BuildFilter('Name', 'Widget')).ToList()

Generic Variance

Covariance (out) lets you use IEnumerable<Derived> where IEnumerable<Base> is expected. Contravariance (in) goes the other way. Both are declared on the interface/delegate type parameter.

csharp
// Covariance (out) — can use a more derived type as the type argument
// IEnumerable<out T> — safe because T only comes OUT (returned)
IEnumerable<string> strings = new List<string> { 'a', 'b' };
IEnumerable<object> objects = strings;   // OK — covariant

// Contravariance (in) — can use a less derived type as the type argument
// IComparer<in T> — safe because T only goes IN (consumed)
IComparer<object> objCmp = Comparer<object>.Default;
IComparer<string> strCmp = objCmp;   // OK — contravariant

// Invariant — not co- or contravariant (e.g. IList<T>)
// IList<object> list = new List<string>();  // compile error

// Defining a covariant interface
interface IProducer<out T> {
    T Produce();   // T only in output position
}

// Defining a contravariant interface
interface IConsumer<in T> {
    void Consume(T item);   // T only in input position
}

// Generic constraints
void Process<T>(T value) where T : IComparable<T>, new() { }
T Max<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) > 0 ? a : b;

// Unconstrained generic utilities
T Identity<T>(T x) => x;
T[] Repeat<T>(T value, int count) => Enumerable.Repeat(value, count).ToArray();

P/Invoke & Native Interop

P/Invoke lets C# call functions in native shared libraries. Use [LibraryImport] (C# 11) over the older [DllImport] for source-generated, AOT-compatible bindings. StructLayout controls struct memory layout for wire protocols.

csharp
using System.Runtime.InteropServices;

// P/Invoke — call native C functions from C#
internal static partial class NativeMethods {
    // Classic P/Invoke
    [DllImport('user32.dll', CharSet = CharSet.Unicode)]
    public static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);

    // LibraryImport (C# 11 source-generated, preferred over DllImport)
    [LibraryImport('libc', EntryPoint = 'getpid')]
    public static partial int GetPid();

    [LibraryImport('libm', EntryPoint = 'sqrt')]
    public static partial double Sqrt(double x);
}

// Struct layout — control memory representation for interop
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct PacketHeader {
    public byte  Type;
    public short Length;
    public int   Checksum;
}

// Marshal — convert between managed and unmanaged memory
string managed = Marshal.PtrToStringAnsi(ptr);
IntPtr unmanaged = Marshal.StringToHGlobalAnsi('hello');
Marshal.FreeHGlobal(unmanaged);

// GCHandle — pin a managed object so unmanaged code can hold a pointer
byte[] buffer = new byte[1024];
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
IntPtr addr = handle.AddrOfPinnedObject();
// pass addr to native code...
handle.Free();   // MUST free when done

Primary Constructors (C# 12)

Primary constructors let you declare constructor parameters directly on the class or struct declaration. The parameters are in scope throughout the entire type body — field initializers, methods, and properties can all reference them, eliminating boilerplate assignment code and making dependency injection feel natural.

csharp
// C# 12 primary constructors — parameters available throughout the class body
public class Logger(string name, LogLevel minLevel) {
    // Parameters are in scope for field initializers and all methods
    private readonly string _prefix = 
#39;[{name}]'; public void Log(LogLevel level, string msg) { if (level >= minLevel) Console.WriteLine(
#39;{_prefix} {level}: {msg}'); } } // Inheritance — pass primary constructor args to base public class ConsoleLogger(string name, LogLevel minLevel, bool useColor) : Logger(name, minLevel) { public void Warn(string msg) { if (useColor) Console.ForegroundColor = ConsoleColor.Yellow; Log(LogLevel.Warning, msg); if (useColor) Console.ResetColor(); } } // Structs also support primary constructors public struct Vector2(double x, double y) { public double X { get; } = x; public double Y { get; } = y; public double Length => Math.Sqrt(X * X + Y * Y); public Vector2 Normalized => new(X / Length, Y / Length); public override string ToString() =>
#39;({X:F2}, {Y:F2})'; } // Records already had primary constructors; now classes/structs do too public class ServiceClient(HttpClient http, string baseUrl, ILogger logger) { public async Task<string> GetAsync(string path) { logger.LogInformation('GET {Url}', baseUrl + path); return await http.GetStringAsync(baseUrl + path); } public async Task PostAsync<T>(string path, T body) { logger.LogInformation('POST {Url}', baseUrl + path); await http.PostAsJsonAsync(baseUrl + path, body); } } // DI-friendly — constructor params naturally map to injected services public class OrderService( IOrderRepository repo, IEventBus events, ILogger<OrderService> log) { public async Task<Order> CreateAsync(CreateOrderRequest req) { var order = new Order(req.CustomerId, req.Items); await repo.SaveAsync(order); await events.PublishAsync(new OrderCreated(order.Id)); log.LogInformation('Order {Id} created', order.Id); return order; } }

Collection Expressions (C# 12)

Collection expressions provide a single, unified [...] syntax for creating arrays, lists, spans, and any collection type with a compatible initializer. The spread operator .. inlines another collection inline, and the compiler chooses the most efficient representation based on the target type.

csharp
// C# 12 collection expressions — uniform syntax for any collection type
// Arrays
int[] squares = [1, 4, 9, 16, 25];

// List<T>
List<string> names = ['Alice', 'Bob', 'Carol'];

// Span<T> / ReadOnlySpan<T> — stack allocation when possible
Span<byte> flags = [0x01, 0x02, 0x04, 0x08];

// Spread operator (..) — inline another collection
int[] first  = [1, 2, 3];
int[] second = [4, 5, 6];
int[] all    = [..first, ..second, 7, 8];   // [1,2,3,4,5,6,7,8]

// Works with any type that has a collection initializer
HashSet<int> set = [1, 2, 3, 2, 1];   // {1, 2, 3}
ImmutableArray<string> immutable = ['x', 'y', 'z'];

// Spread in method arguments
void PrintAll(params IEnumerable<int> nums) {
    foreach (var n in nums) Console.Write(n + ' ');
}
int[] extra = [10, 20];
PrintAll([1, 2, ..extra, 30]);   // 1 2 10 20 30

// Dictionary expressions (C# 12)
Dictionary<string, int> scores = new() {
    ['Alice'] = 95,
    ['Bob']   = 87,
};

// Useful for default / empty collection constants
static readonly int[] Empty = [];

// Target-typed — the compiler infers the collection type from context
IReadOnlyList<string> colors = ['red', 'green', 'blue'];
ReadOnlySpan<char> vowels    = ['a', 'e', 'i', 'o', 'u'];

// Nested collections
int[][] matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];

Raw String Literals

Raw string literals (C# 11) are delimited by three or more double-quote characters and require no escape sequences — backslashes, quotes, and curly braces are all literal. Leading whitespace matching the closing delimiter is automatically stripped, keeping embedded JSON, SQL, HTML, and regex patterns readable without noise.

csharp
// Raw string literals (C# 11+) — delimited by 3+ double quotes
// No escape sequences: backslash, quotes, braces are literal
string path = """C:\Users\Alice\Documents\file.txt""";
string json = """
    {
        'name': 'Alice',
        'scores': [95, 87, 92]
    }
    """;

// Interpolation inside raw strings — prefix with $
string name = 'Alice';
int age  = 30;
string html = 
quot;"" <div class='user'> <span>{name}</span> <span>{age}</span> </div> """; // Multiple $ signs to use {{ }} as literal braces without escaping string template = $""" SELECT * FROM {{tableName}} WHERE id = {{id}}; """; // Indentation: leading whitespace equal to the closing quotes is stripped // The result is properly de-indented — no leading spaces in the output string xml = """ <root> <child attr='value'>text</child> </root> """; // Embed quotes freely — no escaping needed string sql = """ SELECT 'literal string', "quoted identifier" FROM schema.'table name' WHERE name = 'O''Brien' """; // Useful for regex patterns — no double-escaping needed var pattern = new System.Text.RegularExpressions.Regex("""d{4}-d{2}-d{2}"""); bool ok = pattern.IsMatch('2024-07-04'); // true // Single-line raw strings string greeting = """Hello, "World"!""";

Advanced LINQ

Beyond basic filtering and projection, LINQ offers powerful set operations, multi-level grouping, joins between heterogeneous sequences, and C# 6+ additions like MinBy, MaxBy, DistinctBy, and Chunk. ILookup<K,V> is a read-only multi-valued dictionary built in one pass.

csharp
using System.Linq;

record Product(string Name, string Category, decimal Price, int Stock);

var products = new List<Product> {
    new('Widget', 'Tools',    9.99m,  100),
    new('Gadget', 'Tech',    49.99m,   30),
    new('Doohickey', 'Tools',  4.99m, 200),
    new('Thingamajig', 'Tech', 99.99m,  10),
};

// GroupBy + aggregate per group
var summary = products
    .GroupBy(p => p.Category)
    .Select(g => new {
        Category = g.Key,
        Count    = g.Count(),
        Total    = g.Sum(p => p.Price),
        AvgPrice = g.Average(p => p.Price),
        MaxPrice = g.Max(p => p.Price),
    })
    .OrderByDescending(x => x.Total);

// Join two sequences
var orders = new[] { new { ProductName = 'Widget', Qty = 3 } };
var detailed = orders.Join(
    products,
    o => o.ProductName,
    p => p.Name,
    (o, p) => new { o.ProductName, o.Qty, p.Price, Total = o.Qty * p.Price });

// SelectMany — flatten nested sequences
var tags = new[] {
    new { Name = 'A', Tags = new[] { 'x', 'y' } },
    new { Name = 'B', Tags = new[] { 'y', 'z' } },
};
var allTags = tags.SelectMany(t => t.Tags, (t, tag) => (t.Name, tag));

// Zip — combine two sequences element-by-element
var names  = new[] { 'Alice', 'Bob', 'Carol' };
var scores = new[] { 95, 87, 92 };
var paired = names.Zip(scores, (n, s) => 
#39;{n}: {s}'); // Chunk (C# 6+) — split into fixed-size pages var pages = products.Chunk(size: 2); // Lookup — multi-valued dictionary from GroupBy ILookup<string, Product> byCategory = products.ToLookup(p => p.Category); foreach (var p in byCategory['Tools']) Console.WriteLine(p.Name); // DistinctBy / MinBy / MaxBy (C# 6+) var cheapest = products.MinBy(p => p.Price); var mostStock = products.MaxBy(p => p.Stock); var categories = products.DistinctBy(p => p.Category);

Memory & GC

The .NET GC is generational and largely automatic, but high-throughput code benefits from reducing allocations via ArrayPool<T>, the dispose pattern for deterministic cleanup, WeakReference<T> for cache-friendly handles, and MemoryMarshal for zero-copy reinterpretation of raw bytes.

csharp
using System;
using System.Runtime;
using System.Buffers;

// IDisposable pattern — deterministic cleanup of unmanaged resources
public class FileProcessor : IDisposable {
    private FileStream? _stream;
    private bool _disposed;

    public FileProcessor(string path) => _stream = File.OpenRead(path);

    public int Read(byte[] buf) {
        ObjectDisposedException.ThrowIf(_disposed, this);
        return _stream!.Read(buf);
    }

    protected virtual void Dispose(bool disposing) {
        if (_disposed) return;
        if (disposing) _stream?.Dispose();   // managed resources
        _disposed = true;
    }

    public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
    ~FileProcessor() => Dispose(false);    // finalizer as safety net
}

// using declaration — dispose at end of enclosing scope
using var fp = new FileProcessor('data.bin');

// ArrayPool — avoid allocating large short-lived arrays
var pool = ArrayPool<byte>.Shared;
byte[] buf = pool.Rent(4096);
try {
    int read = fp.Read(buf);
    Process(buf.AsSpan(0, read));
} finally {
    pool.Return(buf, clearArray: false);
}

// GC.Collect — rarely needed; useful in benchmarks
GC.Collect(2, GCCollectionMode.Forced, blocking: true);
GC.WaitForPendingFinalizers();

// WeakReference — let GC collect if memory is needed
var cache = new WeakReference<byte[]>(new byte[1024 * 1024]);
if (!cache.TryGetTarget(out var data))
    data = LoadData();

// GCSettings — server vs workstation GC
Console.WriteLine(GCSettings.IsServerGC);        // true in ASP.NET
Console.WriteLine(GCSettings.LatencyMode);       // default: Interactive

// MemoryMarshal — reinterpret bytes without copy
Span<byte> raw = stackalloc byte[8];
MemoryMarshal.Write(raw, 3.14);
double back = MemoryMarshal.Read<double>(raw);   // 3.14

Threading Primitives

The BCL offers a layered threading toolkit: Interlocked for lock-free atomic ops, SemaphoreSlim for async-compatible throttling, ReaderWriterLockSlim for read-heavy workloads, ConcurrentQueue for thread-safe FIFO, and Parallel.ForEachAsync for bounded async fan-out.

csharp
using System.Threading;
using System.Collections.Concurrent;

// Mutex — system-wide exclusive lock (cross-process capable)
using var mutex = new Mutex(false, 'Global\\MyAppSingleInstance');
if (!mutex.WaitOne(0)) { Console.WriteLine('Already running'); return; }

// SemaphoreSlim — async-friendly, limits concurrency
var sem = new SemaphoreSlim(3);   // max 3 concurrent
async Task ThrottledWorkAsync() {
    await sem.WaitAsync();
    try { await DoWorkAsync(); }
    finally { sem.Release(); }
}

// ReaderWriterLockSlim — multiple readers OR one writer
var rwLock = new ReaderWriterLockSlim();
string _sharedData = '';
string Read() {
    rwLock.EnterReadLock();
    try { return _sharedData; }
    finally { rwLock.ExitReadLock(); }
}
void Write(string val) {
    rwLock.EnterWriteLock();
    try { _sharedData = val; }
    finally { rwLock.ExitWriteLock(); }
}

// Interlocked — atomic operations without locks
int _counter = 0;
Interlocked.Increment(ref _counter);
Interlocked.Add(ref _counter, 5);
int old = Interlocked.Exchange(ref _counter, 0);   // reset, return old
Interlocked.CompareExchange(ref _counter, 10, 0);  // set to 10 only if 0

// ConcurrentQueue — thread-safe FIFO
var queue = new ConcurrentQueue<int>();
queue.Enqueue(1);
if (queue.TryDequeue(out int item)) Console.WriteLine(item);

// Parallel.ForEachAsync (C# 6+) — bounded async parallelism
await Parallel.ForEachAsync(
    Enumerable.Range(0, 100),
    new ParallelOptions { MaxDegreeOfParallelism = 4 },
    async (i, ct) => await ProcessItemAsync(i, ct));

// ManualResetEventSlim — signal one or many threads
var ready = new ManualResetEventSlim(false);
Task.Run(() => { Thread.Sleep(500); ready.Set(); });
ready.Wait();
Console.WriteLine('Signal received');

Indexers & Ranges

System.Index and System.Range generalize slicing beyond arrays — any type with an indexer and a Length/Count property supports ^n (from-end) and start..end syntax. Custom types can opt in by adding compatible indexers.

csharp
// Indexers — allow [] access on custom types
public class Grid<T> {
    private readonly T[,] _data;
    public int Rows { get; }
    public int Cols { get; }

    public Grid(int rows, int cols) {
        Rows = rows; Cols = cols;
        _data = new T[rows, cols];
    }

    // 2D indexer
    public T this[int row, int col] {
        get => _data[row, col];
        set => _data[row, col] = value;
    }

    // Slicing a row via Index
    public T[] this[int row, Range cols] {
        get {
            var (offset, len) = cols.GetOffsetAndLength(Cols);
            var result = new T[len];
            Array.Copy(_data, row * Cols + offset, result, 0, len);
            return result;
        }
    }
}

// System.Index — ^ means 'from end'
int[] arr = { 10, 20, 30, 40, 50 };
int last      = arr[^1];   // 50
int secondLast = arr[^2];  // 40

// System.Range — start..end (end exclusive)
int[] middle  = arr[1..4];    // [20, 30, 40]
int[] fromTwo = arr[2..];     // [30, 40, 50]
int[] toThree = arr[..3];     // [10, 20, 30]
int[] copy    = arr[..];      // full copy

// Ranges on strings — returns string (not span)
string s   = 'Hello, World!';
string sub = s[7..12];   // 'World'
string end = s[^6..];    // 'orld!'

// Ranges with Span<T> — zero copy slice
Span<int> span  = arr.AsSpan();
Span<int> slice = span[1..^1];   // [20, 30, 40]

// Index / Range as variables
Index  fromEnd = ^1;
Range  mid     = 1..4;
var    midArr  = arr[mid];   // [20, 30, 40]

// GetOffsetAndLength — compute slice bounds manually
(int offset, int length) = mid.GetOffsetAndLength(arr.Length);

Nullable Reference Types

Enable <Nullable>enable</Nullable> in your project to get flow-sensitive null analysis. The compiler tracks nullability through conditionals, pattern matches, and method contracts expressed via attributes like [NotNullWhen], turning null-dereference bugs into compile-time warnings.

csharp
// Enable nullable analysis: <Nullable>enable</Nullable> in .csproj
// Compiler now distinguishes string (never null) from string? (maybe null)

// Non-nullable — guaranteed non-null, no null-check warnings
string name = 'Alice';

// Nullable — might be null; compiler warns on unsound dereference
string? middle = null;
Console.WriteLine(middle?.ToUpper() ?? '(none)');

// Null-forgiving operator ! — suppresses warning when you know better
string forced = middle!;   // you promise it's not null here

// Required init property — object initializer must provide a value
public class User {
    public required string Username { get; init; }
    public string? Email { get; init; }
    public string DisplayName => Email ?? Username;
}
var u = new User { Username = 'alice' };   // Email is optional

// Null-conditional chaining — short-circuits on null
int? len = u.Email?.Length;
string? domain = u.Email?.Split('@').LastOrDefault();

// Null-coalescing assignment ??=
u.Email ??= 'noreply@example.com';

// Pattern-matching null check
if (u.Email is { } email) Console.WriteLine(
#39;Email: {email}'); // Guard clauses with ArgumentNullException.ThrowIfNull (C# 10+) public void Send(string to, string body) { ArgumentNullException.ThrowIfNull(to); ArgumentNullException.ThrowIfNull(body); // Compiler knows they are non-null here } // NotNullWhen — annotate TryParse-style methods public bool TryGetUser(int id, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out User? user) { user = _store.TryGetValue(id, out var found) ? found : null; return user is not null; } if (TryGetUser(1, out var result)) { Console.WriteLine(result.Username); // no warning — result is non-null here }

Immutability Patterns

C# offers several immutability tools: records with with expressions for non-destructive mutation, readonly struct for stack-efficient value types, init accessors for post-construction locking, and ImmutableList<T> / ImmutableDictionary<K,V> from System.Collections.Immutable for persistent data structures.

csharp
// Record — value equality, immutable by default, with expression
public record Address(string Street, string City, string Country);
public record Person(string Name, int Age, Address HomeAddress);

var alice = new Person('Alice', 30, new Address('1 Main St', 'Springfield', 'US'));
var moved  = alice with { HomeAddress = alice.HomeAddress with { City = 'Shelbyville' } };
Console.WriteLine(alice == moved);   // false

// Readonly struct — value type, fully immutable, efficient in collections
public readonly struct Money(decimal Amount, string Currency) {
    public decimal Amount   { get; } = Amount;
    public string  Currency { get; } = Currency;
    public Money Add(Money other) {
        if (Currency != other.Currency) throw new InvalidOperationException('Currency mismatch');
        return new Money(Amount + other.Amount, Currency);
    }
    public override string ToString() => 
#39;{Amount:F2} {Currency}'; } // ImmutableList — persistent data structure; mutations return new instances using System.Collections.Immutable; var list = ImmutableList.Create(1, 2, 3); var list2 = list.Add(4).Add(5); // list is unchanged var list3 = list2.Remove(2); // [1, 3, 4, 5] // ImmutableDictionary var dict = ImmutableDictionary<string, int>.Empty; var dict2 = dict.Add('a', 1).Add('b', 2).SetItem('a', 99); // Builder pattern for efficient batch mutations var builder = list.ToBuilder(); for (int i = 0; i < 1000; i++) builder.Add(i); var large = builder.ToImmutable(); // single allocation // Freeze pattern — mutable during construction, then locked public class AppConfig { private readonly Dictionary<string, string> _settings = new(); private bool _frozen; public void Set(string key, string val) { if (_frozen) throw new InvalidOperationException('Config is frozen'); _settings[key] = val; } public void Freeze() => _frozen = true; public string Get(string key) => _settings[key]; } // init accessor — settable only in object initializers or constructors public class Options { public int Timeout { get; init; } = 30; public int Retries { get; init; } = 3; public bool Verbose { get; init; } }

Minimal API Patterns

Minimal APIs (.NET 6+) define HTTP endpoints as lambdas or method groups directly on WebApplication, without controller classes. Route groups share prefixes and middleware. Endpoint filters provide cross-cutting concerns like logging and validation, and built-in Results helpers return typed HTTP responses.

csharp
// .NET 6+ Minimal API — no controllers, no startup class
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();

// Route handlers — lambda or method group
app.MapGet('/products', async (IProductRepository repo) =>
    Results.Ok(await repo.GetAllAsync()));

app.MapGet('/products/{id:int}', async (int id, IProductRepository repo) => {
    var product = await repo.FindAsync(id);
    return product is null
        ? Results.NotFound()
        : Results.Ok(product);
});

app.MapPost('/products', async (CreateProductDto dto, IProductRepository repo) => {
    var product = new Product(dto.Name, dto.Price);
    await repo.AddAsync(product);
    return Results.Created(
#39;/products/{product.Id}', product); }); app.MapPut('/products/{id:int}', async (int id, UpdateProductDto dto, IProductRepository repo) => { var existing = await repo.FindAsync(id); if (existing is null) return Results.NotFound(); existing.Update(dto.Name, dto.Price); await repo.SaveAsync(); return Results.NoContent(); }); app.MapDelete('/products/{id:int}', async (int id, IProductRepository repo) => { await repo.DeleteAsync(id); return Results.NoContent(); }); // Route groups (C# .NET 7+) — shared prefix + middleware var api = app.MapGroup('/api/v1').RequireAuthorization(); api.MapGet('/me', (ClaimsPrincipal user) => Results.Ok(user.Identity?.Name)); // Filters — run before/after each endpoint in the group api.AddEndpointFilter(async (ctx, next) => { Console.WriteLine(
#39;Before: {ctx.HttpContext.Request.Path}'); var result = await next(ctx); Console.WriteLine(
#39;After: {ctx.HttpContext.Response.StatusCode}'); return result; }); app.Run(); record CreateProductDto(string Name, decimal Price); record UpdateProductDto(string Name, decimal Price);

Best Practices

Design & Architecture

csharp
// Design & Architecture Best Practices

// SOLID: Single Responsibility — one reason to change
public class InvoiceRenderer { public string Render(Invoice i) => ...; }
public class InvoiceCalculator { public decimal Total(Invoice i) => ...; }

// SOLID: Open/Closed — extend via new types, not edits
public abstract class Discount { public abstract decimal Apply(decimal price); }
public class PercentDiscount(double pct) : Discount {
    public override decimal Apply(decimal price) => price * (decimal)(1 - pct);
}

// Prefer composition over inheritance
public class NotifyingRepository(IRepository inner, IEventBus bus) : IRepository {
    public async Task SaveAsync(Order o) { await inner.SaveAsync(o); await bus.PublishAsync(new OrderSaved(o.Id)); }
}

// Interfaces for testability
public interface ITimeProvider { DateTime UtcNow { get; } }
public class SystemTimeProvider : ITimeProvider { public DateTime UtcNow => DateTime.UtcNow; }

// Records for DTOs — structural equality, immutable, concise
public record CreateUserRequest(string Email, string Password, string DisplayName);

// Sealed classes for leaf types — no unintended subclassing
public sealed class EmailAddress {
    public string Value { get; }
    public EmailAddress(string v) { Value = v.Contains('@') ? v : throw new ArgumentException('Invalid email'); }
}

// internal over public by default — least privilege visibility
internal class OrderValidator { internal bool IsValid(Order o) => o.Items.Any(); }

// File-scoped namespaces — reduces indentation, one per file
namespace MyApp.Orders;

// Expression-bodied members for simple getters/computations
public class Circle(double radius) {
    public double Radius => radius;
    public double Area   => Math.PI * radius * radius;
    public double Circumference => 2 * Math.PI * radius;
}

Null Safety & Defensive Coding

csharp
// Null Safety & Defensive Coding Best Practices

// Enable in .csproj: <Nullable>enable</Nullable>
#nullable enable

// Use ArgumentNullException.ThrowIfNull (C# 10+) at method entry
public void Process(Order order, ILogger logger) {
    ArgumentNullException.ThrowIfNull(order);
    ArgumentNullException.ThrowIfNull(logger);
    // Compiler knows both are non-null here
}

// required init for DTOs — object initializer must supply these
public class UserDto {
    public required string Id   { get; init; }
    public required string Name { get; init; }
    public string? Email        { get; init; }   // optional
}

// ?. and ?? operators — safe chaining
string? raw = GetInput();
int len = raw?.Trim().Length ?? 0;
string display = raw?.ToUpper() ?? '(empty)';

// Pattern matching over null checks
if (raw is { Length: > 0 } trimmed) Console.WriteLine(trimmed);

// is not null — clearest null-guard
if (raw is not null) Console.WriteLine(raw.Length);

// Guard clauses at method entry — fail fast
public decimal Divide(decimal a, decimal b) {
    if (b == 0) throw new ArgumentException('Divisor cannot be zero', nameof(b));
    return a / b;
}

// Prefer empty collections over null — never make callers null-check
public IReadOnlyList<Order> GetOrders() => _orders ?? [];

// Null-coalescing assignment ??= — lazy init
_cache ??= new Dictionary<string, string>();

Async Best Practices

csharp
// Async Best Practices

// Always await — never fire-and-forget silently
public async Task SaveAsync(Order order) {
    await _repo.SaveAsync(order);          // awaited
    await _bus.PublishAsync(new OrderSaved(order.Id));
}

// Never async void — use async Task; except for event handlers
private async void OnButtonClick(object sender, EventArgs e) {   // OK: event handler
    await DoWorkAsync();
}

// CancellationToken everywhere — propagate, never ignore
public async Task<IReadOnlyList<Product>> SearchAsync(
    string query, CancellationToken ct = default) {
    return await _db.Products
        .Where(p => p.Name.Contains(query))
        .ToListAsync(ct);
}

// ConfigureAwait(false) in library code — avoids deadlocks, improves perf
public async Task<byte[]> ReadAllBytesAsync(string path, CancellationToken ct = default) {
    await using var stream = File.OpenRead(path);
    using var ms = new MemoryStream();
    await stream.CopyToAsync(ms, ct).ConfigureAwait(false);
    return ms.ToArray();
}

// Avoid blocking on async — never .Result or .Wait() from sync context
// BAD: var data = GetDataAsync().Result;
// GOOD: await GetDataAsync()

// ValueTask for hot paths that often complete synchronously
public ValueTask<int> GetCachedCountAsync() {
    if (_cache.TryGetValue('count', out int n)) return new ValueTask<int>(n);
    return new ValueTask<int>(LoadCountAsync());
}

// Task.WhenAll for parallel independent work
public async Task<(User, IList<Order>)> LoadDashboardAsync(int userId, CancellationToken ct) {
    var userTask   = _users.FindAsync(userId, ct);
    var ordersTask = _orders.GetByUserAsync(userId, ct);
    await Task.WhenAll(userTask, ordersTask);
    return (await userTask, await ordersTask);
}

LINQ & Collections

csharp
// LINQ & Collections Best Practices

// Prefer LINQ for clarity over manual loops
var expensiveTools = products
    .Where(p => p.Category == 'Tools' && p.Price > 50)
    .OrderBy(p => p.Name)
    .Select(p => p.Name)
    .ToList();

// Avoid multiple enumeration — materialize IEnumerable once
IEnumerable<Order> orders = GetOrders();
var list = orders.ToList();           // single enumeration
var count = list.Count;
var first = list.FirstOrDefault();

// ToList() / ToArray() to materialize and avoid deferred execution surprises
var names = _db.Users.Where(u => u.IsActive).Select(u => u.Name).ToArray();

// HashSet for O(1) membership tests
var allowedIds = new HashSet<int> { 1, 2, 3, 42 };
var filtered   = items.Where(i => allowedIds.Contains(i.Id)).ToList();

// Dictionary for O(1) lookups
var byId = products.ToDictionary(p => p.Id);
if (byId.TryGetValue(targetId, out var product)) Console.WriteLine(product.Name);

// ImmutableArray for read-only data shared across threads
public static readonly ImmutableArray<string> AllowedRoles =
    ['admin', 'editor', 'viewer'];

// IEnumerable in signatures when you only iterate; List/IReadOnlyList when count needed
public IEnumerable<string> GetNames() => _items.Select(i => i.Name);
public IReadOnlyList<Order> GetOrders() => _orders.AsReadOnly();

// Avoid LINQ in hot paths — manual loops are faster for tight inner loops
for (int i = 0; i < buffer.Length; i++) {
    if (buffer[i] == 0) count++;
}

Exception Handling

csharp
// Exception Handling Best Practices

// Only catch what you can actually handle
try {
    var data = await _client.GetStringAsync(url, ct);
    return JsonSerializer.Deserialize<ApiResponse>(data);
} catch (HttpRequestException ex) {
    _logger.LogWarning(ex, 'HTTP request to {Url} failed', url);
    return null;   // caller handles null — we handled what we could
}

// Use specific exception types, not base Exception
public class OrderNotFoundException : Exception {
    public int OrderId { get; }
    public OrderNotFoundException(int id)
        : base(
#39;Order {id} not found') => OrderId = id; public OrderNotFoundException(int id, Exception inner) : base(
#39;Order {id} not found', inner) => OrderId = id; protected OrderNotFoundException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext ctx) : base(info, ctx) { } } // Never swallow exceptions silently // BAD: catch (Exception) { } // GOOD: log, rethrow, or convert to domain error // Log before rethrowing or converting try { await _db.SaveChangesAsync(ct); } catch (DbUpdateException ex) { _logger.LogError(ex, 'Failed to persist order {Id}', order.Id); throw new OrderPersistenceException('Could not save order', ex); } // when clause — filter without catching try { await ProcessAsync(); } catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests) { await Task.Delay(TimeSpan.FromSeconds(5), ct); await ProcessAsync(); } // ExceptionDispatchInfo — rethrow without losing original stack trace ExceptionDispatchInfo? captured = null; try { await RiskyAsync(); } catch (Exception ex) { captured = ExceptionDispatchInfo.Capture(ex); } if (captured is not null) captured.Throw();

Performance & Memory

csharp
// Performance & Memory Best Practices

// Span<T> / Memory<T> for buffer work — zero allocation slicing
public int CountNewlines(ReadOnlySpan<char> text) {
    int count = 0;
    foreach (char c in text) if (c == '
') count++;
    return count;
}
CountNewlines(largeString.AsSpan(offset, length));   // no allocation

// readonly struct for small value types — no defensive copies, no heap alloc
public readonly struct Color(byte r, byte g, byte b) {
    public byte R { get; } = r;
    public byte G { get; } = g;
    public byte B { get; } = b;
    public int ToArgb() => (R << 16) | (G << 8) | B;
}

// ArrayPool — reuse temp arrays, avoid GC pressure
var pool = System.Buffers.ArrayPool<byte>.Shared;
byte[] buf = pool.Rent(4096);
try { /* use buf */ }
finally { pool.Return(buf); }

// StringBuilder for string concatenation in loops
var sb = new System.Text.StringBuilder();
foreach (var line in lines) sb.AppendLine(line);
string result = sb.ToString();

// StringComparison.Ordinal for perf-sensitive string ops
if (string.Equals(a, b, StringComparison.Ordinal)) { }
int idx = text.IndexOf('prefix', StringComparison.Ordinal);

// Avoid boxing value types — use generics instead of object
// BAD: object boxed = 42;  IEnumerable<object> ints = new[] { (object)1, (object)2 };
// GOOD:
void Print<T>(T value) where T : struct => Console.WriteLine(value);

// Minimize allocations in hot paths — measure with BenchmarkDotNet
// [MemoryDiagnoser] on benchmark class shows Gen0/Gen1/Gen2 allocs

// dotMemory / PerfView / ETW for production profiling
// BenchmarkDotNet for micro-benchmarks — never guess, always measure