C# Cheatsheet - Syntax, LINQ & .NET Core Reference
This reference is for .NET developers building web APIs, services, or desktop apps, focusing on the C# syntax you write every day: classes and interfaces with their access modifiers, LINQ for querying collections instead of hand-written loops, async/await for I/O-bound work, generics and delegates, and the property/event model. Unlike a flat C# feature list, entries are grouped by the language construct and note where C# behavior differs from Java or JS. After reading you should be able to replace a nested loop with a composed LINQ query, set up an async method without deadlocking, and design a type with properties, events and a delegate.
Typical Use Case
For developers writing C# services and desktop apps. This sheet gathers the syntax that is easiest to forget: type declarations from class/interface/sealed/abstract to record, the three property forms (get/set, init, expression-bodied), the split between delegates (delegate/Action/Func/Predicate) and events, the LINQ chain from filtering to aggregation, and how async/await works with CancellationToken. Use it when you need to decide quickly whether a DTO should be a record or a class, or whether a callback should be a Func or an event.
Classes & Interfaces 7
public class MyClass { }public interface IMyInterface { void Method(); }public class Derived : Base, IMyInterface { }public abstract class AbstractClass { }public sealed class SealedClass { }public static class Utils { }public record Person(string Name, int Age);Properties & Methods 6
public int Age { get; set; }public string Name { get; init; }public int Age => _age;public void Method() { }public int Add(int a, int b) => a + b;public event EventHandler MyEvent;LINQ Queries 8
var result = list.Where(x => x > 5);var result = list.Select(x => x * 2);var result = list.OrderBy(x => x.Age);var result = list.GroupBy(x => x.Category);var result = list.First(x => x.Id == 1);var result = list.Any(x => x > 0);var result = list.Sum(x => x.Price);var result = list.Aggregate((a, b) => a + b);Asynchronous 6
public async Task<int> GetDataAsync() { }await Task.Delay(1000);var result = await httpClient.GetStringAsync(url);Task.Run(() => { });await Task.WhenAll(task1, task2);CancellationTokenSource cts = new();Generics & Delegates 6
public class Stack<T> { }public T Max<T>(T a, T b) where T : IComparable<T> { }public delegate void MyDelegate(string msg);public event Action<string> OnMessage;public Func<int, int, int> add = (a, b) => a + b;public Predicate<int> isPositive = x => x > 0;Collections & Dictionaries 6
List<int> list = new() { 1, 2, 3 };Dictionary<string, int> dict = new();HashSet<int> set = new() { 1, 2, 3 };Queue<int> queue = new();Stack<int> stack = new();var tuple = (1, "hello");Parameter matrix
| Parameter | Effect | Example |
|---|---|---|
record / init | Immutable data type: positional construction plus init-only properties, ideal for DTOs | public record Person(string Name, int Age); |
Func<T, TResult> / Predicate<T> | Delegates with a return value / returning bool, for mapping and for conditions | Func<int, int, int> add = (a, b) => a + b; |
CancellationTokenSource cts = new() | Cancellation source: pass cts.Token into async methods and interrupt long work with cts.Cancel() | CancellationTokenSource cts = new(); |
async / await / Task.WhenAll | Non-blocking concurrency: WhenAll waits on several tasks at once instead of one by one | await Task.WhenAll(task1, task2); |
Common pitfalls
SymptomCompiler refuses to assign a property after the object was constructed.
CauseRecords and init-only properties cannot be reassigned after initialization, by design.
FixSwitch to a class with get/set when you really need mutable state; keep records for DTOs.
SymptomAn async method is missing await, no exception is thrown, and the task fails silently.
CauseAn async method returns a Task; without await the body never runs and exceptions stay unobserved.
FixAwait every async call, or collect the tasks and wait on them with await Task.WhenAll(...).
SymptomAPI calls inside a loop get slower and slower and the UI freezes.
CauseTask.Run or synchronous blocking I/O inside an async method occupies the thread pool.
FixAwait I/O directly (await httpClient.GetStringAsync(url)) and use Task.WhenAll for concurrency.
SymptomA public event is declared but nobody outside the class can raise it.
CauseEvents can only be raised inside the declaring type; outside code may only subscribe with += / -=.
FixIf external code must raise it, use a plain delegate field such as Action<string> plus a Raise method.
Troubleshooting
1Turn a batch of async calls into real concurrency
await Task.WhenAll(task1, task2);Awaiting one by one is serial; WhenAll runs both at once, so total time is the slower one.
2Add cancellation to a long-running task
CancellationTokenSource cts = new();Create the source, pass cts.Token into the async method, and let callers exit it via cts.Cancel().
3Check that a collection has elements before pulling values out
var result = list.Any(x => x > 0);Any is the existence check; combine it with First / Aggregate instead of relying on FirstOrDefault alone.
Command Examples
record plus init for an immutable DTO
public record Person(string Name, int Age);
var p = new Person("Ada", 36) with { Age = 37 };A record gets value equality, ToString and deconstruction for free, and the with expression copies an instance while overriding selected fields — the idiomatic "change one field, keep the rest". With init-only setters, an accidental later mutation fails to compile instead of corrupting the object at runtime.
A LINQ chain: filter, then sort, then sum
var total = orders
.Where(x => x.Status == "paid")
.OrderByDescending(x => x.Age)
.Select(x => x.Price)
.Sum();LINQ methods return IEnumerable, so a chain is lazy: nothing is computed until the terminal operator runs. Putting Where before Sum therefore shrinks the working set, and for IQueryable (EF Core) the same chain order becomes the generated SQL, where a wrong order can cost orders of magnitude.
A generic method with a constraint
public T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}The where T : IComparable<T> constraint is what makes CompareTo safe inside the body. Without it T only supports trivial checks like equality, and sorting requires a cast and boxing. Constraints are compile-time: a caller passing an unsuitable type fails the build rather than throwing at runtime.
Fan out with Task.WhenAll plus a CancellationTokenSource
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var t1 = httpClient.GetStringAsync(url1, cts.Token);
var t2 = httpClient.GetStringAsync(url2, cts.Token);
string[] results = await Task.WhenAll(t1, t2);await Task.WhenAll(...) waits for all tasks at once, which is what actually buys concurrency; using disposes the source. Passing a CancellationToken lets the caller bound the wait so one slow endpoint cannot hang the whole batch — omit it and you are stuck with the default timeout.
Prefer typed collections over untyped containers
List<int> list = new() { 1, 2, 3 };
Dictionary<string, int> dict = new() { ["id"] = 1 };
HashSet<int> seen = new() { 1, 2, 3 };
Queue<int> queue = new();
Stack<int> stack = new();List<T> and Dictionary<TKey,TValue> beat ArrayList and Hashtable: type errors surface at compile time, and generic collections avoid boxing, often worth one to two orders of magnitude for value types. Hashtable tolerates null keys and compares with Equals while Dictionary forbids both — the usual source of behaviour changes when migrating.
Common Pitfalls
- Blocking on a bare .Result or .Wait() inside an async/await method deadlocks on a synchronisation context (WinForms, WPF, classic ASP.NET pipeline): the UI thread waits for a result whose continuation needs that same UI thread. Use await everywhere — this is the hardest hang to diagnose.
- An async Task method that never awaits only produces a compiler warning, and the exception is swallowed silently into a task that completed doing nothing. Do not paper over it with an empty Task.FromResult, and do not wrap a non-awaited call in try/catch expecting it to throw.
- Every step of a LINQ chain is deferred. Assign the chain to a variable and forget a terminating operation (Count, ToList, foreach) and the query never runs, which shows up as "my code does nothing".
- var is only safe when the right-hand type is unambiguous: var d = GetData() keeps compiling after a return type changes (IEnumerable to List) while runtime behaviour or performance shifts silently. Avoid var for data structures that evolve across versions.
- Touching UI controls or non-thread-safe objects inside Task.Run throws. Use Task.Run for CPU-bound work only; for I/O-bound work just await, otherwise you burn thread-pool threads and invite races.
- With nullable reference types enabled, a missing GetValueOrDefault or explicit null check is only a warning, not an error, and surfaces later as a NullReferenceException. Raise the warning level to error during migration and fix them, or you are just deferring the crash.
Tips
- LINQ queries are lazily evaluated; they run only when the results are enumerated.
- async/await is core to asynchronous programming; avoid blocking the UI thread.
- C# 9 introduced record types, ideal for immutable data models.
FAQ
How do I choose between Func, Action and Predicate?
Decide by return type and parameter list. Func<int, int, int> takes arguments and returns a value, with the trailing type being the return type — here add = (a, b) => a + b is a two-argument function returning one result. Action<string> takes arguments and returns nothing, which is what a notification-style callback needs; public event Action<string> OnMessage on this sheet is exactly that shape. Predicate<int> is the specialised single-argument, bool-returning form — isPositive = x => x > 0 is one — and it exists so a test can be passed around as a value into filtering entry points instead of being hardcoded inline.
What problems do sealed, abstract and static each solve?
All three narrow or fix the boundaries of a type. public sealed class SealedClass blocks inheritance, which suits types whose behaviour is already complete and not meant to be extended — and sealing also lets the compiler optimise member call sites more aggressively. public abstract class AbstractClass is the opposite of static: it can only be derived from, never instantiated, so it carries the contract a set of subtypes must fulfil. public static class Utils forbids both — no inheritance, no instances, only static members — making it the natural home for stateless helpers. The question to ask is whether the type is meant to be implemented by others, frozen against extension, or is purely a namespace of functions.
When is a generic constraint like where T : IComparable<T> actually required?
Constraints tell the compiler what capabilities the type parameter has, and it only accepts your method body once that is guaranteed. public T Max<T>(T a, T b) where T : IComparable<T> is the textbook case: if the body compares a with b, T must implement IComparable<T>, otherwise the compiler has no basis for assuming the comparison is even valid. Leave the constraint off and T is treated as plain object, so comparisons, arithmetic and similar operations all fail to compile. You buy type safety plus the chance for the compiler to emit specialised code for specific value types instead of boxing everything to object.
How do I run several async requests at once instead of awaiting them one by one?
Use await Task.WhenAll(task1, task2). Awaiting one after another means the second request is not even issued until the first comes back, so the round-trip times add up. WhenAll accepts any number of already-started tasks and waits for all of them together, making the total roughly that of the slowest. You can pass more than two, or hand it a collection with Task.WhenAll(tasks). The precondition is that those tasks are running before the call: an async method yields control at its first await, so grab the task variables first and then await them together is what actually produces concurrency.
Which collection should I reach for: Dictionary, HashSet, Queue, Stack or a tuple?
Pick by the operation you need. Dictionary<string, int> holds key-value pairs and is the right structure for lookup, update and de-duplication by key. HashSet<int> keeps only distinct elements and answers membership questions in constant time, which makes it the filter for duplicates. Queue<int> is first-in-first-out, for work processed in arrival order. Stack<int> is last-in-first-out, which is what undo and backtracking need. var tuple = (1, "hello") is not a container at all: it binds a few closely related values together so a method can return two results at once without you defining a type that exists purely to carry them.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us