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
- C# documentation — Microsoft's official language docs
- dotnet.microsoft.com — .NET platform hub, downloads, and roadmap
- .NET API Browser — searchable .NET class library reference
- NuGet — the .NET package registry
- .NET Fiddle — run C# snippets in the browser
Topics
Variables & Types
// 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
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
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
// 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
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
// 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
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
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
// 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
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 hereDelegates & Events
// 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); // 10Extension Methods
// 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>
// 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
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
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] startedSource Generators & Modern C#
// 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.
// 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.
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.
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.
// 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.
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 donePrimary 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.
// 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.
// 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.
// 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.
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.
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.14Threading 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.
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.
// 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.
// 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.
// 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.
// .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
// 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
// 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
// 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
// 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
// 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
// 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