Compiled and systems languages cheat sheetC# logo

C# cheat sheet

C# syntax on one page, grouped by what you are trying to do: LINQ, records, properties, async/await and pattern matching, checked on C# 14 and .NET 10.

Last updated

C# is a statically typed language from Microsoft that compiles to run on .NET, on Windows, macOS and Linux alike. It is behind ASP.NET Core web services, Unity games and a lot of desktop and cloud software. The reference below is grouped by what you are trying to do, and the filter box searches all of it at once. Type linq and every LINQ row comes to you, or type C# 14 to see what that release added.

Every snippet is checked against C# 14 on .NET 10, the current long-term support release. .NET 8, the previous LTS, is the baseline, so anything newer says so in the notes column and you can tell at a glance whether it will run on the SDK you have. Names like names, people and Person are placeholders for your own. If you do not have the .NET SDK installed, the official Docker image is the quickest way to try something: docker run --rm -it -v "$PWD":/src -w /src mcr.microsoft.com/dotnet/sdk:10.0 bash, and the Docker cheat sheet has the rest. Coming from Java? The Java cheat sheet is laid out the same way, so the two are easy to compare side by side.

The dotnet CLI

TaskCommandNotes
Check which version you havedotnet --version dotnet --list-sdks
Run one .cs file, no projectdotnet run app.cs.NET 10. A file-based app: top-level code, no .csproj
Use a NuGet package in that file#:package Humanizer@2.14.1.NET 10. A directive on the first lines of app.cs
Turn the file into a projectdotnet project convert app.cs.NET 10. For when it outgrows one file
New console appdotnet new console -o MyAppMakes a MyApp folder with MyApp.csproj and Program.cs
Other starting pointsdotnet new webapi dotnet new classlib dotnet new xunitdotnet new list shows every template
Run the project in this folderdotnet runBuilds first if anything changed
Pass arguments to your programdotnet run -- --port 8080Everything after -- goes to args
Rerun on every savedotnet watchHot reload where it can, a restart where it cannot
Builddotnet build dotnet build -c ReleaseDebug is the default configuration
Treat every warning as an errordotnet build -warnaserrorOr <TreatWarningsAsErrors>true</TreatWarningsAsErrors> in the .csproj
Run the testsdotnet test
Run some of the testsdotnet test --filter "FullyQualifiedName~Orders"
Add a NuGet packagedotnet add package SerilogLatest stable. --version 4.3.0 pins one
Remove a packagedotnet remove package Serilog
List packages, and outdated onesdotnet list package dotnet list package --outdated
Reference another projectdotnet add reference ../Core/Core.csproj
Make a solution and add projectsdotnet new sln dotnet sln add MyApp/MyApp.csproj.NET 10 writes the newer .slnx format
Restore packagesdotnet restorebuild, run and test do it for you
Publish for deploymentdotnet publish -c Release -o outRelease is the default for publish since .NET 8
One self-contained filedotnet publish -r linux-x64 --self-contained -p:PublishSingleFile=trueRuns on a machine with no .NET installed. win-x64, osx-arm64 and so on
Tidy the code styledotnet format
Install a command-line tooldotnet tool install -g dotnet-efTools go in ~/.dotnet/tools
Clear build outputdotnet clean

dotnet run app.cs is the quickest way to try something on .NET 10: the file is just statements, no class or Main needed. Once there is more than one file, dotnet new console gives you a project, and the .csproj is where the target framework, nullable setting and packages live.

Types and nullable references

Every type is either a value type (int, double, bool, struct, enum), which holds its data directly and is copied on assignment, or a reference type (string, arrays, classes, records), which is a reference to an object. With nullable references on, which every new project template does, a reference type can only hold null if you add ? to it, and the compiler warns you before a null gets where it should not.

TaskCodeNotes
Integerint n = 42;32 bits everywhere. int is an alias for System.Int32
Wider integerlong big = 9_000_000_000L;64 bits. The _ is a digit separator
Floating pointdouble d = 3.14; float f = 3.14f;double is the default for decimals
Exact decimals, for moneydecimal price = 19.99m;Base 10, so 0.1m + 0.2m is exactly 0.3m. The m is required
Booleanbool done = false;Not a number, so if (1) does not compile
Characterchar c = 'A';A 16-bit UTF-16 unit
Textstring name = "Ada";string and String are the same type
Let the compiler infer the typevar names = new List<string>();Local variables only. Still statically typed
Infer the type from the leftList<string> names = new();Target-typed new. Handy for fields
Compile-time constantconst int MaxUsers = 100;Numbers, strings, bools and enums only
Set once, at run timereadonly DateTime created = DateTime.UtcNow;A field. Assignable only here or in a constructor
Value type that can be nullint? age = null;age.HasValue, age.Value, age ?? 0
Reference that can be nullstring? middleName = null;Without the ?, the compiler warns when null is assigned
Turn nullable checking on<Nullable>enable</Nullable>In the .csproj. Or #nullable enable at the top of one file
Value, or a fallback if nullstring shown = middleName ?? "none";
Assign only if nullcache ??= new Dictionary<string, int>();
Member access that stops at nullint? length = middleName?.Length;null if middleName is null, instead of throwing
Assign through ?.customer?.Order = order;C# 14. Does nothing when customer is null
Promise the compiler it is not nullmiddleName!.LengthSilences the warning. Checks nothing at run time
Check for nullif (x is null) { } if (x is not null) { }Safe even if the type overloads ==
Throw if an argument is nullArgumentNullException.ThrowIfNull(name);
Convert to another typedouble avg = (double)total / count;Cast one side, or the division is whole-number
Cut a decimal down to an intint n = (int)3.99;3. Truncates toward zero
Text to a numberint n = int.Parse("42"); decimal d = decimal.Parse("2.5");Throws FormatException on bad input
Text to a number, safelyif (int.TryParse(input, out int n)) { }false instead of an exception
Number to text42.ToString() 255.ToString("X")The second gives FF, in hex
Largest and smallest valuesint.MaxValue long.MinValue
Hex and binary literals0xFF 0b1010_1010No octal literals, so 010 is just 10
Fail on overflow instead of wrappingint total = checked(a + b);Throws OverflowException. a + b silently wraps
Default value of a typedefault(int) default(string)0 and null
Group values without a classvar person = (Name: "Ada", Age: 36);A tuple. person.Name, person.Age
Take a tuple apartvar (name, age) = person;
Swap two variables(a, b) = (b, a);
Name of a variable, as textnameof(age)"age". Survives a rename, unlike a string
Integers of any sizeBigInteger.Pow(2, 100)System.Numerics

Strings and interpolation

A string can never change. Every method that seems to modify one returns a new string, so keep the result: s.ToUpper() on its own line does nothing. Unlike Java, == compares the text.

TaskCodeNotes
Put values in a stringstring msg = $"{name} is {age}";An interpolated string. Any expression goes in the braces
Format numbers$"{price:C} {ratio:P1} {n:N0} {x:F2}"Currency, percent, thousands separators, 2 decimal places
Pad and align$"{name,-8}|{n,5}"Negative aligns left, positive aligns right
Format a date$"{DateTime.Now:yyyy-MM-dd HH:mm}"
A literal brace$"{{ {value} }}"Double it
Backslashes as they are@"C:\temp\notes.txt"A verbatim string. Write a double quote as two
Text with quotes in it"""{"name": "Ada"}"""A raw string literal. Nothing inside needs escaping
Raw text with values in it$$"""{"name": "{{name}}"}"""$$ means {{ }} interpolates and single braces stay as they are
Lengthname.LengthA property, no brackets
One charactername[0] name[^1]^1 is the last one
Part of a strings.Substring(1, 3) s[1..4]Substring takes a start and a length, the range a start and an end
Compare textanswer == "yes"Compares the text, and is fine with null
Compare ignoring casestring.Equals(a, b, StringComparison.OrdinalIgnoreCase)
Alphabetical orderstring.Compare(a, b, StringComparison.Ordinal)Negative if a comes first, 0 if equal
Contains, starts with, ends withs.Contains("cat") s.StartsWith('/') s.EndsWith(".cs")
Find texts.IndexOf("cat")-1 if it is not there
Replace every matchs.Replace("cat", "dog")Plain text. Regex.Replace takes a pattern
Splitstring[] parts = "a,b,,c".Split(',', StringSplitOptions.RemoveEmptyEntries);Without the option you get an empty string for ,,
Joinstring.Join(", ", names)
Trim whitespaces.Trim() s.TrimStart() s.TrimEnd('/')
Null, empty or blankstring.IsNullOrEmpty(s) string.IsNullOrWhiteSpace(s)Static, so they are safe to call on null
Change cases.ToUpper() s.ToLowerInvariant()The Invariant versions ignore the user's language settings
Repeat a characternew string('-', 20)
Reversestring.Concat(s.Reverse())
Build one up in a loopvar sb = new StringBuilder(); sb.Append(word).Append(','); string result = sb.ToString();+= in a loop copies the whole string every time
Loop over the charactersforeach (char c in s) { }
Test a characterchar.IsDigit(c) char.IsLetter(c) char.IsWhiteSpace(c)
Split into liness.Split('\n')Call s.ReplaceLineEndings() first if it might have \r\n

For text across several lines, use a raw string literal: three double quotes on their own line, the text, then three closing double quotes on their own line. The closing quotes set the indentation, and whitespace to their left is removed from every line.

Collections

Arrays have a fixed length. List<T> is the growable default, Dictionary<TKey, TValue> maps keys to values, and HashSet<T> holds unique values. The generic collections are in System.Collections.Generic, which every project imports for you.

TaskCodeNotes
Array with valuesint[] nums = [3, 1, 2];A collection expression. Works for arrays, lists and spans
Array with a sizevar names = new string[5];Filled with null. Numbers start at 0, bools at false
Length of an arraynums.LengthCount on a List. Length on arrays and strings
Sort an arrayArray.Sort(nums);
Two dimensionsint[,] grid = new int[3, 4]; grid[1, 2] = 5;3 rows of 4. grid.GetLength(0) is 3
Growable listList<string> names = ["Ada", "Linus"];Or new List<string>() for an empty one
Addnames.Add("Grace"); names.Insert(0, "Alan");Insert puts it at index 0
Add severalnames.AddRange(["Barbara", "Edsger"]);
Read and replace by indexnames[0] names[^1] names[0] = "Barbara";^1 is the last item
Count and containsnames.Count names.Contains("Ada")
Removenames.Remove("Ada"); names.RemoveAt(0);Remove returns false if it was not there
Remove every matchnames.RemoveAll(n => n.StartsWith('A'));Safe, unlike removing inside a foreach
Find onenames.Find(n => n.Length > 4)null or 0 if nothing matches
Slicenames[1..3]A new list of items 1 and 2
Sortnames.Sort(); people.Sort((a, b) => a.Age.CompareTo(b.Age));
Combine listsint[] all = [.. first, .. second];The spread .. copies every item in
Key to valuevar ages = new Dictionary<string, int> { ["Ada"] = 36 };
Add or replaceages["Bob"] = 40;
Add, failing if it existsages.Add("Ada", 36);Throws ArgumentException for a duplicate key
Look upages["Ada"]Throws KeyNotFoundException for a missing key
Look up, safelyif (ages.TryGetValue("Ada", out int age)) { }
Look up with a defaultages.GetValueOrDefault("Zoe", 0)
Is the key there, remove a keyages.ContainsKey("Ada") ages.Remove("Ada");
Count occurrencescounts[word] = counts.GetValueOrDefault(word) + 1;
Loop over keys and valuesforeach (var (name, age) in ages) { }Takes each KeyValuePair apart. Also ages.Keys and ages.Values
Dictionary sorted by keyvar sorted = new SortedDictionary<string, int>(ages);
Unique valuesvar seen = new HashSet<string>(); seen.Add(x)Add returns false if it was already there
Union, intersection, differencea.UnionWith(b); a.IntersectWith(b); a.ExceptWith(b);Each one changes a
Stackvar stack = new Stack<int>(); stack.Push(1); stack.Peek(); stack.Pop();
Queuevar queue = new Queue<int>(); queue.Enqueue(1); queue.Dequeue();Throws when empty. TryDequeue does not
Always get the smallest nextvar pq = new PriorityQueue<string, int>(); pq.Enqueue("task", 2); pq.Dequeue();Lowest priority number comes out first
Read-only viewIReadOnlyList<string> view = names.AsReadOnly();
Lookup table built onceFrozenDictionary<string, int> codes = ages.ToFrozenDictionary();System.Collections.Frozen. Slower to build, faster to read
Any number of argumentsstatic int Sum(params int[] nums) => nums.Sum();Call it as Sum(1, 2, 3)
params without an arraystatic int Sum(params ReadOnlySpan<int> nums)C# 13. Also IEnumerable<T> and List<T>

Adding or removing items while a foreach loops over the same list throws InvalidOperationException: Collection was modified. Use RemoveAll, or loop over a copy with foreach (var x in list.ToList()).

LINQ

LINQ is a set of methods on anything you can loop over (IEnumerable<T>): filter, transform, sort, group and add up, chained one after another. A query does not run when you write it, only when you loop over it or call something like ToList, Count or First. It lives in System.Linq, which every project imports for you.

TaskCodeNotes
Keep matchesnums.Where(n => n > 10)
Transform each onenames.Select(n => n.ToUpper())
With the index toonames.Select((n, i) => $"{i}: {n}")
Run it into a list or array.ToList() .ToArray()Nothing runs until you do this or loop over it
Into a dictionarypeople.ToDictionary(p => p.Id)Throws ArgumentException on a duplicate key
Into a set.ToHashSet()
Sortpeople.OrderBy(p => p.Age).ThenBy(p => p.Name)Stable, so equal items keep their order
Largest firstpeople.OrderByDescending(p => p.Age)
Sort plain valuesnums.Order() nums.OrderDescending()
First match.First(n => n > 10) .FirstOrDefault(n => n > 10)First throws InvalidOperationException if there is none. FirstOrDefault gives 0 or null
Exactly one match.Single(p => p.Id == id)Throws if there are none or several
Last match.Last() .LastOrDefault()
Any, all, how many.Any() .Any(n => n < 0) .All(n => n > 0) .Count(n => n > 0)
Add uppeople.Sum(p => p.Age) people.Average(p => p.Age)Average throws on an empty sequence
Smallest and largest valuepeople.Min(p => p.Age) people.Max(p => p.Age)
The object with the largest valuepeople.MaxBy(p => p.Age) people.MinBy(p => p.Age)The whole person, not just the age
Grouppeople.GroupBy(p => p.City)Each group has a Key and is itself a sequence
Count each valuewords.CountBy(w => w).NET 9. Pairs of key and count
Drop duplicates.Distinct() .DistinctBy(p => p.Email)
Page through.Skip(20).Take(10)
Take while a condition holds.TakeWhile(n => n < 100) .SkipWhile(n => n < 100)
Batches of three.Chunk(3)Each batch is an array
Flatten lists of listsorders.SelectMany(o => o.Items)
Inner joincustomers.Join(orders, c => c.Id, o => o.CustomerId, (c, o) => new { c.Name, o.Total })Customers with no orders are left out
Left joincustomers.LeftJoin(orders, c => c.Id, o => o.CustomerId, (c, o) => new { c.Name, Total = o?.Total }).NET 10. o is null for a customer with no orders
Pair up two sequencesnames.Zip(ages)Tuples of (First, Second). Stops at the shorter one
Loop with the indexforeach (var (i, name) in names.Index()) { }.NET 9
Combine into one value.Aggregate((a, b) => a * b)
A range of numbersEnumerable.Range(1, 10)Start and count, so 1 to 10
Set operationsa.Union(b) a.Intersect(b) a.Except(b)
Query syntaxfrom p in people where p.Age > 30 orderby p.Name select p.NameCompiles to the same method calls

Because a query runs every time you loop over it, a query over a database or a slow method does the work again on each loop. Call ToList() once if you need the results more than once.

Control flow

TaskCodeNotes
if, else if, elseif (n > 0) { } else if (n < 0) { } else { }The condition must be a bool
Pick one of two valuesstring label = n % 2 == 0 ? "even" : "odd";
switch that gives a valuestring size = n switch { < 10 => "small", < 100 => "medium", _ => "large" };A switch expression. _ matches anything left
switch statementswitch (day) { case DayOfWeek.Saturday: case DayOfWeek.Sunday: Rest(); break; default: Work(); break; }No fall-through: a case with code must end in break, return or throw
Counting loopfor (int i = 0; i < n; i++) { }
Loop over anythingforeach (var name in names) { }
Count downfor (int i = n - 1; i >= 0; i--) { }
while and do-whilewhile (queue.Count > 0) { } do { } while (Retry());do-while always runs at least once
Skip to the next round, or stopcontinue; break;
Leave nested loopsreturn;No labelled break. Move the loops into a method, or use goto
Produce values one at a timeIEnumerable<int> Evens() { for (int i = 0; ; i += 2) yield return i; }An iterator. Runs only as far as the caller reads
Helper function inside a methodint Square(int x) => x * x;A local function. Can be declared after it is used

Pattern matching

A pattern tests a value's type, shape or contents, and can name the parts it matched. Patterns work after is, in switch expressions and in case labels.

TaskCodeNotes
Check a type and use itif (obj is string s && s.Length > 0) { }s only exists where the check passed
One of several valuesif (day is DayOfWeek.Saturday or DayOfWeek.Sunday) { }
A rangeif (age is >= 13 and <= 19) { }
Not somethingif (input is not (null or "")) { }
Match on propertiesif (order is { Status: "paid", Total: > 100 }) { }
Nested propertiesif (person is { Address.City: "London" }) { }
Not null, and name itif (user is { } u) { }{ } matches anything that is not null
switch on the typestring desc = shape switch { Circle c => $"circle {c.Radius}", Square { Side: 0 } => "empty", Square s => $"square {s.Side}", null => "none", _ => "other" };Arms are tried from the top
Add a condition to an armn switch { int x when x % 2 == 0 => "even", _ => "odd" }
Take a record apartpoint switch { (0, 0) => "origin", (var x, 0) => $"on the x axis at {x}", _ => "elsewhere" }Positional pattern. Works on records and anything with Deconstruct
Match two values at once(a, b) switch { (true, true) => "both", (true, false) or (false, true) => "one", _ => "neither" }A tuple pattern
Match an array or listif (args is [var first, .. var rest]) { }A list pattern. rest gets everything after the first
Check the endsif (parts is ["cd", var dir]) { } if (nums is [1, .., 9]) { }.. matches any number of items in between
In a case labelcase int n when n > 0:

C# has no sealed type hierarchies, so a switch over a base type can never be proved complete. The compiler warns with CS8509 until you add a _ arm, and a value that matches nothing throws SwitchExpressionException.

Classes, records and structs

TaskCodeNotes
Define a classpublic class Account { private decimal _balance; }One file can hold several types, named anything
Constructorpublic Account(string owner) { Owner = owner; }No return type
Constructor in the class linepublic class Account(string owner) { public string Owner { get; } = owner; }A primary constructor. owner is in scope for the whole class
One constructor calling anotherpublic Account() : this("anonymous") { }
Create an objectvar a = new Account("Ada"); Account b = new("Bob");
Set properties as you create itvar p = new Person { Name = "Ada", Age = 36 };An object initialiser
Methodpublic void Deposit(decimal amount) { _balance += amount; }
One-expression methodpublic decimal Doubled() => _balance * 2;
Static class and methodpublic static class MathUtil { public static int Add(int a, int b) => a + b; }Called as MathUtil.Add(1, 2)
Who can see itpublic private protected internalMembers default to private, types to internal (this project only)
Inheritpublic class Savings : AccountOne base class, any number of interfaces, after the same colon
Call the base constructorpublic Savings(string owner) : base(owner) { }
Let subclasses replace a methodpublic virtual decimal Fee() => 1m;Methods are not virtual unless you say so, unlike Java
Replace itpublic override decimal Fee() => 0m;
Call the base versionbase.ToString()
Class that must be inheritedpublic abstract class Shape { public abstract double Area(); }Cannot be created with new
Stop inheritancepublic sealed class Money { }
Define an interfacepublic interface IShape { double Area(); }Names start with I by convention
Implement itpublic class Circle(double r) : IShape { public double Area() => Math.PI * r * r; }
Data type in one linepublic record Point(int X, int Y);Constructor, init-only properties, equality by value, ToString and Deconstruct
Copy with one changevar moved = p with { Y = p.Y + 1 };Records only. The original is unchanged
Record that is a value typepublic readonly record struct Money(decimal Amount, string Currency);
Structpublic struct Vector { public double X; public double Y; }A value type: copied on assignment, never null
Fixed set of valuespublic enum Size { Small, Medium, Large }
Enum helpersEnum.Parse<Size>("Small") Enum.GetValues<Size>() size.ToString()
Flags that combine[Flags] public enum Perm { Read = 1, Write = 2, Run = 4 }Perm.Read | Perm.Write, then p.HasFlag(Perm.Write)
Add a method to a type you do not ownpublic static class StringExtensions { public static bool IsBlank(this string s) => string.IsNullOrWhiteSpace(s); }An extension method. Called as name.IsBlank()
Add a property to a type you do not ownpublic static class StringExtensions { extension(string s) { public bool IsBlank => string.IsNullOrWhiteSpace(s); } }C# 14. Extension members. Called as name.IsBlank
Equality by value on a classpublic override bool Equals(object? obj) => obj is Point p && X == p.X && Y == p.Y;
Hash to matchpublic override int GetHashCode() => HashCode.Combine(X, Y);Override both or neither. A record does both for you
Custom text for printingpublic override string ToString() => $"({X}, {Y})";
Program entry pointConsole.WriteLine("Hello");The whole of Program.cs. Top-level statements, no class or Main
Entry point, long formstatic void Main(string[] args) { }Or static async Task Main to use await

Class or record? A record is for data: it compares by value, so two records with the same fields are equal, and with makes a changed copy. A class compares by reference and suits objects whose state changes. A struct is for small values, a few fields at most, that you want copied rather than shared.

Properties

A property looks like a field from outside but runs code on get and set, so a class can add validation later without breaking its callers. Public data goes in properties, not public fields.

TaskCodeNotes
Read and writepublic string Name { get; set; }An auto-property. The compiler writes the hidden field
With a starting valuepublic int Retries { get; set; } = 3;
Read-onlypublic string Id { get; }Set only in a constructor or initialiser
Set only while creatingpublic string Name { get; init; }Works in an object initialiser, then fixed
Must be set by the callerpublic required string Email { get; init; }new User() without Email = ... does not compile
Public read, private writepublic decimal Balance { get; private set; }
Worked out on each readpublic string FullName => $"{First} {Last}";No setter, no stored value
Run code on setpublic string Name { get; set => field = value.Trim(); }C# 14. field is the hidden backing field
Your own backing fieldprivate int _age; public int Age { get => _age; set => _age = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value)); }value is whatever the caller assigned
Shared by every objectpublic static int Count { get; private set; }
In an interfacestring Name { get; }Implementations may add a setter
Index like an arraypublic string this[int i] => _items[i];An indexer. Called as thing[0]

Exceptions

C# has no checked exceptions: nothing forces a caller to catch or declare what a method throws. Catch an exception where there is something useful to do about it, and let the rest travel up.

TaskCodeNotes
Catchtry { Risky(); } catch (IOException ex) { Console.Error.WriteLine(ex.Message); }
Catch specific types firstcatch (FileNotFoundException) { } catch (IOException) { }The compiler rejects them in the wrong order
Catch only when a condition holdscatch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }An exception filter. Anything else keeps travelling
Always runfinally { Cleanup(); }Runs whether the try succeeded, threw or returned
Dispose when the scope endsusing var reader = new StreamReader(path);A using declaration. Closed however the method ends
Dispose at the end of a blockusing (var reader = new StreamReader(path)) { }
Throwthrow new ArgumentException("must be positive", nameof(age));
Throw inside an expression_name = name ?? throw new ArgumentNullException(nameof(name));
Rethrowcatch (Exception ex) { Log(ex); throw; }throw; keeps the stack trace. throw ex; resets it
Wrap one and keep the causethrow new InvalidOperationException("could not load config", ex);The original is in InnerException
Your own exceptionpublic class OutOfStockException(string message) : Exception(message);
Guard clausesArgumentOutOfRangeException.ThrowIfNegative(age); ArgumentException.ThrowIfNullOrWhiteSpace(name);The message names the argument for you
Print the detailsConsole.Error.WriteLine(ex);ToString() includes the type, message, stack trace and inner exceptions
Common ones to throwArgumentException ArgumentNullException InvalidOperationException NotSupportedExceptionBad input, bad state, and not implemented here

Lambdas and delegates

A delegate is a type that holds a method, and a lambda is a method written inline. Func and Action cover almost every case: Func returns a value (its last type argument), Action returns nothing.

TaskCodeNotes
Takes a value, returns a valueFunc<int, int> twice = x => x * 2;
Several argumentsFunc<int, int, int> add = (a, b) => a + b;
Returns nothingAction<string> greet = name => Console.WriteLine($"hi {name}");
Takes nothingFunc<DateTime> now = () => DateTime.UtcNow;
Returns a boolPredicate<int> isEven = n => n % 2 == 0;Or Func<int, bool>, which LINQ uses
Several statementsFunc<string, int> score = s => { int n = s.Length; return n * n; };
Let the compiler pick the typevar add = (int a, int b) => a + b;Gives a Func<int, int, int>
Default argumentvar greet = (string name = "you") => $"hi {name}";
Call ittwice(5) greet("Ada")
Point at an existing methodFunc<string, int> parse = int.Parse;A method group. No brackets
Pass one to a methodstatic int Apply(Func<int, int> f, int x) => f(x);
Use a local variable insideint limit = 10; Func<int, bool> small = x => x < limit;Captures the variable itself, so later changes are seen
Stop it capturing anythingFunc<int, int> sq = static x => x * x;A compile error if it touches a local
Declare an eventpublic event EventHandler? Changed;
Raise itChanged?.Invoke(this, EventArgs.Empty);?. because it is null with no subscribers
Subscribe and unsubscribebutton.Changed += OnChanged; button.Changed -= OnChanged;

async and await

An async method returns a Task (or Task<T> for a result) straight away and finishes the work later. await waits for a Task without blocking the thread, so a web server or UI can do other work in the meantime. The convention is to end async method names in Async.

TaskCodeNotes
Async method with a resultasync Task<string> LoadAsync(string path) => await File.ReadAllTextAsync(path);
Async method, no resultasync Task SaveAsync(string path, string text) { await File.WriteAllTextAsync(path, text); }Task, not void
Call itstring text = await LoadAsync("notes.txt");await works in top-level code too
Start several, wait for allstring[] pages = await Task.WhenAll(urls.Select(u => http.GetStringAsync(u)));Runs them at the same time. Results come back in order
Wait for the first to finishTask<string> first = await Task.WhenAny(a, b);
Handle each as it finishesawait foreach (var task in Task.WhenEach(tasks)) { Use(await task); }.NET 9
Pauseawait Task.Delay(TimeSpan.FromSeconds(1));
Give up after a time limitawait task.WaitAsync(TimeSpan.FromSeconds(5));Throws TimeoutException
Make something cancellableasync Task RunAsync(CancellationToken ct) { await Task.Delay(1000, ct); }Pass the token to everything you await
Cancel after a timeusing var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await RunAsync(cts.Token);Throws OperationCanceledException
Check for cancellation in a loopct.ThrowIfCancellationRequested();
Heavy CPU work off this threadint result = await Task.Run(() => Crunch(data));
Loop in parallel, a few at a timeawait Parallel.ForEachAsync(urls, new ParallelOptions { MaxDegreeOfParallelism = 4 }, async (url, ct) => await FetchAsync(url, ct));
Return values one by one, asyncasync IAsyncEnumerable<int> TicksAsync() { for (int i = 0; i < 3; i++) { await Task.Delay(100); yield return i; } }
Loop over themawait foreach (int tick in TicksAsync()) { }
Already have the resultreturn Task.FromResult(42); return Task.CompletedTask;From a method that is not marked async
Lock a block of codeprivate readonly Lock _gate = new(); lock (_gate) { }.NET 9 and C# 13. An object instead of Lock before that. No await inside
Limit how many run at oncevar gate = new SemaphoreSlim(3); await gate.WaitAsync(); try { } finally { gate.Release(); }

Never block on a Task with .Result or .Wait(): it ties up a thread and can deadlock in apps with a UI or a synchronisation context. Await it instead. And avoid async void everywhere except event handlers, because nothing can await it or catch what it throws.

Files, dates, JSON and other everyday APIs

TaskCodeNotes
PrintConsole.WriteLine($"total: {total}"); Console.Write("no newline");
Print an errorConsole.Error.WriteLine("failed");
Read a line from the keyboardstring? line = Console.ReadLine();null when the input has ended
Read a whole filestring text = File.ReadAllText("notes.txt");System.IO. ReadAllTextAsync to await it
Read the linesstring[] lines = File.ReadAllLines(path);
Stream a big file line by lineforeach (string line in File.ReadLines(path)) { }Reads as you go, unlike ReadAllLines
Write a fileFile.WriteAllText(path, text);Replaces it. AppendAllText adds to the end
Check and createFile.Exists(path) Directory.CreateDirectory(dir)CreateDirectory is fine if it already exists
List a folderDirectory.GetFiles(dir, "*.cs", SearchOption.AllDirectories)
Build a pathPath.Combine("data", "users.json")Right separator for the platform
Parts of a pathPath.GetFileName(path) Path.GetExtension(path) Path.GetDirectoryName(path)
NowDateTime.UtcNow DateTimeOffset.NowUtcNow for timestamps. DateTimeOffset keeps the offset from UTC
A date with no timevar day = new DateOnly(2026, 9, 27); DateOnly.FromDateTime(DateTime.Today)Months are 1 to 12. TimeOnly for a time with no date
Date arithmeticday.AddDays(30) DateTime.UtcNow.AddHours(-1)Returns a new value
Time between two dates(end - start).TotalDaysSubtracting DateTimes gives a TimeSpan
A length of timeTimeSpan.FromMinutes(90).TotalHours1.5
Parse and format a dateDateOnly.Parse("2026-09-27") day.ToString("dd/MM/yyyy")
Object to JSONstring json = JsonSerializer.Serialize(person);System.Text.Json
JSON to an objectPerson? p = JsonSerializer.Deserialize<Person>(json);
camelCase JSON, as web APIs useJsonSerializer.Serialize(person, JsonSerializerOptions.Web).NET 9
HTTP GETstring body = await http.GetStringAsync(url);Make one HttpClient and reuse it, never one per request
HTTP GET, straight into a typeUser? user = await http.GetFromJsonAsync<User>(url);System.Net.Http.Json
Random numberint roll = Random.Shared.Next(1, 7);1 to 6. The upper bound is excluded
ShuffleRandom.Shared.Shuffle(array);Shuffles an array in place. On .NET 10, LINQ's .Shuffle() returns a shuffled copy of any sequence
MathsMath.Max(a, b) Math.Abs(x) Math.Pow(2, 10) Math.Sqrt(x)
RoundMath.Round(2.5) Math.Round(2.5, MidpointRounding.AwayFromZero)2 and 3. The default rounds halves to the even number
Keep a number within boundsMath.Clamp(value, 0, 100)
Test with a regexRegex.IsMatch(email, @"^\S+@\S+$")System.Text.RegularExpressions. A verbatim string saves escaping
Capture groupsMatch m = Regex.Match(email, @"(\w+)@(\w+)"); if (m.Success) { string user = m.Groups[1].Value; }
Replace with a regexRegex.Replace(s, @"\s+", " ")
Unique IDGuid.NewGuid() Guid.CreateVersion7()Version 7 sorts by creation time. .NET 9
Environment variableEnvironment.GetEnvironmentVariable("HOME")null if it is not set
Time somethingvar sw = Stopwatch.StartNew(); sw.Elapsed.TotalMillisecondsSystem.Diagnostics
Thread-safe dictionary and counternew ConcurrentDictionary<string, int>() Interlocked.Increment(ref count)System.Collections.Concurrent and System.Threading

Counting words, start to finish

Splitting a string, counting with LINQ, and sorting by two keys in one program. It prints the three most common words, ties broken alphabetically. Save it as words.cs and run it with dotnet run words.cs: on .NET 10 a single file needs no project.

string text = "the cat sat on the mat and the cat slept";
 
var top = text
    .Split(' ', StringSplitOptions.RemoveEmptyEntries)
    .CountBy(word => word)                     // (Key, Value) pairs
    .OrderByDescending(pair => pair.Value)     // most frequent first
    .ThenBy(pair => pair.Key)                  // then A to Z
    .Take(3);
 
foreach (var (word, count) in top)
{
    Console.WriteLine($"{word,-5} {count}");
}
// the   3
// cat   2
// and   1

CountBy arrived in .NET 9. On .NET 8, use .GroupBy(word => word).Select(g => KeyValuePair.Create(g.Key, g.Count())) in its place.

OrderBy is a stable sort, so items that compare equal keep their original order. List<T>.Sort and Array.Sort are not stable: they use introsort, which starts as a quick sort, switches to heap sort if the recursion goes too deep, and finishes small ranges with insertion sort. All three are on the site as step-through visualisations.

Records and pattern matching

Records supply the constructors, equality and ToString, and a switch expression takes each shape apart. C# cannot prove a switch over a base type is complete, so the _ arm is needed: leave it out and the compiler warns with CS8509.

Shape[] shapes = [new Circle(1), new Rectangle(3, 4), new Square(2)];
 
foreach (var shape in shapes)
{
    Console.WriteLine($"{shape,-35} {Area(shape),6:F2}");
}
Console.WriteLine($"{"total",-35} {shapes.Sum(Area),6:F2}");
// Circle { Radius = 1 }                 3.14
// Rectangle { Width = 3, Height = 4 }  12.00
// Square { Side = 2 }                   4.00
// total                                19.14
 
Console.WriteLine(new Square(2) == new Square(2));   // True: equal by value
Console.WriteLine(new Rectangle(3, 4) with { Height = 5 });
// Rectangle { Width = 3, Height = 5 }
 
static double Area(Shape shape) => shape switch
{
    Circle c => Math.PI * c.Radius * c.Radius,
    Rectangle(var w, var h) => w * h,
    Square { Side: var side } => side * side,
    _ => throw new ArgumentOutOfRangeException(nameof(shape)),
};
 
abstract record Shape;
record Circle(double Radius) : Shape;
record Square(double Side) : Shape;
record Rectangle(double Width, double Height) : Shape;

The three arms use three kinds of pattern: Circle c matches the type and names it, Rectangle(var w, var h) takes the record apart by position, and Square { Side: var side } reads a property by name. Top-level statements come first in the file, and the types they use go after them.

Reading a file, with async, exceptions and nullable references

await reads the file without blocking, a when filter catches only the failure there is a sensible fallback for, and nullable references make the "not a setting" case a null that the compiler forces you to check.

const string path = "app.settings";
await File.WriteAllTextAsync(path, """
    # sample settings
    port = 8080
    host=localhost
    not a setting
    """);
 
List<Setting> settings;
try
{
    settings = await LoadAsync(path);
}
catch (IOException ex) when (ex is FileNotFoundException or DirectoryNotFoundException)
{
    Console.Error.WriteLine($"No config at {path}, using defaults");
    settings = [];
}
 
int port = settings
    .Where(s => s.Key == "port")
    .Select(s => int.Parse(s.Value))
    .FirstOrDefault(80);
 
Console.WriteLine($"{settings.Count} settings, port {port}");
// 2 settings, port 8080
 
static async Task<List<Setting>> LoadAsync(string path, CancellationToken ct = default)
{
    string[] lines = await File.ReadAllLinesAsync(path, ct);
    List<Setting> settings = [];
    foreach (string line in lines)
    {
        // Parse returns Setting?, so the compiler will not let you
        // use the result until you have checked it for null.
        if (Setting.Parse(line) is { } setting)
        {
            settings.Add(setting);
        }
    }
    return settings;
}
 
record Setting(string Key, string Value)
{
    // "port = 8080" becomes a Setting. Comments, blanks and junk become null.
    public static Setting? Parse(string line)
    {
        int eq = line.IndexOf('=');
        if (string.IsNullOrWhiteSpace(line) || line.StartsWith('#') || eq < 0)
        {
            return null;
        }
        return new Setting(line[..eq].Trim(), line[(eq + 1)..].Trim());
    }
}

FileNotFoundException and DirectoryNotFoundException both inherit from IOException, so the filter handles the one failure that has a fallback and lets every other I/O error carry on up. The raw string (the """ block) keeps its line breaks, and the indentation to the left of the closing quotes is removed.

Properties, required members and init

A class with every kind of property in one place: a required one the caller must set, an init one fixed after creation, one that trims its input with the C# 14 field keyword, one with a private setter, and one worked out on each read.

var ada = new User { Email = " ada@example.com ", Name = "Ada" };
ada.Deposit(25m);
ada.Deposit(17.5m);
 
Console.WriteLine($"{ada.Name} <{ada.Email}> has {ada.Balance:F2}, {ada.Status}");
// Ada <ada@example.com> has 42.50, active
 
// ada.Email = "x";     error CS8852: init-only, fixed after creation
// ada.Balance = 100m;  error CS0272: the setter is private
// new User();          error CS9035: Email is required
 
class User
{
    public required string Email { get; init => field = value.Trim(); }
    public string Name { get; init; } = "anonymous";
    public decimal Balance { get; private set; }
    public string Status => Balance > 0 ? "active" : "empty";
 
    public void Deposit(decimal amount)
    {
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
        Balance += amount;
    }
}

The field keyword is new in C# 14. Before it, trimming on set meant declaring a private backing field yourself and writing both the get and the init by hand.

Gotchas

The mistakes that turn up in almost every C# codebase at some point.

Looks rightWhat actually happensDo this instead
Math.Round(2.5)2. Halves round to the even number by defaultMath.Round(2.5, MidpointRounding.AwayFromZero)
0.1 + 0.2 == 0.3False: double is binary floating pointdecimal for money: 0.1m + 0.2m == 0.3m
int avg = total / count; then cast itThe division already happened in whole numbers(double)total / count
list.First(x => ...) when nothing matchesInvalidOperationException: Sequence contains no matching elementFirstOrDefault, then check for null or 0
dict[key] for a missing keyKeyNotFoundExceptionTryGetValue or GetValueOrDefault
list.Remove(x) inside foreach (var x in list)InvalidOperationException: Collection was modifiedlist.RemoveAll(...)
Looping over the same LINQ query twiceThe whole query runs twice, including any database call.ToList() once, then reuse the list
for (int i = 0; i < 3; i++) actions.Add(() => Console.Write(i));Prints 333: every lambda shares the one iCopy it first: int copy = i;, or use foreach
task.Result or task.Wait()Blocks the thread, and can deadlock in a UI appawait task
async void DoWorkAsync()Callers cannot await it, and its exceptions crash the processasync Task DoWorkAsync()
catch (Exception ex) { throw ex; }The stack trace now starts here, not where it failedthrow;
name!.Length! only silences the compiler, so a null still throws NullReferenceExceptionCheck it: name?.Length ?? 0
points[0].X = 5; on a List<Point> of structsError CS1612: the indexer returns a copyReplace the whole item, or make it a class
new HttpClient() for every requestRuns out of sockets under loadOne shared instance, or IHttpClientFactory in ASP.NET Core
DateTime.Now in stored timestampsLocal time, which moves with the clocksDateTime.UtcNow or DateTimeOffset.UtcNow
s += word; in a long loopCopies the whole string every time roundStringBuilder or string.Join

Common questions

Which version of C# does this cheat sheet cover?

C# 14 on .NET 10, the current long-term support (LTS) release, checked with the .NET 10 SDK. Many projects are still on .NET 8, the previous LTS, which ships C# 12, so that is the baseline here: anything that needs something newer, such as the field keyword or extension members (C# 14), params spans (C# 13), CountBy and Task.WhenEach (.NET 9) or LeftJoin and dotnet run app.cs (.NET 10), says so in the notes column.

What is the difference between C# and .NET?

C# is the language. .NET is the runtime that runs compiled C#, plus the standard library (collections, files, HTTP, JSON) and the dotnet command-line tools. Each .NET release ships a C# version with it: .NET 8 has C# 12, .NET 9 has C# 13 and .NET 10 has C# 14. F# and Visual Basic run on .NET too. The old Windows-only .NET Framework stopped at version 4.8; new projects use the cross-platform .NET, which runs on Windows, macOS and Linux.

What is the difference between a class, a struct and a record in C#?

A class is a reference type: assigning it copies the reference, so two variables can point at the same object, and == compares identity. A struct is a value type: assigning it copies the whole value, and it can never be null, which suits small things like a point or an amount of money. A record is a class (or, as a record struct, a struct) that the compiler fills in for data: equality by value, a readable ToString, deconstruction and with for making changed copies.

What does the question mark after a type mean in C#?

On a value type, int? is a nullable int, a real type that can hold a number or null. On a reference type, string? is an annotation for the compiler: with nullable references turned on it says this variable may be null, and the compiler warns you if you use it without checking. Without the ?, a string is promised not to be null and the compiler warns if you assign null to it. The annotation does nothing at run time, so a null from outside code can still get through.

What is LINQ in C#?

LINQ (Language Integrated Query) is a set of methods like Where, Select, OrderBy and GroupBy that work on any collection, and on databases through libraries such as Entity Framework Core. You chain them to filter, transform, sort and group data without writing loops. A LINQ query is lazy: it runs when you loop over it or call ToList, Count, First and similar, and it runs again each time you do.

Why should I avoid async void and .Result?

An async void method cannot be awaited, so the caller cannot tell when it has finished, and an exception inside it cannot be caught by the caller and usually crashes the process. Return Task instead; async void is only for event handlers. Calling .Result or .Wait() on a Task blocks the thread until the task finishes, which wastes a thread on a server and can deadlock in a UI app. Use await all the way up instead.

Can I run a C# file without creating a project?

Yes, on .NET 10. Put your code in a file such as app.cs and run dotnet run app.cs. The file holds plain top-level statements with no class or Main method, and it can pull in NuGet packages with a #:package line at the top. When it grows, dotnet project convert app.cs turns it into a normal project. On older versions you need dotnet new console first.

How is C# different from Java?

They look alike and solve similar problems, but differ in the details. C# has properties instead of getters and setters, value types you can define yourself (structs), LINQ, async and await built into the language, and no checked exceptions. Methods are not virtual unless marked virtual, and == compares strings by their text. Java has sealed hierarchies that make a switch provably complete, and its generics are erased at run time where C#'s are not.

See all cheat sheets

Want this explained by a cat?

The videos cover the same ground in sixty seconds. If there is a tool you want a cheat sheet for next, ask.