← Back to all guides

C# TLDR

A rapid reference guide to the C# programming language. Everything you need to know, distilled.

C# 13 / .NET 9 — November 2024

What is C#?

C# is a modern, object-oriented programming language developed by Microsoft. It runs on the .NET platform and is used for building web apps, desktop apps, games (Unity), cloud services, and more.

Type-Safe

Strong static typing catches errors at compile time. Nullable reference types prevent null reference exceptions.

Cross-Platform

.NET runs on Windows, macOS, and Linux. Build once, deploy anywhere.

Modern Features

Pattern matching, records, async/await, LINQ, and more. C# evolves rapidly with yearly releases.

Rich Ecosystem

NuGet packages, Visual Studio/VS Code tooling, and extensive standard library.

Basics

Hello World

// Minimal (C# 9+ top-level statements)
Console.WriteLine("Hello, World!");

// Traditional
namespace MyApp;

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

Variables

// Explicit typing
int age = 25;
string name = "Alice";
bool isActive = true;

// Type inference with var
var count = 42;           // int
var message = "Hello";    // string

// Constants
const double Pi = 3.14159;

// Nullable types
int? maybeNull = null;
string? nullableString = null;

Methods

// Basic method
int Add(int a, int b)
{
    return a + b;
}

// Expression-bodied method
int Multiply(int a, int b) => a * b;

// Default parameters
void Greet(string name = "World") => Console.WriteLine($"Hello, {name}!");

// Named arguments
Greet(name: "Alice");

// Out parameters
bool TryParse(string s, out int result) { ... }
if (int.TryParse("42", out var value)) { ... }

// Ref parameters (pass by reference)
void Swap(ref int a, ref int b) => (a, b) = (b, a);

String Interpolation

var name = "Alice";
var age = 30;

// Interpolated string
var greeting = $"Hello, {name}! You are {age} years old.";

// Formatting
var price = 19.99m;
Console.WriteLine($"Price: {price:C}");      // Price: $19.99
Console.WriteLine($"Date: {DateTime.Now:yyyy-MM-dd}");

// Raw string literals (C# 11+)
var json = """
    {
        "name": "Alice",
        "age": 30
    }
    """;

Types

Value Types

Category Types Notes
Integers byte, short, int, long 8, 16, 32, 64-bit signed
Unsigned sbyte, ushort, uint, ulong Unsigned variants
Floating float, double, decimal 32, 64-bit; decimal for money
Boolean bool true or false
Character char Unicode character
Struct struct Custom value types
Enum enum Named constants

Reference Types

// String (immutable)
string s = "Hello";

// Object (base of all types)
object obj = 42;        // boxing
int n = (int)obj;       // unboxing

// Dynamic (runtime typing)
dynamic dyn = "Hello";
dyn = 42;               // OK at runtime

// Arrays
int[] numbers = { 1, 2, 3 };
int[] zeros = new int[10];
int[,] matrix = new int[3, 3];  // 2D array

Structs

public struct Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public Point(double x, double y) => (X, Y) = (x, y);

    public double Distance(Point other) =>
        Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
}

var p = new Point(3, 4);

Enums

public enum Status
{
    Pending,      // 0
    Active,       // 1
    Completed,    // 2
    Cancelled     // 3
}

// With explicit values
public enum HttpStatus
{
    OK = 200,
    NotFound = 404,
    ServerError = 500
}

// Flags enum
[Flags]
public enum Permissions
{
    None = 0,
    Read = 1,
    Write = 2,
    Execute = 4,
    All = Read | Write | Execute
}

var perms = Permissions.Read | Permissions.Write;

Tuples

// Unnamed tuple
(int, string) pair = (1, "one");
Console.WriteLine(pair.Item1);  // 1

// Named tuple
(int Id, string Name) person = (1, "Alice");
Console.WriteLine(person.Name);  // Alice

// Return multiple values
(int Min, int Max) GetRange(int[] arr) => (arr.Min(), arr.Max());

// Deconstruction
var (min, max) = GetRange(numbers);

Control Flow

If / Else

if (age >= 18)
{
    Console.WriteLine("Adult");
}
else if (age >= 13)
{
    Console.WriteLine("Teenager");
}
else
{
    Console.WriteLine("Child");
}

// Ternary operator
var status = age >= 18 ? "Adult" : "Minor";

// Null coalescing
var value = nullableValue ?? "default";
nullableValue ??= "assign if null";

Switch

// Statement
switch (day)
{
    case "Monday":
    case "Tuesday":
        Console.WriteLine("Weekday");
        break;
    case "Saturday":
    case "Sunday":
        Console.WriteLine("Weekend");
        break;
    default:
        Console.WriteLine("Unknown");
        break;
}

// Expression (C# 8+)
var message = day switch
{
    "Monday" or "Tuesday" => "Weekday",
    "Saturday" or "Sunday" => "Weekend",
    _ => "Unknown"
};

Loops

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

// Foreach
foreach (var item in collection)
{
    Console.WriteLine(item);
}

// While
while (condition)
{
    // ...
}

// Do-while
do
{
    // executes at least once
} while (condition);

// Break and continue
for (int i = 0; i < 100; i++)
{
    if (i == 50) break;     // exit loop
    if (i % 2 == 0) continue; // skip even
    Console.WriteLine(i);
}

Object-Oriented Programming

Classes

public class Person
{
    // Fields
    private string _name;

    // Properties
    public string Name
    {
        get => _name;
        set => _name = value ?? throw new ArgumentNullException();
    }

    // Auto-property
    public int Age { get; set; }

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

    // Init-only property (C# 9+)
    public string Id { get; init; }

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

    // Method
    public void Greet() => Console.WriteLine($"Hi, I'm {Name}");
}

// Instantiation
var person = new Person("Alice", 30);
var person2 = new Person("Bob", 25) { Id = "123" };

Primary Constructors (C# 12+)

// Parameters available throughout the class
public class Person(string name, int age)
{
    public string Name => name;
    public int Age => age;
    public bool IsAdult => age >= 18;
}

Inheritance

public class Animal
{
    public string Name { get; set; }
    public virtual void Speak() => Console.WriteLine("...");
}

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

    // Call base method
    public void SpeakQuietly()
    {
        base.Speak();
    }
}

// Sealed prevents inheritance
public sealed class FinalClass { }

Interfaces

public interface IShape
{
    double Area { get; }
    double Perimeter();

    // Default implementation (C# 8+)
    void Print() => Console.WriteLine($"Area: {Area}");
}

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

Abstract Classes

public abstract class Shape
{
    public abstract double Area { get; }

    // Can have concrete methods too
    public void PrintArea() => Console.WriteLine($"Area: {Area}");
}

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

Records (C# 9+)

// Immutable reference type with value semantics
public record Person(string Name, int Age);

var alice = new Person("Alice", 30);
var bob = alice with { Name = "Bob" };  // Non-destructive mutation

// Value equality
var alice2 = new Person("Alice", 30);
Console.WriteLine(alice == alice2);  // True

// Record struct (C# 10+)
public record struct Point(double X, double Y);

Collections

Arrays

// Declaration and initialization
int[] numbers = { 1, 2, 3, 4, 5 };
int[] zeros = new int[10];
string[] names = new[] { "Alice", "Bob" };

// Access
var first = numbers[0];
var last = numbers[^1];        // Index from end
var slice = numbers[1..4];    // Range: {2, 3, 4}

// 2D array
int[,] matrix = new int[3, 3];
matrix[0, 0] = 1;

List<T>

var list = new List<string> { "Alice", "Bob" };

// Collection expression (C# 12+)
List<int> nums = [1, 2, 3, 4, 5];

// Operations
list.Add("Charlie");
list.AddRange(["Dave", "Eve"]);
list.Remove("Bob");
list.RemoveAt(0);
list.Insert(0, "First");

// Query
var contains = list.Contains("Alice");
var index = list.IndexOf("Charlie");
var count = list.Count;

Dictionary<K, V>

var dict = new Dictionary<string, int>
{
    ["Alice"] = 30,
    ["Bob"] = 25
};

// Add/update
dict["Charlie"] = 35;
dict.TryAdd("Dave", 28);  // Only if not exists

// Access
if (dict.TryGetValue("Alice", out var age))
{
    Console.WriteLine(age);
}

// Iterate
foreach (var (key, value) in dict)
{
    Console.WriteLine($"{key}: {value}");
}

Other Collections

// HashSet - unique elements
var set = new HashSet<int> { 1, 2, 3 };
set.Add(3);  // Ignored, already exists

// Queue - FIFO
var queue = new Queue<string>();
queue.Enqueue("first");
var item = queue.Dequeue();

// Stack - LIFO
var stack = new Stack<int>();
stack.Push(1);
var top = stack.Pop();

// SortedDictionary, SortedSet - ordered
var sorted = new SortedDictionary<string, int>();

Span<T> and Memory<T>

// Stack-allocated, no heap allocation
Span<int> span = stackalloc int[] { 1, 2, 3 };

// Slice of array without copying
int[] arr = { 1, 2, 3, 4, 5 };
Span<int> slice = arr.AsSpan(1, 3);  // {2, 3, 4}

// ReadOnlySpan for strings
ReadOnlySpan<char> text = "Hello, World!".AsSpan();
var hello = text[..5];  // "Hello"

LINQ

Language Integrated Query — declarative data querying that works with any IEnumerable.

Query Syntax

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

var evens = from n in numbers
            where n % 2 == 0
            orderby n descending
            select n * 2;

// With grouping
var grouped = from p in people
              group p by p.City into g
              select new { City = g.Key, Count = g.Count() };

Method Syntax

// Filtering
var adults = people.Where(p => p.Age >= 18);

// Projection
var names = people.Select(p => p.Name);

// Sorting
var sorted = people.OrderBy(p => p.Name)
                   .ThenByDescending(p => p.Age);

// Aggregation
var count = numbers.Count();
var sum = numbers.Sum();
var avg = numbers.Average();
var max = numbers.Max();

// First/Last
var first = numbers.First();
var firstOrNull = numbers.FirstOrDefault();
var single = numbers.Single(n => n == 5);

// Any/All
var hasAdults = people.Any(p => p.Age >= 18);
var allAdults = people.All(p => p.Age >= 18);

More LINQ Operations

// Skip/Take
var page = items.Skip(20).Take(10);

// Distinct
var unique = items.Distinct();

// GroupBy
var byCity = people.GroupBy(p => p.City);

// Join
var joined = orders.Join(customers,
    o => o.CustomerId,
    c => c.Id,
    (o, c) => new { Order = o, Customer = c });

// Flatten nested collections
var allOrders = customers.SelectMany(c => c.Orders);

// ToList/ToArray/ToDictionary
var list = query.ToList();
var dict = people.ToDictionary(p => p.Id);

// Chunk (C# 13)
var batches = items.Chunk(100);  // IEnumerable of 100-item arrays
LINQ uses deferred execution — queries aren't executed until you iterate or call a terminal operation like ToList().

Async/Await

Write asynchronous code that looks synchronous. Non-blocking I/O without callback hell.

Basic Async

// Async method
public async Task<string> GetDataAsync()
{
    using var client = new HttpClient();
    var response = await client.GetStringAsync("https://api.example.com");
    return response;
}

// Async void (only for event handlers!)
private async void Button_Click(object sender, EventArgs e)
{
    await DoSomethingAsync();
}

// ValueTask for hot paths (avoids allocation when sync)
public async ValueTask<int> GetCachedValueAsync()
{
    if (_cache.TryGetValue(key, out var value))
        return value;  // No allocation
    return await FetchFromDbAsync(key);
}

Parallel Operations

// Wait for all
var task1 = GetUserAsync(id1);
var task2 = GetUserAsync(id2);
var task3 = GetUserAsync(id3);

var users = await Task.WhenAll(task1, task2, task3);

// Wait for first to complete
var first = await Task.WhenAny(task1, task2, task3);

// Process as they complete
var tasks = urls.Select(url => FetchAsync(url)).ToList();
while (tasks.Count > 0)
{
    var completed = await Task.WhenAny(tasks);
    tasks.Remove(completed);
    var result = await completed;
    Process(result);
}

Async Streams (C# 8+)

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

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

Cancellation

public async Task ProcessAsync(CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        await DoWorkAsync(ct);
    }
}

// Usage
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(30));

try
{
    await ProcessAsync(cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Cancelled");
}

Error Handling

Try/Catch/Finally

try
{
    var result = RiskyOperation();
}
catch (FileNotFoundException ex)
{
    Console.WriteLine($"File not found: {ex.FileName}");
}
catch (IOException ex) when (ex.HResult == -2147024816)
{
    // Exception filter
    Console.WriteLine("Disk full");
}
catch (Exception ex)
{
    Console.WriteLine($"Error: {ex.Message}");
    throw;  // Re-throw preserving stack trace
}
finally
{
    // Always runs (cleanup)
    Cleanup();
}

Throwing Exceptions

// Throw new exception
throw new ArgumentException("Invalid argument", nameof(param));

// Common exception types
throw new ArgumentNullException(nameof(param));
throw new InvalidOperationException("Cannot do that now");
throw new NotImplementedException();
throw new NotSupportedException();

// Guard clauses
ArgumentNullException.ThrowIfNull(param);
ArgumentException.ThrowIfNullOrEmpty(str);

Custom Exceptions

public class OrderNotFoundException : Exception
{
    public int OrderId { get; }

    public OrderNotFoundException(int orderId)
        : base($"Order {orderId} not found")
    {
        OrderId = orderId;
    }
}

Using Statement (IDisposable)

// Classic using block
using (var file = File.OpenRead("data.txt"))
{
    // file is disposed at end of block
}

// Using declaration (C# 8+)
using var file = File.OpenRead("data.txt");
// file is disposed at end of scope

// Multiple resources
using var reader = new StreamReader(path);
using var writer = new StreamWriter(outPath);

// Async disposal
await using var conn = new SqlConnection(connStr);

Pattern Matching

Powerful conditional logic with concise syntax. Progressively enhanced from C# 7 through C# 11+.

Type Patterns

// Type check and cast
if (obj is string s)
{
    Console.WriteLine(s.Length);
}

// Negation
if (obj is not null)
{
    // ...
}

// Switch expression
var description = obj switch
{
    int n => $"Integer: {n}",
    string s => $"String: {s}",
    null => "Nothing",
    _ => "Unknown"
};

Property Patterns

var message = person switch
{
    { Age: >= 18, Name: "Admin" } => "Admin access",
    { Age: >= 18 } => "Adult access",
    { Age: < 18 } => "Minor access",
    _ => "Unknown"
};

// Nested properties
if (order is { Customer: { Address: { City: "NYC" } } })
{
    // order.Customer.Address.City == "NYC"
}

Relational Patterns

var grade = score switch
{
    >= 90 => "A",
    >= 80 and < 90 => "B",
    >= 70 and < 80 => "C",
    >= 60 and < 70 => "D",
    _ => "F"
};

List Patterns (C# 11+)

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

var result = nums switch
{
    [] => "Empty",
    [var single] => $"Single: {single}",
    [var first, ..] => $"Starts with: {first}",
    [.., var last] => $"Ends with: {last}",
    [1, 2, 3] => "Exact match",
    [1, .. var rest] => $"Starts with 1, rest: {rest.Length}"
};

Deconstruction Patterns

var (x, y) = GetPoint();  // Deconstruct tuple

// In switch
var quadrant = point switch
{
    (0, 0) => "Origin",
    (> 0, > 0) => "Q1",
    (< 0, > 0) => "Q2",
    (< 0, < 0) => "Q3",
    (> 0, < 0) => "Q4",
    _ => "On axis"
};

Modern C# Features

Nullable Reference Types (C# 8+)

// Enable in project: <Nullable>enable</Nullable>

string name = "Alice";    // Cannot be null
string? maybe = null;    // Explicitly nullable

// Null-forgiving operator (trust me, it's not null)
string s = maybe!;

// Null-conditional operators
var len = maybe?.Length;           // int?
var first = list?[0];             // Access if not null
maybe?.ToUpper();                  // Call if not null

// Null coalescing
var value = maybe ?? "default";
maybe ??= "assign if null";

File-Scoped Namespaces (C# 10+)

// Instead of:
namespace MyApp
{
    class Foo { }
}

// Write:
namespace MyApp;

class Foo { }

Global Usings (C# 10+)

// GlobalUsings.cs or in .csproj
global using System;
global using System.Collections.Generic;
global using System.Linq;

// Implicit usings (SDK sets common ones)
// In .csproj: <ImplicitUsings>enable</ImplicitUsings>

Required Members (C# 11+)

public class Person
{
    public required string Name { get; init; }
    public required int Age { get; init; }
}

// Must initialize required members
var p = new Person { Name = "Alice", Age = 30 };

Collection Expressions (C# 12+)

// Unified syntax for all collection types
int[] arr = [1, 2, 3];
List<int> list = [1, 2, 3];
Span<int> span = [1, 2, 3];

// Spread operator
int[] combined = [..first, ..second, 99];

// Empty collection
List<string> empty = [];

params Collections (C# 13+)

// params with any collection type
void PrintAll(params ReadOnlySpan<string> items)
{
    foreach (var item in items)
        Console.WriteLine(item);
}

PrintAll("a", "b", "c");

Generics

// Generic class
public class Box<T>
{
    public T Value { get; set; }
}

// Generic method
public T Max<T>(T a, T b) where T : IComparable<T>
{
    return a.CompareTo(b) > 0 ? a : b;
}

// Constraints
where T : class              // Reference type
where T : struct             // Value type
where T : new()              // Has parameterless constructor
where T : BaseClass          // Inherits from
where T : IInterface         // Implements interface
where T : notnull            // Non-nullable

Attributes

// Using attributes
[Obsolete("Use NewMethod instead")]
public void OldMethod() { }

[Serializable]
public class MyClass { }

// Custom attribute
[AttributeUsage(AttributeTargets.Method)]
public class LogAttribute : Attribute
{
    public string Message { get; set; }
}

CLI & Project Setup

.NET CLI Commands

# Create new project
dotnet new console -n MyApp        # Console app
dotnet new webapi -n MyApi         # Web API
dotnet new classlib -n MyLib       # Class library

# Build and run
dotnet build
dotnet run
dotnet watch run                   # Hot reload

# Dependencies
dotnet add package Newtonsoft.Json
dotnet remove package Newtonsoft.Json
dotnet restore

# Testing
dotnet new xunit -n MyApp.Tests
dotnet test
dotnet test --filter "Category=Unit"

# Publish
dotnet publish -c Release
dotnet publish -c Release -r win-x64 --self-contained

Project File (.csproj)

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <OutputType>Exe</OutputType>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  </ItemGroup>
</Project>

Configuration

// appsettings.json
{
  "ConnectionStrings": {
    "Default": "Server=localhost;Database=mydb"
  },
  "Logging": {
    "LogLevel": { "Default": "Information" }
  }
}

// Reading configuration
var builder = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .AddEnvironmentVariables();

var config = builder.Build();
var connStr = config.GetConnectionString("Default");

Dependency Injection

// Register services
var services = new ServiceCollection();
services.AddSingleton<ILogger, ConsoleLogger>();
services.AddScoped<IUserService, UserService>();
services.AddTransient<IEmailSender, SmtpEmailSender>();

var provider = services.BuildServiceProvider();

// Resolve
var logger = provider.GetRequiredService<ILogger>();

// In ASP.NET Core
public class MyController(IUserService users)
{
    // Primary constructor injection
}