Jump to section
C# Cheatsheet
A practical C# guide covering syntax, OOP, LINQ, async programming, collections, .NET development, and modern language features.
What is C#?
C# is Microsoft's general-purpose language that runs on the .NET runtime. It's used for everything from Windows desktop apps to web APIs, mobile apps with MAUI, and games with Unity. That said — most developers pick it up for web or desktop work first.
where C# is used
- ASP.NET Core — the most popular C# web framework.
- Used heavily in enterprise backends and REST APIs.
- WPF and WinForms for Windows desktop applications.
- MAUI for cross-platform desktop and mobile.
- Unity uses C# as its scripting language.
- One of the top two choices for indie and professional game dev.
- C# compiles to IL (Intermediate Language), not machine code.
- The CLR (Common Language Runtime) runs it — handles memory, threads, garbage collection.
your first C# program
CSharp
// modern style — C# 9+ (top-level statements)
// no class or Main needed
Console.WriteLine("Hello, World!");
// ─────────────────────────────────────────────
// classic style — works in all versions
using System;
namespace MyApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}comments and output
CSharp
// single-line comment
/*
multi-line comment
spans several lines
*/
/// <summary>
/// XML documentation comment — shown in
/// IntelliSense and generated docs.
/// </summary>
/// <param name="name">The user's name.</param>
/// <returns>A greeting string.</returns>
string Greet(string name) => $"Hello, {name}!";
// TODO: refactor this later
// shown as a task in Visual Studio
// output
Console.WriteLine("With newline");
Console.Write("Without newline ");
Console.Write("same line");
// formatted output
Console.WriteLine($"Score: {95 * 2}");
Console.WriteLine("Name: {0}, Age: {1}", "Alice", 28);Variables & Data Types
C# is strongly typed — every variable has a type. Most of the time var lets the compiler figure it out for you. And if you're working with money, always use decimal. That's not a suggestion.
value types
CSharp
int count = 42;
long bigNum = 9_000_000_000L;
short small = 30_000;
byte b = 255;
float f = 3.14f; // needs f suffix
double d = 3.14159265; // default decimal type
decimal m = 19.99m; // needs m suffix — use for money
bool flag = true;
char c = 'A';
// underscores make large numbers readable
int million = 1_000_000;declaring variables — var and const
CSharp
// explicit type
int age = 25;
string name = "Alice";
// var — compiler infers the type
var score = 100; // int
var city = "Colombo"; // string
var price = 9.99; // double
// const — fixed at compile time
const double Pi = 3.14159;
const int MaxSize = 100;
// readonly — fixed after construction
readonly string AppName = "MyApp";
// declare now, assign later
// must assign before using
int count;
count = 0;type conversion
CSharp
// implicit — automatic when safe
int i = 42;
long l = i; // int → long (no data loss)
double d = i; // int → double
// explicit cast — may lose data
double price = 9.99;
int cents = (int) price; // 9 (decimal lost)
// Convert class — flexible conversions
int n = Convert.ToInt32("42");
double x = Convert.ToDouble("3.14");
string s = Convert.ToString(42);
bool b = Convert.ToBoolean("true");
// Parse — throws if invalid
int parsed = int.Parse("42");
// TryParse — safe, no exception
if (int.TryParse("42", out int result))
{
Console.WriteLine(result); // 42
}
if (int.TryParse("abc", out int bad))
{
// never runs — "abc" isn't a number
}Strings
C# strings have two things Java developers immediately notice: interpolation with $ is built in and clean, and == actually compares content by default. Both are welcome improvements.
interpolation, verbatim, and concatenation
CSharp
string name = "Alice";
int score = 95;
// interpolation — cleanest way
string msg = $"Hello, {name}! Score: {score * 2}";
// expression inside {}
Console.WriteLine($"Next year: {2024 + 1}");
// verbatim — no escape needed for backslashes
string path = @"C:UsersAliceDocuments";
string multi = @"Line one
Line two
Line three";
// raw string (C# 11+) — no escaping at all
string json = """
{
"name": "Alice",
"age": 28
}
""";
// concatenation with +
string full = "Hello, " + name + "!";common string methods
CSharp
string s = " Hello, C#! ";
// length and access
s.Length // 14
s[0] // ' ' (first char)
// trim whitespace
string clean = s.Trim(); // "Hello, C#!"
s.TrimStart(); // remove leading
s.TrimEnd(); // remove trailing
// case
clean.ToUpper() // "HELLO, C#!"
clean.ToLower() // "hello, c#!"
// search
clean.Contains("C#") // true
clean.StartsWith("Hello") // true
clean.EndsWith("!") // true
clean.IndexOf("C") // 7
// modify (returns new string — strings are immutable)
clean.Replace("C#", "World") // "Hello, World!"
// extract
clean.Substring(7) // "C#!"
clean.Substring(7, 2) // "C#"
// split
"a,b,c".Split(',') // ["a","b","c"]
// null and empty checks
string.IsNullOrEmpty(null) // true
string.IsNullOrEmpty("") // true
string.IsNullOrWhiteSpace(" ") // true== comparison and StringBuilder
CSharp
// == compares content — works correctly in C#
string a = "hello";
string b = "hello";
a == b // true ✅ (unlike Java)
a.Equals(b) // true
// case-insensitive comparison
a.Equals("HELLO",
StringComparison.OrdinalIgnoreCase) // true
// StringBuilder — for building in loops
// ❌ slow: creates a new string every iteration
string result = "";
for (int i = 0; i < 1000; i++)
result += i;
// ✅ fast: modifies in place
var sb = new System.Text.StringBuilder();
for (int i = 0; i < 1000; i++)
sb.Append(i);
string final = sb.ToString();
// StringBuilder methods
var sb2 = new System.Text.StringBuilder("Hello");
sb2.Append(", World"); // "Hello, World"
sb2.Insert(5, "!"); // "Hello!, World"
sb2.Replace("World", "C#"); // "Hello!, C#"
sb2.Remove(5, 1); // "Hello, C#"Operators
C# operators mostly follow what you'd expect. The ones worth paying extra attention to are the null-related ones — ??, ?., and ??= — which show up everywhere in modern C# code.
arithmetic, comparison, and logical
CSharp
// arithmetic
int a = 10, b = 3;
a + b // 13
a - b // 7
a * b // 30
a / b // 3 (integer division — decimal dropped)
a % b // 1 (remainder)
// get decimal result from int division
(double) a / b // 3.333...
10.0 / 3 // 3.333...
// compound assignment
int x = 10;
x += 5; // 15
x -= 3; // 12
x *= 2; // 24
x /= 4; // 6
x %= 5; // 1
// increment / decrement
x++; // post-increment
++x; // pre-increment
x--; // post-decrement
// comparison
5 == 5 // true
5 != 3 // true
5 > 3 // true
5 <= 5 // true
// logical
true && false // false (AND)
true || false // true (OR)
!true // false (NOT)
// ternary
int age = 20;
string label = age >= 18 ? "Adult" : "Minor";null-related operators — ??, ?., ??=
CSharp
// ?? — fallback when null
string name = null;
string display = name ?? "Guest";
// "Guest"
// chain ?? for multiple fallbacks
string a = null, b = null, c = "found";
string result = a ?? b ?? c;
// "found"
// ?. — call method/property only if not null
// returns null instead of throwing
string? input = null;
int? len = input?.Length;
// null — no NullReferenceException
// chain ?.
string? city = user?.Address?.City;
// null if user or Address is null
// ??= — assign only if currently null
string? cached = null;
cached ??= LoadFromDatabase();
// only calls LoadFromDatabase if cached is null
// combine ?? and ?.
string result2 = user?.Name ?? "Anonymous";Control Flow
C# switch got a major upgrade starting in C# 8 — switch expressions, property patterns, and tuple patterns. The old form still works, but the new form is considerably more expressive.
if, else if, else
CSharp
int score = 75;
if (score >= 90)
{
Console.WriteLine("A");
}
else if (score >= 75)
{
Console.WriteLine("B");
}
else if (score >= 60)
{
Console.WriteLine("C");
}
else
{
Console.WriteLine("F");
}
// single-statement if (no braces)
// only for very simple cases
if (score > 50)
Console.WriteLine("Passed");
// if expression (C# 9+)
string grade = score >= 90 ? "A"
: score >= 75 ? "B"
: score >= 60 ? "C"
: "F";switch — classic and expression form
CSharp
// classic switch
string day = "MON";
switch (day)
{
case "MON":
Console.WriteLine("Monday");
break;
case "SAT":
case "SUN":
Console.WriteLine("Weekend");
break;
default:
Console.WriteLine("Weekday");
break;
}
// switch expression (C# 8+) — cleaner
string label = day switch
{
"MON" => "Monday",
"TUE" => "Tuesday",
"SAT" or "SUN" => "Weekend",
_ => "Other day" // _ = default
};
// pattern matching with when
int n = 42;
string desc = n switch
{
0 => "zero",
< 0 => "negative",
> 0 and < 100 => "small positive",
_ => "large"
};
// property pattern
var user = new { Name = "Alice", Age = 28 };
string category = user switch
{
{ Age: < 18 } => "Minor",
{ Age: >= 65 } => "Senior",
_ => "Adult"
};Loops
C# has the standard four loop types. foreach is the cleanest for collections — use it whenever you only need to read each item. for is better when you need the index or need to modify elements.
for and foreach
CSharp
// for loop
for (int i = 0; i < 5; i++)
{
Console.WriteLine(i); // 0 1 2 3 4
}
// count down
for (int i = 5; i > 0; i--)
{
Console.WriteLine(i); // 5 4 3 2 1
}
// loop through array by index
int[] nums = { 10, 20, 30, 40 };
for (int i = 0; i < nums.Length; i++)
{
Console.WriteLine(nums[i]);
}
// foreach — cleaner for reading
foreach (int n in nums)
{
Console.WriteLine(n);
}
// foreach over a list
var names = new List<string>
{ "Alice", "Bob", "Carol" };
foreach (var name in names)
{
Console.WriteLine($"Hello, {name}");
}while, do-while, break, continue
CSharp
// while
int i = 0;
while (i < 5)
{
Console.WriteLine(i);
i++;
}
// do-while — runs at least once
int j = 10;
do
{
Console.WriteLine(j); // prints 10
j++;
} while (j < 5);
// break — exit loop immediately
for (int k = 0; k < 10; k++)
{
if (k == 5) break;
Console.Write(k + " "); // 0 1 2 3 4
}
// continue — skip this iteration
for (int k = 0; k < 5; k++)
{
if (k == 2) continue;
Console.Write(k + " "); // 0 1 3 4
}Arrays & Collections
Arrays are fixed-size and fast. For most real work you'll reach for List<T> instead — it grows as needed and has a much richer API. Dictionary and HashSet round out the three you'll use most.
arrays
CSharp
// declare and initialise
int[] nums = { 1, 2, 3, 4, 5 };
string[] names = { "Alice", "Bob" };
bool[] flags = new bool[3]; // [false, false, false]
// access and update
nums[0] // 1
nums[^1] // 5 (last element — C# 8+)
nums[1] = 99;
// length
nums.Length // 5
// sort and search
Array.Sort(nums);
Array.Reverse(nums);
int idx = Array.IndexOf(nums, 3); // index or -1
// 2D array
int[,] matrix = {
{ 1, 2, 3 },
{ 4, 5, 6 }
};
matrix[0, 0] // 1
matrix[1, 2] // 6
// array slicing (C# 8+)
int[] slice = nums[1..4]; // index 1 to 3List<T>
CSharp
using System.Collections.Generic;
// create
var nums = new List<int> { 1, 2, 3 };
var names = new List<string>();
// add
names.Add("Alice");
names.Add("Bob");
names.Insert(1, "Carol"); // insert at index
// access
names[0] // "Alice"
names.Count // 3
names[^1] // last item (C# 8+)
// search
names.Contains("Bob") // true
names.IndexOf("Carol") // 1
names.Find(n => n.Length > 3) // "Alice"
// remove
names.Remove("Bob"); // by value
names.RemoveAt(0); // by index
names.RemoveAll(n => n.StartsWith("A"));
// iterate
foreach (var name in names)
Console.WriteLine(name);
// sort
names.Sort();
names.Sort((a, b) =>
a.Length.CompareTo(b.Length));
// convert from array
var list = new List<int>(nums);
int[] arr = names.ToArray();Dictionary<TKey, TValue> and HashSet<T>
CSharp
// ── Dictionary ──────────────────────────────
var scores = new Dictionary<string, int>
{
{ "Alice", 90 },
{ "Bob", 85 }
};
// add and update
scores["Carol"] = 92; // add new key
scores["Alice"] = 95; // update existing
// access — use TryGetValue to avoid exceptions
if (scores.TryGetValue("Alice", out int s))
Console.WriteLine(s); // 95
scores.GetValueOrDefault("Dave", 0); // 0
// check and remove
scores.ContainsKey("Bob") // true
scores.Remove("Bob");
// iterate
foreach (var (key, val) in scores)
Console.WriteLine($"{key}: {val}");
// ── HashSet ──────────────────────────────
var tags = new HashSet<string>();
tags.Add("csharp");
tags.Add("dotnet");
tags.Add("csharp"); // ignored — already exists
tags.Contains("dotnet") // true
tags.Count // 2
tags.Remove("dotnet");
// set operations
var a = new HashSet<int> { 1, 2, 3, 4 };
var b = new HashSet<int> { 3, 4, 5, 6 };
a.UnionWith(b); // {1,2,3,4,5,6}
a.IntersectWith(b); // {3,4}
a.ExceptWith(b); // {1,2}Methods
C# methods are straightforward, but a few features are worth knowing early — expression-bodied methods, optional parameters, and out parameters. You'll see all three regularly in real codebases.
defining and calling methods
CSharp
// standard method
static int Add(int a, int b)
{
return a + b;
}
// expression-bodied — same thing, shorter
static int Add(int a, int b) => a + b;
// void — returns nothing
static void PrintSum(int a, int b)
=> Console.WriteLine(a + b);
// multiple return values with tuple
static (int Min, int Max) MinMax(int[] arr)
=> (arr.Min(), arr.Max());
// calling methods
int sum = Add(3, 4); // 7
PrintSum(5, 6); // 11
var result = MinMax(new[] { 3, 1, 9, 2 });
result.Min // 1
result.Max // 9optional params, named args, out and ref
CSharp
// optional parameters — must come last
static void CreateUser(
string name,
string role = "user",
bool isActive = true)
{
Console.WriteLine($"{name} | {role} | {isActive}");
}
CreateUser("Alice");
// Alice | user | true
CreateUser("Bob", "admin");
// Bob | admin | true
// named arguments — skip to the one you need
CreateUser("Carol", isActive: false);
// Carol | user | false
// out — method returns a value via parameter
// used by TryParse pattern
static bool Divide(
int a, int b, out double result)
{
if (b == 0) { result = 0; return false; }
result = (double) a / b;
return true;
}
if (Divide(10, 3, out double r))
Console.WriteLine(r); // 3.333...
// ref — pass by reference, can modify original
static void Double(ref int value)
=> value *= 2;
int n = 5;
Double(ref n);
Console.WriteLine(n); // 10
// params — variable number of arguments
static int Sum(params int[] numbers)
=> numbers.Sum();
Sum(1, 2); // 3
Sum(1, 2, 3, 4, 5); // 15OOP — Classes & Objects
C# classes are clean and expressive. Properties with get and set replace getter/setter methods entirely, and records (C# 9+) make immutable data classes a one-liner.
class structure and creating objects
CSharp
public class Person
{
// auto-property — compiler generates backing field
public string Name { get; set; }
public int Age { get; set; }
// read-only property
public string Id { get; } = Guid.NewGuid().ToString();
// property with validation
private int _score;
public int Score
{
get => _score;
set => _score = value < 0 ? 0 : value;
}
// constructor
public Person(string name, int age)
{
Name = name;
Age = age;
}
// method
public string Introduce()
=> $"Hi, I'm {Name}, age {Age}";
}
// create an object
var alice = new Person("Alice", 28);
var bob = new Person("Bob", 32);
// access properties and methods
Console.WriteLine(alice.Name); // Alice
Console.WriteLine(alice.Introduce()); // Hi, I'm Alice...
// object initialiser syntax
var carol = new Person("Carol", 25)
{
Score = 90
};access modifiers, static, and record types
CSharp
// access modifiers
public class BankAccount
{
private double _balance; // only this class
protected string OwnerId; // this + subclasses
internal string BranchCode; // same project
public string AccountNumber; // everywhere
public double GetBalance() => _balance;
public void Deposit(double amount)
{
if (amount > 0) _balance += amount;
}
}
// static members
public class MathHelper
{
public static double Pi = 3.14159;
// call on class, not instance
public static int Square(int n) => n * n;
}
MathHelper.Square(5); // 25
MathHelper.Pi; // 3.14159
// record — immutable data type (C# 9+)
// generates equals, hashcode, toString automatically
public record User(string Name, int Age, string Email);
var alice = new User("Alice", 28, "[email protected]");
Console.WriteLine(alice);
// User { Name = Alice, Age = 28, Email = alice@... }
// with expression — copy with one change
var older = alice with { Age = 29 };OOP — Inheritance & Interfaces
C# supports single class inheritance and multiple interface implementation. Classes are not sealed by default — add sealed when you explicitly don't want a class extended.
inheritance — virtual, override, abstract
CSharp
// base class
public class Animal
{
public string Name { get; set; }
public Animal(string name) => Name = name;
// virtual — can be overridden
public virtual void Speak()
=> Console.WriteLine($"{Name} makes a sound");
}
// subclass — : Animal means "extends Animal"
public class Dog : Animal
{
public string Breed { get; set; }
public Dog(string name, string breed) : base(name)
=> Breed = breed;
// override the parent method
public override void Speak()
=> Console.WriteLine($"{Name} barks");
public void Fetch()
=> Console.WriteLine($"{Name} fetches!");
}
// abstract class — cannot be instantiated
public abstract class Shape
{
public abstract double Area(); // must implement
public void Describe() // shared method
=> Console.WriteLine($"Area: {Area()}");
}
public class Circle : Shape
{
double _r;
public Circle(double r) => _r = r;
public override double Area()
=> Math.PI * _r * _r;
}
var dog = new Dog("Rex", "Labrador");
dog.Speak(); // Rex barks
dog.Fetch(); // Rex fetches!
Animal a = new Dog("Buddy", "Poodle");
a.Speak(); // Buddy barks (polymorphism)interfaces
CSharp
// define an interface
public interface IPrintable
{
void Print();
// default implementation (C# 8+)
void PrintTwice()
{
Print();
Print();
}
}
public interface ISaveable
{
bool Save(string path);
}
// implement multiple interfaces
public class Report : IPrintable, ISaveable
{
public string Title { get; set; }
public void Print()
=> Console.WriteLine($"Printing: {Title}");
public bool Save(string path)
{
Console.WriteLine($"Saving to {path}");
return true;
}
// PrintTwice() comes from default impl — free
}
var r = new Report { Title = "Q3 Results" };
r.Print(); // Printing: Q3 Results
r.PrintTwice(); // prints twice
r.Save("report.pdf");
// interface as a type
IPrintable printable = r;
printable.Print();Nullability
C# has two kinds of null handling. Nullable value types (int?) have been around since C# 2. Nullable reference types — where string? means a string that can be null — arrived in C# 8 and are now the standard.
nullable value types — int?, bool?, struct?
CSharp
// nullable value types
int? age = null;
bool? isReady = null;
// check before using
if (age.HasValue)
Console.WriteLine(age.Value);
// safe fallback
int display = age ?? 0; // 0 if null
int safe = age.GetValueOrDefault(0); // same
// nullable arithmetic
int? a = 5;
int? b = null;
int? sum = a + b; // null (null propagates)
// pattern matching with nullables
if (age is int n)
Console.WriteLine($"Age is {n}");
// nullable in method signatures
static string FormatAge(int? age)
=> age.HasValue
? $"Age: {age.Value}"
: "Age unknown";nullable reference types (C# 8+)
CSharp
// enable in .csproj (recommended project-wide)
// <Nullable>enable</Nullable>
// or per-file
#nullable enable
string name = "Alice"; // cannot be null
string? middle = null; // can be null
// compiler warns if you dereference without checking
Console.WriteLine(middle.Length); // ← warning!
// safe patterns
if (middle != null)
Console.WriteLine(middle.Length); // safe
Console.WriteLine(middle?.Length); // null-conditional
Console.WriteLine(middle ?? "N/A"); // fallback
// null-forgiving operator !
// tells compiler: I know this isn't null
string value = GetMaybeNull()!;
// use only when you're 100% sureThe null-forgiving operator ! tells the compiler 'trust me, this isn't null'. If you're wrong, you get a NullReferenceException at runtime. Use it only when you're certain.
Exception Handling
Exception handling in C# follows the try/catch/finally pattern. The using statement is worth learning early — it makes resource cleanup automatic and is used constantly in file and database code.
try, catch, finally, when
CSharp
try
{
int result = 10 / 0; // throws DivideByZeroException
string s = null;
s.Length; // throws NullReferenceException
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Divide error: {ex.Message}");
}
catch (NullReferenceException ex)
{
Console.WriteLine($"Null error: {ex.Message}");
}
catch (Exception ex) when (ex.Message.Contains("file"))
{
// 'when' filter — catch only if condition is true
Console.WriteLine("File-related error");
}
catch (Exception ex)
{
// catch-all — always last
Console.WriteLine($"Error: {ex.Message}");
}
finally
{
// always runs — use for cleanup
Console.WriteLine("Cleanup complete");
}
// throw and rethrow
static void ValidateAge(int age)
{
if (age < 0)
throw new ArgumentException(
"Age cannot be negative", nameof(age));
}
// rethrow preserving the stack trace
catch (Exception ex)
{
Log(ex);
throw; // not 'throw ex' — that resets trace
}custom exceptions and using statement
CSharp
// custom exception
public class InsufficientFundsException : Exception
{
public decimal Amount { get; }
public InsufficientFundsException(decimal amount)
: base($"Insufficient funds. Need {amount} more.")
{
Amount = amount;
}
}
// throw it
throw new InsufficientFundsException(50.00m);
// catch it
try { /* ... */ }
catch (InsufficientFundsException ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine($"Short by: {ex.Amount}");
}
// using statement — auto-dispose
using (var reader = new StreamReader("file.txt"))
{
string content = reader.ReadToEnd();
}
// reader.Dispose() called automatically
// using declaration (C# 8+) — no braces
using var reader2 = new StreamReader("file.txt");
string line = reader2.ReadLine();
// disposed at end of enclosing scopeLINQ
LINQ — Language-Integrated Query — lets you filter, transform, and query collections using the same syntax regardless of the source. It's one of the things C# developers genuinely miss when working in other languages.
method syntax — the everyday approach
CSharp
using System.Linq;
var nums = new List<int> { 1, 2, 3, 4, 5, 6 };
// filter
nums.Where(n => n % 2 == 0)
// [2, 4, 6]
// transform
nums.Select(n => n * 10)
// [10, 20, 30, 40, 50, 60]
// sort
nums.OrderBy(n => n) // ascending
nums.OrderByDescending(n => n) // descending
// aggregates
nums.Sum() // 21
nums.Average() // 3.5
nums.Min() // 1
nums.Max() // 6
nums.Count() // 6
nums.Count(n => n > 3) // 3
// first and single
nums.First() // 1 (throws if empty)
nums.FirstOrDefault() // 1 (null/0 if empty)
nums.FirstOrDefault(n => n > 10) // 0 (not found)
nums.Single(n => n == 3) // 3 (throws if not exactly 1)
// check
nums.Any(n => n > 5) // true
nums.All(n => n > 0) // true
// chain operations
var result = nums
.Where(n => n % 2 == 0) // [2, 4, 6]
.Select(n => n * n) // [4, 16, 36]
.OrderByDescending(n => n) // [36, 16, 4]
.ToList();query syntax and useful operations
CSharp
var nums = new[] { 1, 2, 3, 4, 5, 6 };
// query syntax — reads like SQL
var evens = from n in nums
where n % 2 == 0
orderby n descending
select n * 10;
// [60, 40, 20]
// group by
var words = new[] { "hi", "hello", "hey", "bye" };
var grouped = words.GroupBy(w => w[0]);
foreach (var group in grouped)
{
Console.WriteLine($"{group.Key}: {string.Join(", ", group)}");
}
// h: hi, hello, hey
// b: bye
// pagination
nums.Take(3) // [1, 2, 3]
nums.Skip(3) // [4, 5, 6]
nums.Take(2..4) // [3, 4] (C# 8+ range)
// distinct — remove duplicates
new[] { 1, 2, 2, 3, 3, 3 }.Distinct()
// [1, 2, 3]
// flatten nested collections
var nested = new[] {
new[] { 1, 2 },
new[] { 3, 4 }
};
nested.SelectMany(n => n)
// [1, 2, 3, 4]
// working with objects
var users = new List<User>
{
new User("Alice", 28, "[email protected]"),
new User("Bob", 17, "[email protected]"),
new User("Carol", 32, "[email protected]")
};
var adults = users
.Where(u => u.Age >= 18)
.OrderBy(u => u.Name)
.Select(u => u.Name)
.ToList();
// ["Alice", "Carol"]Async / Await
async and await are how C# handles operations that take time — network calls, file reads, database queries. The code looks synchronous and reads top-to-bottom, but it doesn't block the thread while waiting.
async, await, and Task
CSharp
using System.Threading.Tasks;
// async method returning a value
async Task<string> FetchUserAsync(int id)
{
// await pauses here without blocking
await Task.Delay(1000); // simulates network
return $"User {id}";
}
// async method returning nothing — use Task not void
async Task SendEmailAsync(string to)
{
await Task.Delay(500);
Console.WriteLine($"Email sent to {to}");
}
// calling async methods — must await
async Task RunAsync()
{
string user = await FetchUserAsync(1);
Console.WriteLine(user); // "User 1"
await SendEmailAsync("[email protected]");
}
// top-level await (C# 9+ / .NET 5+)
var result = await FetchUserAsync(1);
Console.WriteLine(result);
// async void — only for event handlers
async void Button_Click(object s, EventArgs e)
{
await DoWorkAsync();
}parallel tasks and best practices
CSharp
// ❌ sequential — one after another (~2 seconds)
var user1 = await FetchUserAsync(1); // wait 1s
var user2 = await FetchUserAsync(2); // wait 1s
// ✅ parallel — both run at same time (~1 second)
var task1 = FetchUserAsync(1);
var task2 = FetchUserAsync(2);
var results = await Task.WhenAll(task1, task2);
// results[0] = "User 1"
// results[1] = "User 2"
// Task.WhenAny — continue on first to finish
var first = await Task.WhenAny(task1, task2);
Console.WriteLine(await first);
// ConfigureAwait(false) — in library code
// avoids deadlocks in certain contexts
async Task LibraryMethodAsync()
{
await SomeOperationAsync()
.ConfigureAwait(false);
}
// CancellationToken — support cancellation
async Task FetchAsync(CancellationToken ct)
{
await Task.Delay(5000, ct); // throws if cancelled
}
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(3));
await FetchAsync(cts.Token);Never use .Result or .Wait() on a Task in async code — it blocks the thread and can cause deadlocks. Always await instead.
Tips & Good Habits
These are the habits that make C# code safer and easier to work with. Some prevent real bugs. Others just make your code easier to read — which matters more than it sounds when you're working on a team.
always use decimal for money
CSharp
// ❌ double — imprecise for money
double price = 0.1 + 0.2;
Console.WriteLine(price);
// 0.30000000000000004 ← not 0.3!
// ✅ decimal — exact
decimal price2 = 0.1m + 0.2m;
Console.WriteLine(price2);
// 0.3 ← correct
// always use decimal for financial values
decimal productPrice = 19.99m;
decimal taxRate = 0.15m;
decimal total = productPrice * (1 + taxRate);
// 22.9885m ← exactC# naming conventions
CSharp
// ✅ correct naming
public class UserService // PascalCase class
{
private string _connectionString; // _camelCase field
public int MaxRetries = 3; // PascalCase constant
public async Task<User> GetUserAsync(int userId)
{ // PascalCase + Async
int localCount = 0; // camelCase local var
return await FindAsync(userId);
}
}
public interface IUserRepository // I prefix
{
Task<User> FindAsync(int id);
}
// ❌ inconsistent naming
public class userservice {
public int maxretries = 3;
public async Task<User> getUser(int ID) { }
}prefer var for readability
CSharp
// ✅ use var when the type is obvious from context
var name = "Alice"; // clearly string
var count = 42; // clearly int
var users = new List<User>(); // clearly List<User>
var result = GetUser(id); // method name says it
// ✅ use var in foreach
foreach (var user in users)
Console.WriteLine(user.Name);
// consider explicit type when it adds clarity
Dictionary<string, List<int>> lookup
= new Dictionary<string, List<int>>();
// var is fine here too — just preference
// ❌ var that hides what the type actually is
var x = Process(); // what does this return?
// explicit is clearer:
OrderResult x = Process();handy one-liners worth bookmarking
CSharp
// join a collection into a string
string csv = string.Join(", ", names);
// "Alice, Bob, Carol"
// generate a range of numbers
var range = Enumerable.Range(1, 10).ToList();
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// nameof — refactor-safe string of a name
Console.WriteLine(nameof(userName)); // "userName"
throw new ArgumentNullException(nameof(input));
// pattern matching with is
object obj = "hello";
if (obj is string s && s.Length > 3)
Console.WriteLine(s.ToUpper());
// switch on type
static string Describe(object o) => o switch
{
int n => $"integer: {n}",
string s => $"string: {s}",
null => "null",
_ => "something else"
};
// quick null check and throw
static void Process(string input)
{
ArgumentNullException.ThrowIfNull(input);
// input is guaranteed non-null here
}
// target-typed new (C# 9+) — skip repeating type
List<string> names = new(); // same as new List<string>()
Dictionary<string, int> map = new();No login required to share feedback
More Cheatsheets
Keep your reference handy
Explore more zero-to-hero cheatsheets for the tools you use daily.