C# cheatsheet
Save this file from here if you want a copy.
================================================================================
CSHARP-CHEATSHEET.TXT
================================================================================
NAME
csharp-cheatsheet.txt -- offline C# / .NET reference manual
SYNOPSIS
grep -A 40 "^SECTION_NAME" csharp-cheatsheet.txt
less csharp-cheatsheet.txt
grep -n "SECTION_NAME" csharp-cheatsheet.txt
DESCRIPTION
This file is a single flat plain-text reference manual covering the
.NET CLI, the C# language, the base class library collections, LINQ,
common interview / LeetCode-style algorithms and data structures,
async programming, ASP.NET Core, Entity Framework Core, JSON, file
I/O, networking, PostgreSQL, debugging, design patterns, and general
best practices.
It is written to be grepped, not rendered. Every major topic has a
large banner header made of '=' characters. Every subsection inside
a topic has a smaller banner made of '-' characters. There is no
markdown, no bullets unless natural, and no code fences. Code is
always indented four spaces.
SUGGESTED ALIAS
Add something like this to your shell rc file:
cscs() {
if [ -z "$1" ]; then
less ~/csharp-cheatsheet.txt
else
grep -n -i --color=always "$*" ~/csharp-cheatsheet.txt | less -R
fi
}
Usage:
cscs LIST
cscs DICTIONARY
cscs "TREE DFS"
cscs DOTNET CLI
cscs LINQ
Since every section header is a unique all-caps banner line, grep -n
will point you straight at the line number, and you can jump there
directly with:
less +<line number> ~/csharp-cheatsheet.txt
TABLE OF CONTENTS
NAMESPACES
MATH
SKELETON CODE
DOTNET CLI
SOLUTIONS AND PROJECTS
NUGET
VARIABLES AND TYPES
OPERATORS
CONTROL FLOW
METHODS AND PARAMETERS
CLASSES
STRUCTS
RECORDS
ENUMS
INTERFACES
INHERITANCE AND POLYMORPHISM
GENERICS
DELEGATES AND EVENTS
EXTENSION METHODS
EXCEPTION HANDLING
IDISPOSABLE AND USING
STRINGS
ARRAY
LIST
LINKEDLIST
DICTIONARY
HASHSET
QUEUE
STACK
PRIORITY QUEUE
LINQ
BINARY SEARCH
TREE DFS
TREE BFS
BACKTRACKING
SLIDING WINDOW
TWO POINTERS
UNION FIND
TRIE
HEAP
RECURSION AND MEMOIZATION
DYNAMIC PROGRAMMING
BIT MANIPULATION
GRAPHS
ASYNC
ASPNET CORE
ENTITY FRAMEWORK CORE
JSON
FILE IO
NETWORKING
POSTGRESQL
COMMON EXCEPTIONS
DEBUGGING
DESIGN PATTERNS
BEST PRACTICES
LEETCODE
================================================================================
NAMESPACES
================================================================================
using System;
Console
Math
Random
Convert
DateTime
DateOnly
TimeOnly
TimeSpan
Environment
Exception
Enum
using System.Collections.Generic;
List<T>
Dictionary<TKey, TValue>
HashSet<T>
Queue<T>
Stack<T>
PriorityQueue<TElement, TPriority>
LinkedList<T>
SortedDictionary<TKey, TValue>
SortedSet<T>
using System.Linq;
Min()
Max()
Sum()
Average()
Count()
Where()
Select()
SelectMany()
OrderBy()
OrderByDescending()
ThenBy()
Distinct()
GroupBy()
ToList()
ToArray()
ToDictionary()
Any()
All()
First()
FirstOrDefault()
Last()
LastOrDefault()
Single()
Skip()
Take()
using System.Text;
StringBuilder
Encoding
using System.Text.Json;
JsonSerializer
JsonDocument
JsonElement
JsonSerializerOptions
using System.Text.RegularExpressions;
Regex
Match
MatchCollection
using System.IO;
File
FileInfo
Directory
DirectoryInfo
Path
StreamReader
StreamWriter
FileStream
using System.Threading;
Thread
CancellationToken
CancellationTokenSource
Monitor
Mutex
Semaphore
SemaphoreSlim
using System.Threading.Tasks;
Task
Task<T>
ValueTask
Parallel
Parallel.ForEach()
using System.Diagnostics;
Debug
Stopwatch
Process
using System.Net.Http;
HttpClient
HttpRequestMessage
HttpResponseMessage
using System.Security.Cryptography;
SHA256
SHA512
MD5
RandomNumberGenerator
using System.Collections.Concurrent;
ConcurrentDictionary<TKey, TValue>
ConcurrentQueue<T>
ConcurrentStack<T>
BlockingCollection<T>
Notes
• Modern .NET (6+) enables Implicit Usings by default.
• Most console applications do not require writing these using directives manually.
• LINQ methods (Min, Max, Sum, Where, Select, etc.) require:
using System.Linq;
================================================================================
MATH
================================================================================
Minimum
Math.Min(a, b);
Maximum
Math.Max(a, b);
Absolute Value
Math.Abs(x);
Power
Math.Pow(x, y);
Square Root
Math.Sqrt(x);
Round
Math.Round(x);
Ceiling
Math.Ceiling(x);
Floor
Math.Floor(x);
Truncate
Math.Truncate(x);
Clamp
Math.Clamp(value, min, max);
Sign
Math.Sign(x);
Remainder
x % y
DivRem
Math.DivRem(a, b);
Maximum Integer
int.MaxValue
Minimum Integer
int.MinValue
Maximum Long
long.MaxValue
Minimum Long
long.MinValue
Maximum Double
double.MaxValue
Minimum Double
double.MinValue
Positive Infinity
double.PositiveInfinity
Negative Infinity
double.NegativeInfinity
NaN
double.NaN
Check NaN
double.IsNaN(x);
Check Infinity
double.IsInfinity(x);
PI
Math.PI
Euler's Number
Math.E
Sin
Math.Sin(radians);
Cos
Math.Cos(radians);
Tan
Math.Tan(radians);
Asin
Math.Asin(x);
Acos
Math.Acos(x);
Atan
Math.Atan(x);
Atan2
Math.Atan2(y, x);
Log Base e
Math.Log(x);
Log Base 10
Math.Log10(x);
Log Base N
Math.Log(x, baseValue);
Exponential
Math.Exp(x);
BigMul
Math.BigMul(a, b);
Fused Multiply Add (.NET 8+)
Math.FusedMultiplyAdd(a, b, c);
Greatest Value in Array
nums.Max();
Smallest Value in Array
nums.Min();
Sum Array
nums.Sum();
Average
nums.Average();
================================================================================
RANDOM
================================================================================
Create
Random random = new Random();
Random Integer
random.Next();
Random Range
random.Next(min, max);
Random Double
random.NextDouble();
================================================================================
COMMON LEETCODE MATH
================================================================================
Middle Index
int mid = left + (right - left) / 2;
Avoid Integer Overflow
long result = (long)a * b;
Swap
(a, b) = (b, a);
Even
x % 2 == 0
Odd
x % 2 != 0
Max of Three
Math.Max(a, Math.Max(b, c));
Min of Three
Math.Min(a, Math.Min(b, c));
================================================================================
SKELETON CODE
================================================================================
using System;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}
================================================================================
DOTNET CLI
================================================================================
DESCRIPTION
The dotnet command line interface is the entry point for creating,
buil Single()
Skip()
Take()
ding, running, testing, and publishing .NET projects. It replaces
most of what used to require Visual Studio's GUI and works identically
on Linux, macOS, and Windows.
-------------------------------------------------------------------------------
DOTNET NEW
-------------------------------------------------------------------------------
Scaffolds a new project or file from a template.
dotnet new console -o MyApp
dotnet new classlib -o MyLib
dotnet new webapi -o MyApi
dotnet new mvc -o MyMvcApp
dotnet new xunit -o MyApp.Tests
dotnet new sln -o MySolution
dotnet new gitignore
dotnet new editorconfig
List every installed template:
dotnet new list
Search for a template on NuGet.org:
dotnet new search blazor
-------------------------------------------------------------------------------
DOTNET RUN
-------------------------------------------------------------------------------
Builds (if needed) and runs the project in the current directory.
dotnet run
Pass arguments to your program after a double dash:
dotnet run -- --port 5000 --verbose
Run a specific project when several exist:
dotnet run --project ./src/MyApp/MyApp.csproj
Run in a specific configuration:
dotnet run -c Release
-------------------------------------------------------------------------------
DOTNET BUILD
-------------------------------------------------------------------------------
Compiles the project and its dependencies without running it.
dotnet build
dotnet build -c Release
dotnet build -c Release -o ./out
dotnet build --no-restore
dotnet build --arch x64
-------------------------------------------------------------------------------
DOTNET PUBLISH
-------------------------------------------------------------------------------
Produces a deployable set of files (framework-dependent or
self-contained).
dotnet publish -c Release -o ./publish
Self-contained single-file publish for Linux x64:
dotnet publish -c Release -r linux-x64 --self-contained true \
-p:PublishSingleFile=true -o ./publish
Trim unused assemblies (AOT-friendly):
dotnet publish -c Release -r linux-x64 --self-contained true \
-p:PublishTrimmed=true
-------------------------------------------------------------------------------
DOTNET RESTORE
-------------------------------------------------------------------------------
Downloads and installs NuGet dependencies listed in the project file.
This runs implicitly before build/run/publish/test, but can be called
directly.
dotnet restore
dotnet restore --force
dotnet restore --source https://api.nuget.org/v3/index.json
-------------------------------------------------------------------------------
DOTNET CLEAN
-------------------------------------------------------------------------------
Removes build output (bin/obj) produced by previous builds.
dotnet clean
dotnet clean -c Release
-------------------------------------------------------------------------------
DOTNET WATCH
-------------------------------------------------------------------------------
Watches source files and automatically rebuilds/reruns/retests on
change. Extremely useful during API development.
dotnet watch run
dotnet watch test
dotnet watch --project ./src/MyApi/MyApi.csproj run
-------------------------------------------------------------------------------
DOTNET TEST
-------------------------------------------------------------------------------
Runs unit tests using the test runner configured in the project
(xUnit, NUnit, MSTest).
dotnet test
dotnet test --filter "FullyQualifiedName~MyNamespace.MyTests"
dotnet test --logger "console;verbosity=detailed"
dotnet test --collect:"XPlat Code Coverage"
-------------------------------------------------------------------------------
DOTNET TOOL
-------------------------------------------------------------------------------
Manages .NET global and local command-line tools.
dotnet tool install -g dotnet-ef
dotnet tool install -g dotnet-outdated-tool
dotnet tool list -g
dotnet tool update -g dotnet-ef
dotnet tool uninstall -g dotnet-ef
Local (project-scoped) tools use a tool manifest:
dotnet new tool-manifest
dotnet tool install dotnet-ef
dotnet tool restore
-------------------------------------------------------------------------------
DOTNET WORKLOAD
-------------------------------------------------------------------------------
Manages optional workloads such as mobile (MAUI), WebAssembly, and
other SDK extensions.
dotnet workload list
dotnet workload install maui
dotnet workload update
dotnet workload search
-------------------------------------------------------------------------------
DOTNET SLN
-------------------------------------------------------------------------------
Manages a .sln solution file's list of projects.
dotnet new sln -n MySolution
dotnet sln add src/MyApi/MyApi.csproj
dotnet sln add src/**/*.csproj
dotnet sln remove src/MyApi/MyApi.csproj
dotnet sln list
-------------------------------------------------------------------------------
DOTNET ADD
-------------------------------------------------------------------------------
Adds a package reference or project reference.
dotnet add package Newtonsoft.Json
dotnet add package Npgsql --version 8.0.3
dotnet add reference ../MyLib/MyLib.csproj
-------------------------------------------------------------------------------
DOTNET REMOVE
-------------------------------------------------------------------------------
Removes a package reference or project reference.
dotnet remove package Newtonsoft.Json
dotnet remove reference ../MyLib/MyLib.csproj
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Running dotnet run from the solution root when multiple projects
exist causes an ambiguous "Specify which project" error. Either cd
into the project directory or pass --project explicitly.
Forgetting dotnet restore after editing a .csproj by hand -- most IDEs
do this automatically, but the raw CLI does not always pick up changes
without a restore.
Confusing dotnet build output with dotnet publish output. Build output
is not meant for deployment; publish output is.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Know the difference between framework-dependent deployment (FDD) and
self-contained deployment (SCD). FDD requires the .NET runtime to be
installed on the target machine; SCD bundles the runtime with the app.
Know that dotnet is a single entry point binary; the actual SDK tools
are resolved based on the global.json file or the latest installed SDK.
================================================================================
SOLUTIONS AND PROJECTS
================================================================================
DESCRIPTION
A .sln file groups multiple .csproj projects together (a web API, a
class library, a test project, etc). Solutions are optional for a
single project but standard for anything real-world.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
mkdir MyProduct && cd MyProduct
dotnet new sln -n MyProduct
dotnet new webapi -o src/MyProduct.Api
dotnet new classlib -o src/MyProduct.Core
dotnet new classlib -o src/MyProduct.Infrastructure
dotnet new xunit -o tests/MyProduct.Tests
-------------------------------------------------------------------------------
ADDING PROJECTS
-------------------------------------------------------------------------------
dotnet sln add src/MyProduct.Api/MyProduct.Api.csproj
dotnet sln add src/MyProduct.Core/MyProduct.Core.csproj
dotnet sln add src/MyProduct.Infrastructure/MyProduct.Infrastructure.csproj
dotnet sln add tests/MyProduct.Tests/MyProduct.Tests.csproj
Add every csproj under a folder at once (shell glob, bash/zsh):
dotnet sln add (find . -name "*.csproj")
-------------------------------------------------------------------------------
REMOVING PROJECTS
-------------------------------------------------------------------------------
dotnet sln remove src/MyProduct.Infrastructure/MyProduct.Infrastructure.csproj
Removing from the solution does not delete files on disk. It only
removes the reference from the .sln.
-------------------------------------------------------------------------------
LISTING PROJECTS
-------------------------------------------------------------------------------
dotnet sln list
-------------------------------------------------------------------------------
REFERENCE PROJECTS
-------------------------------------------------------------------------------
Project references let one project use types from another. Run this
from inside the project that needs the dependency.
cd src/MyProduct.Api
dotnet add reference ../MyProduct.Core/MyProduct.Core.csproj
dotnet add reference ../MyProduct.Infrastructure/MyProduct.Infrastructure.csproj
View existing references:
dotnet list reference
Typical layered dependency direction:
Api -> Core, Infrastructure
Infrastructure -> Core
Core -> (nothing, pure domain logic)
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Creating a circular reference (Core referencing Infrastructure while
Infrastructure references Core) will fail to build. Keep dependencies
flowing one direction, typically toward Core.
Forgetting to add a newly created project to the .sln means IDEs like
Rider or Visual Studio will not show it, even though dotnet build from
that project's own folder still works.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to explain Clean Architecture / Onion Architecture layering
(Domain / Core at the center, Infrastructure and Api on the outside
depending inward). This project layout question comes up often in
mid-to-senior .NET interviews.
================================================================================
NUGET
================================================================================
DESCRIPTION
NuGet is the package manager for .NET, comparable to npm for Node or
pip for Python. Packages are distributed as .nupkg files and consumed
via package references in the .csproj.
-------------------------------------------------------------------------------
INSTALLING
-------------------------------------------------------------------------------
dotnet add package Serilog
dotnet add package Npgsql --version 8.0.3
dotnet add package Microsoft.EntityFrameworkCore.Design
Installing into a specific project from the solution root:
dotnet add src/MyProduct.Api/MyProduct.Api.csproj package Serilog
-------------------------------------------------------------------------------
REMOVING
-------------------------------------------------------------------------------
dotnet remove package Serilog
-------------------------------------------------------------------------------
LISTING
-------------------------------------------------------------------------------
dotnet list package
dotnet list package --outdated
dotnet list package --vulnerable
dotnet list package --deprecated
-------------------------------------------------------------------------------
RESTORING
-------------------------------------------------------------------------------
dotnet restore
dotnet restore --force --no-cache
-------------------------------------------------------------------------------
VERSION PINNING
-------------------------------------------------------------------------------
Version numbers can be pinned exactly, or use a floating range.
<PackageReference Include="Npgsql" Version="8.0.3" />
<PackageReference Include="Npgsql" Version="[8.0.3]" />
<PackageReference Include="Npgsql" Version="8.*" />
Central package version management (one place for all versions) uses
a Directory.Packages.props file at the solution root:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Npgsql" Version="8.0.3" />
</ItemGroup>
</Project>
-------------------------------------------------------------------------------
LOCAL PACKAGES
-------------------------------------------------------------------------------
Point NuGet at a local folder of .nupkg files, useful for testing an
unpublished package.
dotnet nuget add source /home/user/local-packages --name LocalFeed
dotnet add package MyLocalLib --source LocalFeed
-------------------------------------------------------------------------------
PRIVATE FEEDS
-------------------------------------------------------------------------------
Common in companies using Azure Artifacts, GitHub Packages, or a
self-hosted feed like BaGet or ProGet.
dotnet nuget add source https://nuget.pkg.github.com/OWNER/index.json \
--name github --username USERNAME --password TOKEN --store-password-in-clear-text
Feeds are stored in NuGet.Config, either per-solution or at
~/.nuget/NuGet/NuGet.Config for the whole machine.
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Mixing package versions across projects in the same solution without
central package management, which leads to subtle binding redirect and
"version conflict" warnings at build time.
Committing NuGet.Config with a plaintext password from
--store-password-in-clear-text into source control. Use environment
variables or a credential provider instead in real projects.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Know that PackageReference (modern, in the .csproj) replaced
packages.config (legacy, XML file listing every package). Almost all
new .NET projects use PackageReference.
================================================================================
VARIABLES AND TYPES
================================================================================
DESCRIPTION
C# is statically typed. Every variable has a type known at compile
time, either declared explicitly or inferred with var. The type
system splits into value types (structs, stored inline / on the
stack when local) and reference types (classes, stored on the heap
with a reference on the stack).
-------------------------------------------------------------------------------
DECLARING VARIABLES
-------------------------------------------------------------------------------
int age = 30;
string name = "Jake";
var total = 42; // type inferred as int
const double Pi = 3.14159; // compile-time constant
readonly int id; // set once, in constructor only
-------------------------------------------------------------------------------
PRIMITIVE TYPES
-------------------------------------------------------------------------------
bool true / false, 1 byte
byte 0 to 255, unsigned 8-bit
sbyte -128 to 127, signed 8-bit
short -32,768 to 32,767, signed 16-bit
ushort 0 to 65,535, unsigned 16-bit
int -2,147,483,648 to 2,147,483,647, signed 32-bit
uint 0 to 4,294,967,295, unsigned 32-bit
long -9.2 quintillion to 9.2 quintillion, signed 64-bit
ulong 0 to 18.4 quintillion, unsigned 64-bit
float 32-bit IEEE floating point, ~6-9 digit precision
double 64-bit IEEE floating point, ~15-17 digit precision
decimal 128-bit, base-10, used for money, ~28-29 digit precision
char a single UTF-16 code unit
string an immutable sequence of chars
-------------------------------------------------------------------------------
NULLABLE TYPES
-------------------------------------------------------------------------------
Value types are non-nullable by default. Add ? to allow null.
int? maybeAge = null;
double? maybeScore = null;
if (maybeAge.HasValue)
{
Console.WriteLine(maybeAge.Value);
}
int age = maybeAge ?? 0; // null-coalescing default
int age2 = maybeAge ?? throw new ArgumentNullException();
Nullable reference types (enabled via <Nullable>enable</Nullable> in
the .csproj) make the compiler warn when a reference type that is not
marked with ? is assigned or passed a possible null.
string? middleName = null; // explicitly nullable
string firstName = "Jake"; // compiler assumes never null
-------------------------------------------------------------------------------
REFERENCE TYPES VS VALUE TYPES
-------------------------------------------------------------------------------
Value types (struct, int, bool, enum, DateTime, all primitives):
- copied by value on assignment or when passed to a method
- live on the stack when they are local variables, or inline
inside whatever object contains them
- default value is all-zero-bits, never null (unless Nullable<T>)
Reference types (class, string, array, delegate, interface):
- assignment copies the reference, not the object
- live on the heap, garbage collected
- default value is null
struct PointStruct { public int X, Y; }
class PointClass { public int X, Y; }
PointStruct a = new PointStruct { X = 1, Y = 2 };
PointStruct b = a; // b is a full independent copy
b.X = 99; // a.X is still 1
PointClass c = new PointClass { X = 1, Y = 2 };
PointClass d = c; // d points at the same object as c
d.X = 99; // c.X is now also 99
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
Convert.ToInt32("42")
int.Parse("42")
int.TryParse("42", out int result)
double.Parse("3.14", CultureInfo.InvariantCulture)
value.ToString()
value.GetType()
typeof(int)
default(int) // 0
default(string) // null
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
int? id = null;
int safeId = id ?? -1;
object boxed = 42; // boxing: value type -> heap
int unboxed = (int)boxed; // unboxing: heap -> value type back
int x = 5;
int y = 10;
(x, y) = (y, x); // tuple swap, no temp variable needed
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Comparing a nullable value type directly against a literal without
checking HasValue, which silently works for == but can surprise
people coming from other languages.
Believing structs are always faster than classes. Large structs
copied frequently (function parameters, list elements) can be slower
than a class reference due to copy overhead. Keep structs small,
typically 16 bytes or less as a rule of thumb.
Forgetting that decimal, not double or float, is the correct type
for money and other exact base-10 arithmetic.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be able to explain boxing and unboxing and why it is a performance
concern (heap allocation, GC pressure) when value types are stored
in a non-generic collection like ArrayList or passed as object.
Be able to state why float/double are unsuitable for currency: binary
floating point cannot represent most base-10 fractions exactly.
================================================================================
OPERATORS
================================================================================
DESCRIPTION
C# supports the usual arithmetic, comparison, logical, and bitwise
operators, plus several C#-specific null-handling and pattern
operators that come up constantly in modern code.
-------------------------------------------------------------------------------
ARITHMETIC
-------------------------------------------------------------------------------
+ - * / % ++ --
int a = 7 / 2; // 3, integer division truncates
double b = 7.0 / 2; // 3.5
int c = 7 % 2; // 1, remainder
-------------------------------------------------------------------------------
COMPARISON AND LOGICAL
-------------------------------------------------------------------------------
== != < > <= >=
&& || !
Short-circuit evaluation applies to && and ||: the right side is not
evaluated if the left side already determines the result.
-------------------------------------------------------------------------------
BITWISE
-------------------------------------------------------------------------------
& AND
| OR
^ XOR
~ NOT (bitwise complement)
<< left shift
>> right shift (arithmetic for signed types)
>>> unsigned right shift (C# 11+)
-------------------------------------------------------------------------------
NULL HANDLING OPERATORS
-------------------------------------------------------------------------------
string? name = GetName();
string safe = name ?? "unknown"; // null-coalescing
name ??= "default"; // null-coalescing assignment
int? len = name?.Length; // null-conditional
name?.Trim().ToUpper(); // chained null-conditional
var first = list?[0]; // null-conditional indexer
-------------------------------------------------------------------------------
OTHER OPERATORS
-------------------------------------------------------------------------------
obj is SomeType // type check
obj as SomeType // safe cast, null on failure
(SomeType)obj // hard cast, throws on failure
typeof(SomeType)
nameof(someVariable)
condition ? valueIfTrue : valueIfFalse // ternary
x switch { 1 => "one", 2 => "two", _ => "other" } // switch expression
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
int mask = 0b_1010_1010;
bool isEven = (n & 1) == 0;
int doubled = n << 1;
int halved = n >> 1;
var description = age switch
{
< 13 => "child",
< 20 => "teenager",
< 65 => "adult",
_ => "senior"
};
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Using = instead of == inside a condition. C# will not compile this for
bool conditions the way C does, but it still trips people up when
porting logic mentally from other languages.
Assuming (as) throws on failure. It returns null instead; a hard cast
with parentheses is what throws an InvalidCastException.
Forgetting operator precedence around bitwise operators mixed with
comparisons, e.g. writing "if (x & 1 == 0)" instead of
"if ((x & 1) == 0)" -- == binds tighter than &, changing the result.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Bit tricks come up often: n & (n - 1) clears the lowest set bit,
n & -n isolates the lowest set bit, and (n & 1) == 0 checks evenness
without using the modulo operator.
================================================================================
CONTROL FLOW
================================================================================
DESCRIPTION
Covers if/else, switch statements and expressions, pattern matching,
and every loop construct in C#.
-------------------------------------------------------------------------------
IF / ELSE
-------------------------------------------------------------------------------
if (score >= 90)
{
grade = "A";
}
else if (score >= 80)
{
grade = "B";
}
else
{
grade = "F";
}
-------------------------------------------------------------------------------
SWITCH STATEMENT
-------------------------------------------------------------------------------
switch (dayOfWeek)
{
case DayOfWeek.Saturday:
case DayOfWeek.Sunday:
isWeekend = true;
break;
default:
isWeekend = false;
break;
}
-------------------------------------------------------------------------------
SWITCH EXPRESSION (C# 8+)
-------------------------------------------------------------------------------
string category = age switch
{
< 13 => "child",
>= 13 and < 20 => "teen",
>= 20 and < 65 => "adult",
_ => "senior"
};
-------------------------------------------------------------------------------
PATTERN MATCHING
-------------------------------------------------------------------------------
Type patterns:
if (shape is Circle c)
{
Console.WriteLine(c.Radius);
}
Property patterns:
if (person is { Age: >= 18, Country: "US" })
{
Console.WriteLine("Eligible");
}
Positional patterns (with records):
record Point(int X, int Y);
static string Classify(Point p) => p switch
{
(0, 0) => "origin",
(var x, 0) => $"on x-axis at {x}",
(0, var y) => $"on y-axis at {y}",
_ => "somewhere else"
};
List patterns (C# 11+):
int[] numbers = { 1, 2, 3 };
if (numbers is [1, 2, 3])
{
Console.WriteLine("exact match");
}
if (numbers is [var first, .., var last])
{
Console.WriteLine($"{first} .. {last}");
}
-------------------------------------------------------------------------------
LOOPS
-------------------------------------------------------------------------------
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
}
foreach (var item in collection)
{
Console.WriteLine(item);
}
int i = 0;
while (i < 10)
{
Console.WriteLine(i);
i++;
}
int j = 0;
do
{
Console.WriteLine(j);
j++;
} while (j < 10);
Loop control keywords:
break exits the nearest enclosing loop or switch
continue skips to the next iteration
goto jumps to a labeled statement, rarely used, mostly for
breaking out of nested loops when a boolean flag is
awkward
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Modifying a collection while iterating it with foreach throws
InvalidOperationException ("Collection was modified"). Iterate over
a copy (ToList()) or use a for loop with an index if mutation during
iteration is required.
Forgetting break in a switch statement (not switch expression) causes
a compile error in C#, unlike C/C++/Java where it silently falls
through. C# only allows fallthrough between empty case labels.
Off-by-one errors in for loops, especially when converting from
0-indexed to 1-indexed thinking mid-algorithm.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
List patterns and property patterns show up increasingly often in
modern C# codebases and are worth being fluent in for interviews at
companies on recent .NET versions.
================================================================================
METHODS AND PARAMETERS
================================================================================
DESCRIPTION
Methods are the basic unit of behavior in C#. This section covers
method declarations plus every parameter modifier: optional, named,
ref, out, in, and params.
-------------------------------------------------------------------------------
BASIC METHOD DECLARATION
-------------------------------------------------------------------------------
public int Add(int a, int b)
{
return a + b;
}
public int Add(int a, int b) => a + b; // expression-bodied member
-------------------------------------------------------------------------------
OPTIONAL PARAMETERS
-------------------------------------------------------------------------------
public void Greet(string name, string greeting = "Hello")
{
Console.WriteLine($"{greeting}, {name}!");
}
Greet("Jake"); // "Hello, Jake!"
Greet("Jake", "Hey"); // "Hey, Jake!"
Optional parameters must come after all required parameters.
-------------------------------------------------------------------------------
NAMED PARAMETERS
-------------------------------------------------------------------------------
void CreateUser(string name, int age, bool isAdmin = false) { }
CreateUser(name: "Jake", age: 30, isAdmin: true);
CreateUser(age: 30, name: "Jake"); // order does not matter when named
-------------------------------------------------------------------------------
REF, OUT, IN, PARAMS
-------------------------------------------------------------------------------
ref -- pass by reference, must be initialized before the call, the
method can read and modify it, the change is visible to the caller.
void Double(ref int x) { x *= 2; }
int n = 5;
Double(ref n); // n is now 10
out -- pass by reference specifically for returning a value, does not
need to be initialized before the call, the method must assign it.
bool TryParseAge(string input, out int age)
{
return int.TryParse(input, out age);
}
if (TryParseAge("30", out int result))
{
Console.WriteLine(result);
}
in -- pass by reference but read-only inside the method, used to
avoid copying large structs without allowing mutation.
void Print(in Vector3 v) { Console.WriteLine(v.X); }
params -- allows a variable number of arguments packed into an array.
int Sum(params int[] numbers)
{
int total = 0;
foreach (var n in numbers) total += n;
return total;
}
Sum(1, 2, 3, 4); // any number of arguments
Sum(); // zero is fine too
-------------------------------------------------------------------------------
LOCAL FUNCTIONS
-------------------------------------------------------------------------------
A function defined inside another function, useful for recursion
helpers and avoiding polluting the class with private helpers.
public int Factorial(int n)
{
return Helper(n);
int Helper(int x) => x <= 1 ? 1 : x * Helper(x - 1);
}
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Forgetting the ref/out keyword at the call site, not just the
declaration. Both places must repeat the keyword.
Overusing ref/out where a simple return value or tuple return would
read more clearly. Modern C# often prefers returning a tuple over
multiple out parameters.
(bool success, int value) TryDivide(int a, int b) =>
b == 0 ? (false, 0) : (true, a / b);
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Know the difference between ref and out clearly -- it is a frequent
interview question. ref requires definite assignment before the call;
out requires definite assignment before the method returns.
================================================================================
CLASSES
================================================================================
DESCRIPTION
Classes are reference types and the primary way to model objects with
state and behavior in C#.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
public class Employee
{
public string Name { get; set; }
public decimal Salary { get; private set; }
public Employee(string name, decimal salary)
{
Name = name;
Salary = salary;
}
public void GiveRaise(decimal amount)
{
Salary += amount;
}
}
var e = new Employee("Jake", 90000m);
-------------------------------------------------------------------------------
PROPERTIES
-------------------------------------------------------------------------------
public class Point
{
public int X { get; set; } // auto property
public int Y { get; init; } // init-only, C# 9+
public int Sum => X + Y; // computed property
private int _z;
public int Z // full property
{
get => _z;
set => _z = value < 0 ? 0 : value;
}
}
-------------------------------------------------------------------------------
CONSTRUCTORS
-------------------------------------------------------------------------------
public class Person
{
public string Name { get; }
public int Age { get; }
public Person(string name) : this(name, 0) { }
public Person(string name, int age)
{
Name = name;
Age = age;
}
static Person()
{
// static constructor, runs once, before first use of the type
}
}
Primary constructors (C# 12+):
public class Person(string name, int age)
{
public string Name => name;
public int Age => age;
}
-------------------------------------------------------------------------------
STATIC MEMBERS
-------------------------------------------------------------------------------
public class Counter
{
public static int InstanceCount { get; private set; }
public Counter()
{
InstanceCount++;
}
}
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
obj.ToString()
obj.Equals(other)
obj.GetHashCode()
obj.GetType()
object.ReferenceEquals(a, b)
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Overriding Equals without also overriding GetHashCode, which breaks
dictionary and hash set behavior silently.
Making every property auto-implemented with a public setter, even
when the object should be immutable after construction. Prefer init
or private set for anything not meant to change after creation.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to explain the difference between a field and a property,
and why properties (even trivial auto-properties) are preferred for
public API surface -- they allow adding logic later without a
breaking binary change.
================================================================================
STRUCTS
================================================================================
DESCRIPTION
Structs are value types, typically used for small, immutable, data-
only aggregates where copy semantics and avoiding heap allocation
matter (points, colors, money amounts, small coordinate pairs).
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
readonly struct ReadOnlyPoint // C# 7.2+, whole struct is immutable
{
public int X { get; }
public int Y { get; }
public ReadOnlyPoint(int x, int y) => (X, Y) = (x, y);
}
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
Structs implicitly inherit from System.ValueType, and get default
Equals/GetHashCode based on field-by-field comparison unless you
override them (recommended for performance-sensitive structs).
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
Point a = new Point(1, 2);
Point b = a; // full copy
// mutating b never affects a
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Copying a struct is O(size of struct), which is why large structs
(more than roughly 16-24 bytes as a common guideline) can become
slower to pass around than a class reference (which is always a
fixed 8 bytes on 64-bit).
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Making a large mutable struct with public setters and passing it
around by value, leading to confusing bugs where a "modification"
silently vanishes because it happened on a copy.
Using a struct purely out of a belief that "structs are always
faster." That is only true for small, short-lived, rarely-copied
data.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Know when to reach for a struct: small, immutable, value-semantics
data that is created and destroyed often (e.g. inside a tight loop),
where avoiding GC pressure from heap allocation matters.
================================================================================
RECORDS
================================================================================
DESCRIPTION
Records (C# 9+) are reference types (or record structs, value types)
designed for immutable data with built-in value-based equality,
ToString, and non-destructive mutation via "with" expressions.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
public record Point(int X, int Y);
public record Person
{
public string Name { get; init; }
public int Age { get; init; }
}
public record struct Coordinates(double Lat, double Lng); // value type
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
Records get, for free:
- value-based Equals and GetHashCode (compares property values,
not references)
- a readable ToString(), e.g. "Point { X = 1, Y = 2 }"
- a Deconstruct method for positional records
- the "with" expression for non-destructive copies
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
var p1 = new Point(1, 2);
var p2 = new Point(1, 2);
Console.WriteLine(p1 == p2); // True, value equality
var p3 = p1 with { Y = 99 }; // copy with Y changed
Console.WriteLine(p3); // Point { X = 1, Y = 99 }
var (x, y) = p1; // deconstruction
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Expecting record equality on a record with mutable collection
properties (like List<T>) to compare contents deeply. Value equality
on records compares each property using its own Equals, and
List<T>.Equals is reference equality, so two records holding
"equal-looking" lists will not compare equal.
Using a plain record (reference type) when a record struct would
avoid heap allocation for small, frequently created data.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Records are the idiomatic modern choice for DTOs and request/response
models in ASP.NET Core APIs because of immutability and free value
equality, which makes tests and caching keys much simpler to reason
about than classes with mutable properties.
================================================================================
ENUMS
================================================================================
DESCRIPTION
Enums define a named set of integral constants. They compile down to
an underlying integral type (int by default).
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
public enum OrderStatus
{
Pending, // 0
Shipped, // 1
Delivered, // 2
Cancelled // 3
}
public enum HttpStatusGroup : byte
{
Informational = 1,
Success = 2,
Redirection = 3,
ClientError = 4,
ServerError = 5
}
[Flags]
public enum Permissions
{
None = 0,
Read = 1 << 0,
Write = 1 << 1,
Execute = 1 << 2,
All = Read | Write | Execute
}
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
Enum.GetValues<OrderStatus>()
Enum.GetNames<OrderStatus>()
Enum.Parse<OrderStatus>("Shipped")
Enum.TryParse<OrderStatus>("Shipped", out var status)
status.ToString()
status.HasFlag(Permissions.Write)
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
var status = OrderStatus.Shipped;
Console.WriteLine((int)status); // 1
Console.WriteLine(status.ToString()); // "Shipped"
var perms = Permissions.Read | Permissions.Write;
Console.WriteLine(perms.HasFlag(Permissions.Write)); // True
Console.WriteLine(perms); // "Read, Write"
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Storing enum values as strings in a database and then reordering or
renaming enum members, which silently breaks stored data. Prefer
storing the underlying int (with a comment mapping values) or a
string that matches Enum.Parse exactly, and never reorder existing
members.
Forgetting [Flags] on a bitmask-style enum, which makes ToString()
print the raw number instead of a readable combination.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be able to explain why inserting a new enum value in the middle of
an existing enum (rather than appending at the end) is dangerous for
anything persisted by its integer value.
================================================================================
INTERFACES
================================================================================
DESCRIPTION
An interface defines a contract of members that an implementing type
must provide. C# supports multiple interface implementation (unlike
multiple class inheritance, which is not allowed).
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
public interface IShape
{
double Area();
double Perimeter();
}
public interface ILogger
{
void Log(string message);
void LogError(string message) => Log($"ERROR: {message}"); // default
// interface
// method,
// C# 8+
}
public class Circle : IShape
{
public double Radius { get; init; }
public double Area() => Math.PI * Radius * Radius;
public double Perimeter() => 2 * Math.PI * Radius;
}
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
IShape shape = new Circle { Radius = 2 };
Console.WriteLine(shape.Area());
Explicit interface implementation (used when a class implements two
interfaces with a colliding member name):
public interface IEnglishGreeter { string Greet(); }
public interface ISpanishGreeter { string Greet(); }
public class Bilingual : IEnglishGreeter, ISpanishGreeter
{
string IEnglishGreeter.Greet() => "Hello";
string ISpanishGreeter.Greet() => "Hola";
}
Bilingual b = new();
string en = ((IEnglishGreeter)b).Greet();
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Adding a non-default member to a widely implemented interface, which
breaks every implementing class until they add the new member. Use
default interface methods to add new members without breaking
existing implementers.
Overusing interfaces for types that only ever have one implementation
"just in case," adding indirection without real benefit. Prefer
introducing an interface when a second implementation, or a test
double, actually shows up.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to discuss interface segregation (many small, focused
interfaces rather than one large one) and how interfaces enable
dependency inversion and mocking in unit tests.
================================================================================
INHERITANCE AND POLYMORPHISM
================================================================================
DESCRIPTION
C# supports single class inheritance. Polymorphism is achieved
through virtual/override members and interfaces.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
public abstract class Animal
{
public string Name { get; }
protected Animal(string name) => Name = name;
public abstract string MakeSound();
public virtual string Describe() => $"{Name} says {MakeSound()}";
}
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override string MakeSound() => "Woof";
}
public class Puppy : Dog
{
public Puppy(string name) : base(name) { }
public override string Describe() => base.Describe() + " (softly)";
}
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
base.SomeMethod() // call the parent implementation
sealed override Method() // prevent further overriding
new Method() // hide, not override, a base member
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
Animal a = new Dog("Rex");
Console.WriteLine(a.Describe()); // "Rex says Woof"
// MakeSound is resolved at runtime based on the actual object type,
// not the declared variable type -- this is dynamic dispatch.
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Using "new" to hide a base member instead of "override," which
silently breaks polymorphism -- calling the method through a base
class reference invokes the base implementation, not the derived
one, which surprises almost everyone the first time they hit it.
Deep inheritance chains (more than 2-3 levels) that become hard to
reason about. Favor composition over inheritance once a hierarchy
starts feeling forced.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to explain "new" vs "override" with a concrete example,
since it is one of the most commonly asked C#-specific OOP questions.
Be ready to discuss "favor composition over inheritance" and give
a real example of when you chose one over the other.
================================================================================
GENERICS
================================================================================
DESCRIPTION
Generics let a type or method be parameterized over a type, avoiding
both code duplication and the boxing/casting overhead of using
object for a general-purpose container.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
public class Box<T>
{
public T Value { get; set; }
}
public class Repository<TEntity, TKey> where TEntity : class
{
public TEntity? GetById(TKey id) => default;
}
public T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
-------------------------------------------------------------------------------
GENERIC CONSTRAINTS
-------------------------------------------------------------------------------
where T : class // reference type
where T : struct // value type
where T : new() // has a public parameterless constructor
where T : SomeBaseClass // must derive from SomeBaseClass
where T : ISomeInterface // must implement ISomeInterface
where T : notnull // cannot be a nullable type
where T : U // T must derive from another type param U
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
typeof(List<>)
typeof(T)
default(T)
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
var intBox = new Box<int> { Value = 42 };
var stringBox = new Box<string> { Value = "hi" };
int bigger = Max(3, 7);
string laterAlphabetically = Max("apple", "banana");
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Using object instead of a generic parameter for a "general purpose"
container, forcing casts and boxing at every call site.
Forgetting a constraint and then trying to call a method (like
CompareTo, or a constructor) that only exists on some possible types
T could be. The compiler will refuse, and the fix is almost always
adding the appropriate where clause.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Know the difference between covariance (out T, e.g.
IEnumerable<out T>) and contravariance (in T, e.g. IComparer<in T>)
for generic interfaces, and why arrays are covariant but not
type-safe at runtime as a result.
================================================================================
DELEGATES AND EVENTS
================================================================================
DESCRIPTION
A delegate is a type-safe function pointer. Events are a
publish/subscribe wrapper around delegates that restricts who can
invoke the handlers.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
public delegate int Operation(int a, int b);
Operation add = (a, b) => a + b;
Operation multiply = (a, b) => a * b;
Built-in generic delegates, used far more often than custom ones:
Func<int, int, int> add2 = (a, b) => a + b; // has a return value
Action<string> log = msg => Console.WriteLine(msg); // no return value
Predicate<int> isEven = n => n % 2 == 0; // returns bool
Events:
public class Button
{
public event EventHandler? Clicked;
public void SimulateClick()
{
Clicked?.Invoke(this, EventArgs.Empty);
}
}
var button = new Button();
button.Clicked += (sender, args) => Console.WriteLine("Clicked!");
button.SimulateClick();
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
someDelegate.Invoke(args)
someDelegate?.Invoke(args) // null-safe invoke
someDelegate += anotherHandler // multicast, add
someDelegate -= aHandler // multicast, remove
someDelegate.GetInvocationList()
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
Multicast delegate calling multiple handlers in order added:
Action greet = () => Console.WriteLine("Hi");
greet += () => Console.WriteLine("Hello");
greet(); // prints both lines
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Invoking an event without a null check when there are no subscribers,
which throws NullReferenceException. Always use ?.Invoke() or check
for null first.
Forgetting to unsubscribe long-lived event handlers, which is a
common source of memory leaks: the publisher holds a reference to
the subscriber through the delegate, keeping it alive.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to explain why events use += / -= rather than direct
assignment (=), and how that protects external code from wiping out
other subscribers' handlers.
================================================================================
EXTENSION METHODS
================================================================================
DESCRIPTION
Extension methods add new methods to an existing type, including
types you do not own (like string or List<T>), without subclassing
or modifying the original type.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
public static class StringExtensions
{
public static bool IsNullOrBlank(this string? value) =>
string.IsNullOrWhiteSpace(value);
public static string Truncate(this string value, int maxLength) =>
value.Length <= maxLength ? value : value[..maxLength] + "...";
}
Must be declared in a static class, and the first parameter must be
marked with "this".
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
string? name = null;
bool blank = name.IsNullOrBlank(); // works even though name is null
string bio = "A very long bio that goes on and on";
string preview = bio.Truncate(10); // "A very lon..."
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Overusing extension methods for logic that really belongs as a
regular method on a type you own, scattering related behavior across
unrelated static classes and making it harder to discover via
IntelliSense/autocomplete on the actual type.
Forgetting to import the namespace containing the static class,
which makes the extension method invisible at the call site with no
obvious error message pointing at "add a using statement."
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Nearly all of LINQ (Where, Select, OrderBy, etc.) is implemented as
extension methods on IEnumerable<T> in System.Linq -- a good example
to cite when asked "what are extension methods used for in practice."
================================================================================
EXCEPTION HANDLING
================================================================================
DESCRIPTION
C# uses structured exception handling with try/catch/finally. All
exceptions derive from System.Exception.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
try
{
int result = 10 / divisor;
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Cannot divide by zero: {ex.Message}");
}
catch (Exception ex) when (ex.Message.Contains("critical"))
{
// filtered catch, only runs when the condition is true
}
finally
{
Console.WriteLine("Always runs, even if an exception was thrown");
}
Custom exceptions:
public class InsufficientFundsException : Exception
{
public decimal Shortfall { get; }
public InsufficientFundsException(decimal shortfall)
: base($"Short by {shortfall:C}")
{
Shortfall = shortfall;
}
}
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
throw new SomeException("message");
throw; // re-throw, preserves stack trace
throw ex; // re-throw, resets stack trace (avoid)
ex.Message
ex.StackTrace
ex.InnerException
ex.Data
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
try
{
DoSomethingRisky();
}
catch (IOException ex)
{
LogError(ex);
throw; // preserves original stack trace for the caller
}
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Using "throw ex;" instead of "throw;" inside a catch block, which
resets the stack trace and makes production debugging much harder.
Catching Exception broadly and swallowing it silently (empty catch
block), hiding real bugs. Catch the specific exception type you can
actually handle, and let the rest propagate.
Using exceptions for ordinary control flow (e.g. parsing user input
with int.Parse wrapped in try/catch instead of int.TryParse), which
is slower and reads worse than the non-throwing alternative.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to explain "throw;" vs "throw ex;" precisely -- it is one of
the most common C#-specific interview questions on exception
handling.
Know that exception filters (catch (Exception ex) when (condition))
let you inspect an exception without unwinding the stack if the
condition is false, unlike catching and rethrowing manually.
================================================================================
IDISPOSABLE AND USING
================================================================================
DESCRIPTION
IDisposable is the standard pattern for deterministic cleanup of
unmanaged resources (file handles, database connections, network
sockets) that the garbage collector cannot reliably clean up on its
own timeline.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
public class FileLogger : IDisposable
{
private readonly StreamWriter _writer;
private bool _disposed;
public FileLogger(string path)
{
_writer = new StreamWriter(path, append: true);
}
public void Log(string message) => _writer.WriteLine(message);
public void Dispose()
{
if (_disposed) return;
_writer.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
}
}
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
using var logger = new FileLogger("log.txt"); // C# 8+ using declaration
using (var logger2 = new FileLogger("log2.txt")) { } // classic using block
await using var conn = new SomeAsyncDisposable(); // for IAsyncDisposable
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
// Dispose() called automatically here, even if an exception
// is thrown inside the block
}
using var logger = new FileLogger("app.log");
logger.Log("Started");
// Dispose() called automatically at the end of the enclosing scope
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Forgetting to wrap a disposable resource (DbConnection, StreamReader,
HttpClient response body, etc.) in a using statement, leaking
unmanaged handles until the finalizer eventually runs, if ever.
Creating a new HttpClient per request instead of reusing one long-
lived instance (or IHttpClientFactory) -- HttpClient is intended to
be reused, and disposing/recreating it repeatedly can exhaust
sockets under load (socket exhaustion).
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to explain the Dispose pattern including the disposed flag
check and GC.SuppressFinalize, and why deterministic disposal
matters even though .NET has a garbage collector.
================================================================================
STRINGS
================================================================================
DESCRIPTION
Strings in C# are immutable sequences of UTF-16 chars. Every "mutating"
string operation actually returns a new string. For heavy concatenation
in a loop, use StringBuilder instead.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
string a = "hello";
string b = new string('x', 5); // "xxxxx"
string c = string.Empty;
string? d = null;
string multi = """
This is a raw string literal (C# 11+).
No escaping needed for quotes or backslashes.
""";
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
s.Length
s.ToUpper() / s.ToLower()
s.Trim() / s.TrimStart() / s.TrimEnd()
s.Substring(start, length)
s[start..end] // range indexer, C# 8+
s.Split(',')
s.Split(',', StringSplitOptions.RemoveEmptyEntries)
string.Join(", ", items)
s.Replace("old", "new")
s.Contains("sub")
s.StartsWith("prefix") / s.EndsWith("suffix")
s.IndexOf("sub") / s.LastIndexOf("sub")
string.IsNullOrEmpty(s)
string.IsNullOrWhiteSpace(s)
s.PadLeft(10) / s.PadRight(10)
s.ToCharArray()
s.Equals(other, StringComparison.OrdinalIgnoreCase)
string.Compare(a, b)
s.Reverse() // returns IEnumerable<char>, needs new string(...)
-------------------------------------------------------------------------------
STRINGBUILDER
-------------------------------------------------------------------------------
var sb = new StringBuilder();
sb.Append("Hello");
sb.Append(' ').Append("World");
sb.AppendLine("!");
sb.Insert(0, ">> ");
sb.Replace("World", "C#");
sb.Remove(0, 3);
string result = sb.ToString();
sb.Clear();
-------------------------------------------------------------------------------
FORMATTING AND INTERPOLATION
-------------------------------------------------------------------------------
string name = "Jake";
int age = 30;
string a = $"{name} is {age} years old";
string b = $"{name,10}"; // right-align in 10 chars
string c = $"{price:C}"; // currency format
string d = $"{value:N2}"; // number, 2 decimals
string e = $"{date:yyyy-MM-dd}";
string f = $"{ratio:P1}"; // percentage, 1 decimal
string g = string.Format("{0} is {1}", name, age);
Interpolated raw string with embedded expressions (C# 11+):
string h = $"""
Name: {name}
Age: {age}
""";
-------------------------------------------------------------------------------
REGEX
-------------------------------------------------------------------------------
using System.Text.RegularExpressions;
bool isMatch = Regex.IsMatch(input, @"^\d{3}-\d{4}$");
Match m = Regex.Match(input, @"(\d+)-(\d+)");
if (m.Success)
{
string first = m.Groups[1].Value;
}
MatchCollection all = Regex.Matches(input, @"\d+");
string replaced = Regex.Replace(input, @"\s+", " ");
string[] parts = Regex.Split(input, @",\s*");
Source-generated regex (fast, compile-time, C# 13/.NET 9+):
public partial class PhoneMatcher
{
[GeneratedRegex(@"^\d{3}-\d{4}$")]
public static partial Regex PhonePattern();
}
-------------------------------------------------------------------------------
PARSING
-------------------------------------------------------------------------------
int n = int.Parse("42");
bool ok = int.TryParse("42", out int result);
double d = double.Parse("3.14", CultureInfo.InvariantCulture);
DateTime dt = DateTime.Parse("2024-01-15");
DateTime.TryParseExact("15/01/2024", "dd/MM/yyyy",
CultureInfo.InvariantCulture, DateTimeStyles.None, out var exact);
-------------------------------------------------------------------------------
COMMON LEETCODE PATTERNS
-------------------------------------------------------------------------------
Reverse a string:
char[] chars = s.ToCharArray();
Array.Reverse(chars);
string reversed = new string(chars);
Check palindrome, ignoring case and non-alphanumeric:
static bool IsPalindrome(string s)
{
int left = 0, right = s.Length - 1;
while (left < right)
{
if (!char.IsLetterOrDigit(s[left])) { left++; continue; }
if (!char.IsLetterOrDigit(s[right])) { right--; continue; }
if (char.ToLower(s[left]) != char.ToLower(s[right])) return false;
left++; right--;
}
return true;
}
Character frequency count:
var freq = new Dictionary<char, int>();
foreach (char c in s)
{
freq[c] = freq.GetValueOrDefault(c) + 1;
}
Anagram check via sorted chars:
static bool IsAnagram(string a, string b)
{
if (a.Length != b.Length) return false;
var ca = a.ToCharArray(); Array.Sort(ca);
var cb = b.ToCharArray(); Array.Sort(cb);
return new string(ca) == new string(cb);
}
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Length O(1)
Indexing s[i] O(1)
Concatenation s + t O(n + m), new string allocated
Substring O(k), k = length of the substring
Contains / IndexOf O(n) worst case
StringBuilder.Append amortized O(1) per append
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Concatenating strings with + inside a loop, which allocates a new
string on every iteration -- O(n^2) total for n appends. Use
StringBuilder instead.
Using == for culture-sensitive comparisons without specifying
StringComparison, which can behave unexpectedly across locales
(e.g. Turkish "I" casing issues). Prefer
StringComparison.Ordinal or OrdinalIgnoreCase for exact/technical
comparisons, and CurrentCulture only when comparing user-facing text.
Forgetting that strings are immutable, so s.Trim() alone does nothing
unless the result is assigned back: s = s.Trim();
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Know why string concatenation in a loop is a classic performance
anti-pattern and be ready to explain StringBuilder's internal
resizable buffer as the fix.
Sliding window and two-pointer string problems (longest substring
without repeating characters, valid palindrome, anagram grouping)
are extremely common interview questions -- see the STRINGS,
SLIDING WINDOW, and TWO POINTERS sections together.
================================================================================
ARRAY
================================================================================
DESCRIPTION
A fixed-size, zero-indexed collection of elements of the same type,
allocated as one contiguous block of memory. The size cannot change
after creation.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
int[] nums = new int[5];
int[] nums2 = { 1, 2, 3, 4, 5 };
int[] nums3 = new int[] { 1, 2, 3 };
string[] names = new string[3];
int[,] grid = new int[3, 3]; // 2D rectangular array
int[,] grid2 = { { 1, 2 }, { 3, 4 } };
int[][] jagged = new int[3][]; // jagged array
jagged[0] = new int[] { 1, 2 };
jagged[1] = new int[] { 3, 4, 5 };
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
nums.Length
grid.GetLength(0) / grid.GetLength(1)
Array.Sort(nums)
Array.Reverse(nums)
Array.IndexOf(nums, 3)
Array.Copy(source, destination, length)
Array.Clear(nums)
Array.Fill(nums, 0)
Array.Resize(ref nums, 10) // creates a new array
Array.BinarySearch(nums, 3) // array must be sorted
nums.Clone()
nums[1..3] // range indexer, new array
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
int[] nums = { 5, 3, 8, 1 };
Array.Sort(nums); // { 1, 3, 5, 8 }
for (int row = 0; row < grid.GetLength(0); row++)
{
for (int col = 0; col < grid.GetLength(1); col++)
{
Console.Write(grid[row, col] + " ");
}
}
-------------------------------------------------------------------------------
COMMON LEETCODE PATTERNS
-------------------------------------------------------------------------------
In-place reverse:
static void Reverse(int[] arr)
{
int left = 0, right = arr.Length - 1;
while (left < right)
{
(arr[left], arr[right]) = (arr[right], arr[left]);
left++; right--;
}
}
Rotate array right by k:
static void Rotate(int[] nums, int k)
{
k %= nums.Length;
Array.Reverse(nums);
Array.Reverse(nums, 0, k);
Array.Reverse(nums, k, nums.Length - k);
}
Prefix sum:
int[] prefix = new int[nums.Length + 1];
for (int i = 0; i < nums.Length; i++)
prefix[i + 1] = prefix[i] + nums[i];
// sum of nums[i..j] inclusive == prefix[j + 1] - prefix[i]
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Access by index O(1)
Search (unsorted) O(n)
Search (sorted, binary) O(log n)
Insert / delete O(n) -- requires shifting or a new array
Array.Sort O(n log n), introspective sort
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Trying to "add" or "remove" an element from a fixed array directly.
Arrays cannot resize; use Array.Resize (which reallocates and copies)
or switch to List<T> if the size is not fixed in advance.
Confusing a jagged array (int[][], each row can be a different
length, rows are separate array objects) with a rectangular
multidimensional array (int[,], one contiguous block, all rows the
same length). They have different syntax and different performance
characteristics -- jagged arrays are usually faster for row access.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Arrays are the backbone of almost every classic algorithm problem.
Be fluent with in-place manipulation, since many interview questions
explicitly ask for O(1) extra space.
================================================================================
LIST
================================================================================
DESCRIPTION
List<T> is a dynamically resizable array, the default general-purpose
collection in C# for ordered data whose size changes over time.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
var list = new List<int>();
var list2 = new List<int> { 1, 2, 3 };
var list3 = new List<int>(capacity: 100);
var list4 = Enumerable.Range(1, 10).ToList();
var list5 = existingArray.ToList();
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
list.Add(4)
list.AddRange(otherList)
list.Insert(0, 99)
list.Remove(3) // removes first matching value
list.RemoveAt(0)
list.RemoveAll(x => x < 0)
list.Contains(3)
list.IndexOf(3)
list.Sort()
list.Sort((a, b) => b.CompareTo(a)) // custom comparer, descending
list.Reverse()
list.Count
list.Clear()
list[0] // indexer
list.Find(x => x > 5)
list.FindAll(x => x > 5)
list.Exists(x => x > 5)
list.ForEach(x => Console.WriteLine(x))
list.ToArray()
list.GetRange(0, 3)
list.BinarySearch(3) // list must be sorted
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
var names = new List<string> { "Bob", "Alice", "Carl" };
names.Sort(); // alphabetical in place
names.Add("Dave");
names.RemoveAt(0);
var evens = names.Where(n => n.Length == 3).ToList();
-------------------------------------------------------------------------------
COMMON LEETCODE PATTERNS
-------------------------------------------------------------------------------
Building a result list while iterating, very common in problems
asking to "return all valid combinations":
var results = new List<List<int>>();
var current = new List<int>();
// ... backtracking logic pushes/pops from current ...
results.Add(new List<int>(current)); // copy, not a reference!
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Access by index O(1)
Add at end amortized O(1)
Insert / remove at front O(n)
Insert / remove at index O(n)
Contains / IndexOf O(n)
Sort O(n log n)
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Adding "current" directly to "results" during backtracking instead of
a copy (new List<int>(current)). Since current is mutated afterward,
every reference in results silently points at the same, later-changed
list.
Using RemoveAt or Remove inside a foreach loop over the same list,
which throws InvalidOperationException. Iterate backwards with a for
loop, or use RemoveAll, when removing conditionally.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
List<T> is backed by an array that doubles in capacity when it fills
up -- know this when asked to explain "amortized O(1)" append.
================================================================================
LINKEDLIST
================================================================================
DESCRIPTION
LinkedList<T> is a doubly-linked list. Rarely the right default
choice compared to List<T>, but useful when frequent insertion or
removal happens in the middle of the sequence and you already hold
a node reference.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
var list = new LinkedList<int>();
var list2 = new LinkedList<int>(new[] { 1, 2, 3 });
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
list.AddFirst(0)
list.AddLast(4)
list.AddBefore(node, 99)
list.AddAfter(node, 100)
list.Remove(3)
list.RemoveFirst()
list.RemoveLast()
list.Find(3) // returns LinkedListNode<T>
list.First / list.Last // LinkedListNode<T>
node.Value
node.Next / node.Previous
list.Count
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
var ll = new LinkedList<int>();
ll.AddLast(1);
ll.AddLast(2);
var node = ll.AddLast(3);
ll.AddAfter(node, 4); // 1 -> 2 -> 3 -> 4
for (var n = ll.First; n != null; n = n.Next)
{
Console.Write(n.Value + " ");
}
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Add/remove at a known node O(1)
Add/remove at front/back O(1)
Find by value O(n)
Access by index not supported directly, O(n) to walk to it
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Reaching for LinkedList<T> by habit from other languages when
List<T> would perform better in practice for almost all real-world
workloads, since List<T> has better cache locality even though its
Big-O for middle insertion is worse.
Note: classic "singly-linked list" LeetCode problems (reverse a
linked list, detect a cycle, merge two sorted lists) are usually
defined with a custom ListNode class in interviews, not
System.Collections.Generic.LinkedList<T>. Know both.
public class ListNode
{
public int val;
public ListNode? next;
public ListNode(int val = 0, ListNode? next = null)
{
this.val = val;
this.next = next;
}
}
-------------------------------------------------------------------------------
COMMON LEETCODE PATTERNS
-------------------------------------------------------------------------------
Reverse a singly linked list:
static ListNode? Reverse(ListNode? head)
{
ListNode? prev = null;
while (head != null)
{
var next = head.next;
head.next = prev;
prev = head;
head = next;
}
return prev;
}
Detect a cycle (Floyd's tortoise and hare):
static bool HasCycle(ListNode? head)
{
var slow = head;
var fast = head;
while (fast?.next != null)
{
slow = slow!.next;
fast = fast.next.next;
if (slow == fast) return true;
}
return false;
}
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Know Floyd's cycle detection cold -- it appears constantly, both as
its own question and as a building block for "find the start of the
cycle" and "find the middle of a list" problems.
================================================================================
DICTIONARY
================================================================================
DESCRIPTION
Dictionary<TKey, TValue> is a hash table mapping unique keys to
values, the default C# collection for O(1) average lookups.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
var dict = new Dictionary<string, int>();
var dict2 = new Dictionary<string, int>
{
["Alice"] = 30,
["Bob"] = 25
};
var dict3 = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
dict["Alice"] = 31; // add or overwrite
dict.Add("Carl", 40); // throws if key exists
dict.TryAdd("Carl", 40); // false if key exists, no throw
dict.TryGetValue("Alice", out int age);
dict.ContainsKey("Alice");
dict.ContainsValue(30);
dict.Remove("Bob");
dict.Remove("Bob", out int removedValue);
dict.Keys
dict.Values
dict.Count
dict.GetValueOrDefault("Dave", 0);
dict.Clear();
Iteration:
foreach (var kvp in dict)
{
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
foreach (var (key, value) in dict) // deconstruction, C# 7+
{
Console.WriteLine($"{key}: {value}");
}
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
var wordCount = new Dictionary<string, int>();
foreach (var word in words)
{
wordCount[word] = wordCount.GetValueOrDefault(word) + 1;
}
-------------------------------------------------------------------------------
COMMON LEETCODE PATTERNS
-------------------------------------------------------------------------------
Two Sum:
static int[] TwoSum(int[] nums, int target)
{
var seen = new Dictionary<int, int>(); // value -> index
for (int i = 0; i < nums.Length; i++)
{
int complement = target - nums[i];
if (seen.TryGetValue(complement, out int j))
{
return new[] { j, i };
}
seen[nums[i]] = i;
}
return Array.Empty<int>();
}
Group anagrams by sorted-char key:
var groups = new Dictionary<string, List<string>>();
foreach (var word in words)
{
var chars = word.ToCharArray();
Array.Sort(chars);
var key = new string(chars);
if (!groups.TryGetValue(key, out var list))
{
list = new List<string>();
groups[key] = list;
}
list.Add(word);
}
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Add / lookup / remove average O(1), worst case O(n) with heavy
hash collisions
Iteration O(n)
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Using dict[key] to read a possibly-missing key, which throws
KeyNotFoundException. Use TryGetValue or GetValueOrDefault instead.
Using a mutable object as a dictionary key without overriding
Equals/GetHashCode consistently, which breaks lookups after the
object's state changes.
Modifying a dictionary's keys while iterating it, which throws
InvalidOperationException just like with List<T>.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Dictionary-based counting/lookup is probably the single most common
tool in interview problem solving -- know GetValueOrDefault and
TryGetValue cold, they come up in nearly every "frequency count" or
"seen before" style problem.
================================================================================
HASHSET
================================================================================
DESCRIPTION
HashSet<T> stores unique elements with average O(1) add, remove, and
contains checks. Also supports classic set algebra (union,
intersection, difference).
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
var set = new HashSet<int>();
var set2 = new HashSet<int> { 1, 2, 3 };
var set3 = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var set4 = existingList.ToHashSet();
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
set.Add(4) // returns bool, false if already present
set.Remove(2)
set.Contains(3)
set.Count
set.UnionWith(otherSet)
set.IntersectWith(otherSet)
set.ExceptWith(otherSet)
set.SymmetricExceptWith(otherSet)
set.IsSubsetOf(otherSet)
set.IsSupersetOf(otherSet)
set.Overlaps(otherSet)
set.SetEquals(otherSet)
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
var seen = new HashSet<int>();
foreach (var n in nums)
{
if (!seen.Add(n))
{
Console.WriteLine($"Duplicate found: {n}");
}
}
-------------------------------------------------------------------------------
COMMON LEETCODE PATTERNS
-------------------------------------------------------------------------------
Contains Duplicate:
static bool ContainsDuplicate(int[] nums) =>
nums.Length != nums.Distinct().Count();
Longest Consecutive Sequence:
static int LongestConsecutive(int[] nums)
{
var set = new HashSet<int>(nums);
int longest = 0;
foreach (var n in set)
{
if (!set.Contains(n - 1)) // start of a sequence
{
int length = 1;
while (set.Contains(n + length)) length++;
longest = Math.Max(longest, length);
}
}
return longest;
}
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Add / remove / contains average O(1)
UnionWith / IntersectWith O(n + m)
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Reaching for List<T>.Contains() in a loop to check for duplicates or
membership, which is O(n) per check (O(n^2) total). A HashSet<T>
turns the same check into O(1) average.
Forgetting that HashSet<T> does not preserve insertion order (that
guarantee belongs to LinkedHashSet-style structures in other
languages, not .NET's HashSet<T>).
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
"Have you seen this before" style problems (duplicates, visited
nodes in graph traversal, deduping) are the classic use case -- pair
this section mentally with DICTIONARY and GRAPHS.
================================================================================
QUEUE
================================================================================
DESCRIPTION
Queue<T> is a first-in-first-out (FIFO) collection, the standard
tool for breadth-first search and any producer/consumer-style
ordering.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
var queue = new Queue<int>();
var queue2 = new Queue<int>(new[] { 1, 2, 3 });
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
queue.Enqueue(4)
queue.Dequeue() // removes and returns the front
queue.Peek() // returns the front without removing
queue.TryDequeue(out int value)
queue.TryPeek(out int value)
queue.Count
queue.Contains(3)
queue.Clear()
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
var q = new Queue<int>();
q.Enqueue(1);
q.Enqueue(2);
q.Enqueue(3);
Console.WriteLine(q.Dequeue()); // 1
Console.WriteLine(q.Peek()); // 2
-------------------------------------------------------------------------------
COMMON LEETCODE PATTERNS
-------------------------------------------------------------------------------
Breadth-first search on a graph:
static void Bfs(Dictionary<int, List<int>> graph, int start)
{
var visited = new HashSet<int> { start };
var queue = new Queue<int>();
queue.Enqueue(start);
while (queue.Count > 0)
{
int node = queue.Dequeue();
Console.WriteLine(node);
foreach (var neighbor in graph[node])
{
if (visited.Add(neighbor))
{
queue.Enqueue(neighbor);
}
}
}
}
Level-order traversal of a binary tree, level by level:
var queue = new Queue<TreeNode>();
queue.Enqueue(root);
while (queue.Count > 0)
{
int levelSize = queue.Count;
for (int i = 0; i < levelSize; i++)
{
var node = queue.Dequeue();
Console.Write(node.val + " ");
if (node.left != null) queue.Enqueue(node.left);
if (node.right != null) queue.Enqueue(node.right);
}
}
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Enqueue / Dequeue / Peek O(1)
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Calling Dequeue() on an empty queue, which throws
InvalidOperationException. Check queue.Count > 0 first, or use
TryDequeue.
Confusing Queue<T> (FIFO) with Stack<T> (LIFO) under pressure --
remember "queue" like a line at a store, first person in line is
served first.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Queue<T> is essentially mandatory for BFS. If a problem asks for
"shortest path" or "minimum steps" in an unweighted graph or grid,
BFS with a Queue<T> is almost always the answer.
================================================================================
STACK
================================================================================
DESCRIPTION
Stack<T> is a last-in-first-out (LIFO) collection, used for
backtracking, expression parsing, undo history, and depth-first
search implemented iteratively.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
var stack = new Stack<int>();
var stack2 = new Stack<int>(new[] { 1, 2, 3 });
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
stack.Push(4)
stack.Pop() // removes and returns the top
stack.Peek() // returns the top without removing
stack.TryPop(out int value)
stack.TryPeek(out int value)
stack.Count
stack.Contains(3)
stack.Clear()
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
var s = new Stack<int>();
s.Push(1);
s.Push(2);
s.Push(3);
Console.WriteLine(s.Pop()); // 3
Console.WriteLine(s.Peek()); // 2
-------------------------------------------------------------------------------
COMMON LEETCODE PATTERNS
-------------------------------------------------------------------------------
Valid Parentheses:
static bool IsValid(string s)
{
var stack = new Stack<char>();
var pairs = new Dictionary<char, char> { [')'] = '(', [']'] = '[', ['}'] = '{' };
foreach (char c in s)
{
if (c == '(' || c == '[' || c == '{')
{
stack.Push(c);
}
else if (pairs.ContainsKey(c))
{
if (stack.Count == 0 || stack.Pop() != pairs[c]) return false;
}
}
return stack.Count == 0;
}
Monotonic stack, Daily Temperatures (next warmer day):
static int[] DailyTemperatures(int[] temps)
{
var result = new int[temps.Length];
var stack = new Stack<int>(); // stores indices
for (int i = 0; i < temps.Length; i++)
{
while (stack.Count > 0 && temps[i] > temps[stack.Peek()])
{
int idx = stack.Pop();
result[idx] = i - idx;
}
stack.Push(i);
}
return result;
}
Iterative DFS on a graph:
var stack = new Stack<int>();
stack.Push(start);
var visited = new HashSet<int>();
while (stack.Count > 0)
{
int node = stack.Pop();
if (!visited.Add(node)) continue;
Console.WriteLine(node);
foreach (var neighbor in graph[node])
{
if (!visited.Contains(neighbor)) stack.Push(neighbor);
}
}
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Push / Pop / Peek O(1)
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Calling Pop() or Peek() on an empty stack, which throws
InvalidOperationException. Check stack.Count > 0 first, or use
TryPop/TryPeek.
Forgetting that .NET's Stack<T> enumerates top-to-bottom (LIFO
order), which surprises people expecting insertion order when they
foreach directly over it.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
"Monotonic stack" is a named pattern worth memorizing by name: keep
the stack's contents increasing or decreasing, popping whenever the
invariant would break, to solve "next greater/smaller element" style
problems in O(n) instead of O(n^2).
================================================================================
PRIORITY QUEUE
================================================================================
DESCRIPTION
PriorityQueue<TElement, TPriority> (added in .NET 6) is a binary-heap
based collection that always dequeues the element with the lowest
priority value first (a min-heap by default).
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
var pq = new PriorityQueue<string, int>();
var pq2 = new PriorityQueue<string, int>(
Comparer<int>.Create((a, b) => b.CompareTo(a))); // max-heap version
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
pq.Enqueue(item, priority)
pq.Dequeue() // removes and returns lowest-priority item
pq.Peek() // views lowest-priority item without removing
pq.TryDequeue(out var item, out var priority)
pq.TryPeek(out var item, out var priority)
pq.Count
pq.EnqueueRange(items)
pq.Clear()
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
var pq = new PriorityQueue<string, int>();
pq.Enqueue("low priority task", 5);
pq.Enqueue("urgent task", 1);
pq.Enqueue("medium task", 3);
while (pq.Count > 0)
{
Console.WriteLine(pq.Dequeue());
}
// urgent task, medium task, low priority task
-------------------------------------------------------------------------------
COMMON LEETCODE PATTERNS
-------------------------------------------------------------------------------
Kth largest element using a min-heap of size k:
static int FindKthLargest(int[] nums, int k)
{
var pq = new PriorityQueue<int, int>();
foreach (var n in nums)
{
pq.Enqueue(n, n);
if (pq.Count > k) pq.Dequeue();
}
return pq.Peek();
}
Dijkstra's shortest path:
static int[] Dijkstra(Dictionary<int, List<(int to, int weight)>> graph,
int start, int nodeCount)
{
var dist = new int[nodeCount];
Array.Fill(dist, int.MaxValue);
dist[start] = 0;
var pq = new PriorityQueue<int, int>();
pq.Enqueue(start, 0);
while (pq.Count > 0)
{
int node = pq.Dequeue();
foreach (var (to, weight) in graph[node])
{
int newDist = dist[node] + weight;
if (newDist < dist[to])
{
dist[to] = newDist;
pq.Enqueue(to, newDist);
}
}
}
return dist;
}
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Enqueue O(log n)
Dequeue O(log n)
Peek O(1)
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Forgetting PriorityQueue<TElement, TPriority> is a min-heap by
default. For a max-heap, either negate the priority values or supply
a reversed IComparer<T> at construction.
Expecting stable ordering among equal-priority elements. .NET's
PriorityQueue does not guarantee FIFO order for ties.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Know the "keep a heap of size k" trick for top-k / kth-largest
problems -- it turns an O(n log n) full sort into O(n log k).
Dijkstra's algorithm is the classic priority-queue interview
question for weighted shortest-path problems; contrast it with plain
BFS, which only works for unweighted graphs.
================================================================================
LINQ
================================================================================
DESCRIPTION
Language Integrated Query. A set of extension methods over
IEnumerable<T> (and IQueryable<T> for databases) that lets you
filter, project, sort, and aggregate data declaratively, either with
method syntax or query syntax.
-------------------------------------------------------------------------------
METHOD SYNTAX VS QUERY SYNTAX
-------------------------------------------------------------------------------
var adults = people.Where(p => p.Age >= 18).OrderBy(p => p.Name);
var adults2 =
from p in people
where p.Age >= 18
orderby p.Name
select p;
Both compile to the same thing. Method syntax is far more common in
real codebases; query syntax reads better for complex joins.
-------------------------------------------------------------------------------
FILTERING AND PROJECTION
-------------------------------------------------------------------------------
people.Where(p => p.Age >= 18)
people.Select(p => p.Name)
people.Select((p, index) => $"{index}: {p.Name}")
people.SelectMany(p => p.PhoneNumbers) // flattens nested collections
people.OfType<Manager>() // filters by runtime type
-------------------------------------------------------------------------------
ORDERING
-------------------------------------------------------------------------------
people.OrderBy(p => p.LastName)
people.OrderByDescending(p => p.Age)
people.OrderBy(p => p.LastName).ThenBy(p => p.FirstName)
people.Reverse()
-------------------------------------------------------------------------------
GROUPING AND AGGREGATION
-------------------------------------------------------------------------------
people.GroupBy(p => p.Department)
people.Aggregate((a, b) => a.Age > b.Age ? a : b) // fold
people.Aggregate(0, (sum, p) => sum + p.Age) // seeded fold
people.Count()
people.Count(p => p.Age >= 18)
people.Sum(p => p.Salary)
people.Average(p => p.Age)
people.Min(p => p.Age)
people.Max(p => p.Age)
var byDept = people
.GroupBy(p => p.Department)
.Select(g => new { Department = g.Key, Count = g.Count() });
-------------------------------------------------------------------------------
SET-STYLE AND SLICE OPERATIONS
-------------------------------------------------------------------------------
people.Distinct()
people.DistinctBy(p => p.Email) // .NET 6+
people.Skip(10)
people.Take(5)
people.SkipWhile(p => p.Age < 18)
people.TakeWhile(p => p.Age < 65)
people.Chunk(3) // .NET 6+, splits into arrays of 3
-------------------------------------------------------------------------------
JOIN AND ZIP
-------------------------------------------------------------------------------
var query =
from order in orders
join customer in customers on order.CustomerId equals customer.Id
select new { order.Id, customer.Name };
var joined = orders.Join(
customers,
order => order.CustomerId,
customer => customer.Id,
(order, customer) => new { order.Id, customer.Name });
var zipped = list1.Zip(list2, (a, b) => a + b);
-------------------------------------------------------------------------------
CONVERSION
-------------------------------------------------------------------------------
people.ToList()
people.ToArray()
people.ToDictionary(p => p.Id)
people.ToDictionary(p => p.Id, p => p.Name)
people.ToLookup(p => p.Department) // like ToDictionary but allows
// duplicate keys, one-to-many
people.ToHashSet()
-------------------------------------------------------------------------------
ELEMENT AND BOOLEAN OPERATIONS
-------------------------------------------------------------------------------
people.Any()
people.Any(p => p.Age >= 18)
people.All(p => p.Age >= 18)
people.Contains(somePerson)
people.First() // throws if empty
people.FirstOrDefault() // null/default if empty
people.FirstOrDefault(p => p.Age >= 18)
people.Last()
people.LastOrDefault()
people.Single() // throws unless exactly one element
people.SingleOrDefault()
people.ElementAt(2)
people.ElementAtOrDefault(2)
-------------------------------------------------------------------------------
DEFERRED EXECUTION
-------------------------------------------------------------------------------
LINQ queries built with Where/Select/etc. are lazy -- they do not
run until enumerated (via foreach, ToList(), Count(), etc). This
means the same query can produce different results if the underlying
data changes between definition and enumeration.
var query = numbers.Where(n => n > threshold); // not run yet
threshold = 100;
foreach (var n in query) { ... } // runs now, uses 100
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
var topEarners = employees
.Where(e => e.Salary > 100000)
.OrderByDescending(e => e.Salary)
.Select(e => new { e.Name, e.Salary })
.Take(5)
.ToList();
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Where / Select / SelectMany O(n), lazy, one pass on enumeration
OrderBy / OrderByDescending O(n log n)
GroupBy O(n) average
Distinct O(n) average, uses a hash set internally
Count() on IEnumerable O(n) unless the source has a fast Count
(arrays and List<T> use O(1) directly)
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Calling .Count() > 0 instead of .Any() to check for emptiness. Any()
stops at the first element; Count() may enumerate the entire
sequence depending on the source type.
Enumerating the same deferred query multiple times when the source
is expensive (a database query, a web call), re-running the whole
operation each time. Materialize once with .ToList() if you need to
reuse the result.
Using First() when the sequence might be empty, causing an
unhandled InvalidOperationException in production. Prefer
FirstOrDefault() and check for null/default explicitly.
Chaining so many LINQ operators that a simple loop would be clearer
and faster to both read and execute -- LINQ readability is a
trade-off, not a free win, especially inside hot paths.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to explain deferred (lazy) execution vs immediate execution,
and name which LINQ methods force immediate evaluation (ToList,
ToArray, ToDictionary, Count, Sum, First, etc. all force it).
Be ready to translate a LINQ chain into the equivalent explicit
for/foreach loop and vice versa -- interviewers sometimes ask this to
confirm you understand what's happening under the hood, not just the
syntax.
================================================================================
BINARY SEARCH
================================================================================
DESCRIPTION
A divide-and-conquer search over a sorted range that eliminates half
the remaining candidates on each step.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
Classic binary search:
static int BinarySearch(int[] nums, int target)
{
int left = 0, right = nums.Length - 1;
while (left <= right)
{
int mid = left + (right - left) / 2; // avoids overflow
if (nums[mid] == target) return mid;
if (nums[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
Lower bound / leftmost insertion point (first index where nums[i] >= target):
static int LowerBound(int[] nums, int target)
{
int left = 0, right = nums.Length;
while (left < right)
{
int mid = left + (right - left) / 2;
if (nums[mid] < target) left = mid + 1;
else right = mid;
}
return left;
}
Binary search on the answer (common pattern for optimization
problems, e.g. "minimum capacity to ship packages within D days"):
static int MinimumCapacity(int lo, int hi, Func<int, bool> canFinish)
{
while (lo < hi)
{
int mid = lo + (hi - lo) / 2;
if (canFinish(mid)) hi = mid;
else lo = mid + 1;
}
return lo;
}
Built-in:
Array.BinarySearch(sortedArray, target);
list.BinarySearch(target);
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Binary search O(log n)
Requires the input to already be sorted; sorting first costs
O(n log n) if it is not.
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Writing mid = (left + right) / 2, which can overflow for very large
indices. Use left + (right - left) / 2 instead.
Getting the loop boundary condition wrong (<= vs <) and either
missing the last element or looping forever. Decide up front whether
"right" is inclusive or exclusive and stay consistent.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
"Binary search on the answer" is a distinct pattern from "binary
search on an array" -- recognizing when a problem's answer space is
monotonic (if X works, does X+1 also work?) is the key insight for
a whole category of optimization problems.
================================================================================
TREE DFS
================================================================================
DESCRIPTION
Depth-first traversal of a tree, going as deep as possible down one
branch before backtracking. Three common orders: preorder, inorder,
postorder.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
public class TreeNode
{
public int val;
public TreeNode? left;
public TreeNode? right;
public TreeNode(int val = 0, TreeNode? left = null, TreeNode? right = null)
{
this.val = val; this.left = left; this.right = right;
}
}
Recursive preorder (root, left, right):
static void Preorder(TreeNode? node, List<int> result)
{
if (node == null) return;
result.Add(node.val);
Preorder(node.left, result);
Preorder(node.right, result);
}
Recursive inorder (left, root, right) -- yields sorted order for a BST:
static void Inorder(TreeNode? node, List<int> result)
{
if (node == null) return;
Inorder(node.left, result);
result.Add(node.val);
Inorder(node.right, result);
}
Recursive postorder (left, right, root):
static void Postorder(TreeNode? node, List<int> result)
{
if (node == null) return;
Postorder(node.left, result);
Postorder(node.right, result);
result.Add(node.val);
}
Iterative preorder using an explicit stack:
static List<int> PreorderIterative(TreeNode? root)
{
var result = new List<int>();
if (root == null) return result;
var stack = new Stack<TreeNode>();
stack.Push(root);
while (stack.Count > 0)
{
var node = stack.Pop();
result.Add(node.val);
if (node.right != null) stack.Push(node.right);
if (node.left != null) stack.Push(node.left);
}
return result;
}
Max depth:
static int MaxDepth(TreeNode? node) =>
node == null ? 0 : 1 + Math.Max(MaxDepth(node.left), MaxDepth(node.right));
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Any full traversal O(n), visits every node once
Space (recursive) O(h), h = height of the tree, due to call stack;
O(n) worst case for a completely unbalanced tree
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Forgetting the null base case, causing a NullReferenceException the
moment recursion hits a leaf's child.
Confusing preorder/inorder/postorder under interview pressure --
memorize them by remembering where "root" falls in the name relative
to left and right.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Inorder traversal of a valid BST always yields values in sorted
order -- a frequently used fact for "validate BST" and "kth smallest
element in a BST" problems.
================================================================================
TREE BFS
================================================================================
DESCRIPTION
Breadth-first (level-order) traversal of a tree, visiting all nodes
at depth d before any node at depth d+1. Implemented with a Queue<T>.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
Level order traversal, grouped by level:
static List<List<int>> LevelOrder(TreeNode? root)
{
var result = new List<List<int>>();
if (root == null) return result;
var queue = new Queue<TreeNode>();
queue.Enqueue(root);
while (queue.Count > 0)
{
int levelSize = queue.Count;
var level = new List<int>();
for (int i = 0; i < levelSize; i++)
{
var node = queue.Dequeue();
level.Add(node.val);
if (node.left != null) queue.Enqueue(node.left);
if (node.right != null) queue.Enqueue(node.right);
}
result.Add(level);
}
return result;
}
Right side view (last node visible from the right at each level):
static List<int> RightSideView(TreeNode? root)
{
var result = new List<int>();
if (root == null) return result;
var queue = new Queue<TreeNode>();
queue.Enqueue(root);
while (queue.Count > 0)
{
int levelSize = queue.Count;
for (int i = 0; i < levelSize; i++)
{
var node = queue.Dequeue();
if (i == levelSize - 1) result.Add(node.val);
if (node.left != null) queue.Enqueue(node.left);
if (node.right != null) queue.Enqueue(node.right);
}
}
return result;
}
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Full traversal O(n)
Space O(w), w = maximum width of the tree, O(n) worst case
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Forgetting to capture queue.Count into a fixed levelSize variable
before the inner loop, which breaks level-by-level grouping because
the queue's count keeps changing as children are enqueued mid-loop.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Any problem phrased "level by level" or "shortest path in an
unweighted tree/graph" is a strong signal to reach for BFS over DFS.
================================================================================
BACKTRACKING
================================================================================
DESCRIPTION
A systematic way to explore all candidate solutions by building them
incrementally and abandoning ("backtracking" from) a partial
candidate as soon as it cannot possibly lead to a valid solution.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
Subsets (power set):
static List<List<int>> Subsets(int[] nums)
{
var result = new List<List<int>>();
var current = new List<int>();
Backtrack(0);
return result;
void Backtrack(int start)
{
result.Add(new List<int>(current));
for (int i = start; i < nums.Length; i++)
{
current.Add(nums[i]);
Backtrack(i + 1);
current.RemoveAt(current.Count - 1); // undo
}
}
}
Permutations:
static List<List<int>> Permute(int[] nums)
{
var result = new List<List<int>>();
var current = new List<int>();
var used = new bool[nums.Length];
Backtrack();
return result;
void Backtrack()
{
if (current.Count == nums.Length)
{
result.Add(new List<int>(current));
return;
}
for (int i = 0; i < nums.Length; i++)
{
if (used[i]) continue;
used[i] = true;
current.Add(nums[i]);
Backtrack();
current.RemoveAt(current.Count - 1); // undo
used[i] = false; // undo
}
}
}
Combination Sum (reuse elements allowed):
static List<List<int>> CombinationSum(int[] candidates, int target)
{
var result = new List<List<int>>();
var current = new List<int>();
Backtrack(0, target);
return result;
void Backtrack(int start, int remaining)
{
if (remaining == 0)
{
result.Add(new List<int>(current));
return;
}
if (remaining < 0) return;
for (int i = start; i < candidates.Length; i++)
{
current.Add(candidates[i]);
Backtrack(i, remaining - candidates[i]); // i, not i + 1: reuse
current.RemoveAt(current.Count - 1);
}
}
}
N-Queens style board backtracking follows the same shape: place a
piece, recurse, check a validity function, and undo the placement on
the way back up.
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Highly problem-dependent, usually exponential in the worst case
(e.g. O(2^n) for subsets, O(n!) for permutations), because
backtracking explores a search tree. Pruning (early return on an
invalid partial state) reduces the effective branching factor in
practice without changing the theoretical worst case.
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Forgetting the "undo" step after the recursive call (removing the
last added element, un-marking a used flag), which leaves stale
state polluting later branches of the search tree.
Adding "current" itself to "result" instead of a copy, so every
stored result is later mutated by continued backtracking (see the
same mistake noted in the LIST section).
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Backtracking problems almost always follow the same skeleton:
choose, explore (recurse), un-choose. Internalizing that skeleton
makes it much easier to adapt to subsets, permutations, combination
sum, N-Queens, word search, and Sudoku solvers, which all share the
same shape with different validity/termination conditions.
================================================================================
SLIDING WINDOW
================================================================================
DESCRIPTION
Maintains a contiguous subrange (the "window") over an array or
string and expands/shrinks it incrementally, avoiding the need to
recompute from scratch for every possible subrange.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
Fixed-size window, max sum of any k consecutive elements:
static int MaxSumSubarray(int[] nums, int k)
{
int windowSum = 0;
for (int i = 0; i < k; i++) windowSum += nums[i];
int maxSum = windowSum;
for (int i = k; i < nums.Length; i++)
{
windowSum += nums[i] - nums[i - k]; // slide by one
maxSum = Math.Max(maxSum, windowSum);
}
return maxSum;
}
Variable-size window, longest substring without repeating characters:
static int LengthOfLongestSubstring(string s)
{
var lastSeen = new Dictionary<char, int>();
int left = 0, longest = 0;
for (int right = 0; right < s.Length; right++)
{
char c = s[right];
if (lastSeen.TryGetValue(c, out int prevIndex) && prevIndex >= left)
{
left = prevIndex + 1; // shrink window past the duplicate
}
lastSeen[c] = right;
longest = Math.Max(longest, right - left + 1);
}
return longest;
}
Minimum window substring (shrink while still valid, then try to shrink further):
static string MinWindow(string s, string t)
{
var need = new Dictionary<char, int>();
foreach (var c in t) need[c] = need.GetValueOrDefault(c) + 1;
var window = new Dictionary<char, int>();
int have = 0, needCount = need.Count;
int left = 0, bestLen = int.MaxValue, bestStart = 0;
for (int right = 0; right < s.Length; right++)
{
char c = s[right];
window[c] = window.GetValueOrDefault(c) + 1;
if (need.ContainsKey(c) && window[c] == need[c]) have++;
while (have == needCount)
{
if (right - left + 1 < bestLen)
{
bestLen = right - left + 1;
bestStart = left;
}
char leftChar = s[left];
window[leftChar]--;
if (need.ContainsKey(leftChar) && window[leftChar] < need[leftChar]) have--;
left++;
}
}
return bestLen == int.MaxValue ? "" : s.Substring(bestStart, bestLen);
}
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Sliding window O(n), each index enters and leaves the window at
most once, even though it looks like nested loops
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Recomputing the window sum/state from scratch on every slide instead
of incrementally adding/removing the entering/leaving element,
turning an O(n) algorithm into O(n*k) or O(n^2).
Off-by-one errors on the window boundaries, especially around
whether "right" is inclusive.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
A strong signal for sliding window: the problem mentions a
"contiguous subarray/substring" and asks for a max/min/count subject
to some constraint. Recognizing fixed-size vs variable-size window
upfront saves a lot of false starts.
================================================================================
TWO POINTERS
================================================================================
DESCRIPTION
Uses two indices moving through a sequence (from both ends inward,
or both moving forward at different speeds) to avoid nested loops.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
Two Sum on a sorted array (opposite ends moving inward):
static int[] TwoSumSorted(int[] nums, int target)
{
int left = 0, right = nums.Length - 1;
while (left < right)
{
int sum = nums[left] + nums[right];
if (sum == target) return new[] { left, right };
if (sum < target) left++;
else right--;
}
return Array.Empty<int>();
}
Remove duplicates from a sorted array in place (slow/fast pointers):
static int RemoveDuplicates(int[] nums)
{
if (nums.Length == 0) return 0;
int slow = 0;
for (int fast = 1; fast < nums.Length; fast++)
{
if (nums[fast] != nums[slow])
{
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1;
}
Container With Most Water:
static int MaxArea(int[] height)
{
int left = 0, right = height.Length - 1, best = 0;
while (left < right)
{
int area = Math.Min(height[left], height[right]) * (right - left);
best = Math.Max(best, area);
if (height[left] < height[right]) left++;
else right--;
}
return best;
}
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Two pointers O(n), each pointer moves forward monotonically, so the
total number of steps across both pointers is bounded
by n
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Applying the "opposite ends" two-pointer pattern on unsorted data
when it requires sortedness to work correctly (e.g. Two Sum on an
unsorted array needs a dictionary approach instead, or sorting
first, which changes the original indices).
Off-by-one errors in the loop condition (< vs <=), especially in
"remove duplicates in place" style problems.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Two pointers and sliding window are closely related; the difference
is that two pointers often work on sorted arrays comparing values at
both ends, while sliding window tracks a contiguous range and its
running state.
================================================================================
UNION FIND
================================================================================
DESCRIPTION
Also called Disjoint Set Union (DSU). Tracks a partition of elements
into disjoint sets, supporting near-constant-time "are these
connected" queries and merges. The workhorse for connectivity and
cycle-detection problems on undirected graphs.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
public class UnionFind
{
private readonly int[] _parent;
private readonly int[] _rank;
public UnionFind(int n)
{
_parent = new int[n];
_rank = new int[n];
for (int i = 0; i < n; i++) _parent[i] = i;
}
public int Find(int x)
{
if (_parent[x] != x)
{
_parent[x] = Find(_parent[x]); // path compression
}
return _parent[x];
}
public bool Union(int a, int b)
{
int rootA = Find(a);
int rootB = Find(b);
if (rootA == rootB) return false; // already connected
if (_rank[rootA] < _rank[rootB])
{
(rootA, rootB) = (rootB, rootA);
}
_parent[rootB] = rootA;
if (_rank[rootA] == _rank[rootB]) _rank[rootA]++;
return true;
}
}
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
uf.Find(x) // finds the representative/root of x's set
uf.Union(a, b) // merges the sets containing a and b
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
Detect a cycle while building an undirected graph edge by edge:
var uf = new UnionFind(n);
foreach (var (a, b) in edges)
{
if (!uf.Union(a, b))
{
Console.WriteLine("Cycle detected!");
break;
}
}
Count connected components:
var uf = new UnionFind(n);
foreach (var (a, b) in edges) uf.Union(a, b);
int components = Enumerable.Range(0, n)
.Select(uf.Find)
.Distinct()
.Count();
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Find / Union (with path compression + union by rank)
effectively O(alpha(n)), where alpha is the inverse Ackermann
function -- so close to O(1) that it is treated as constant time
in practice for any realistic n.
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Implementing Find without path compression, which degrades to O(n)
per call in the worst case (a long chain), instead of near O(1).
Forgetting Union by rank/size, which can also lead to long chains
and slower Find calls, though path compression alone still keeps it
fast in practice.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Union-Find is the standard tool whenever a problem talks about
"groups," "connected components," "provinces," or "redundant
connection" (cycle detection) on an undirected graph -- often faster
and simpler to write correctly than a full DFS/BFS-based solution
for these specific questions.
================================================================================
TRIE
================================================================================
DESCRIPTION
A prefix tree, used for fast string prefix lookups: autocomplete,
spell checking, and word search style problems.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
public class TrieNode
{
public Dictionary<char, TrieNode> Children { get; } = new();
public bool IsEndOfWord { get; set; }
}
public class Trie
{
private readonly TrieNode _root = new();
public void Insert(string word)
{
var node = _root;
foreach (char c in word)
{
if (!node.Children.TryGetValue(c, out var next))
{
next = new TrieNode();
node.Children[c] = next;
}
node = next;
}
node.IsEndOfWord = true;
}
public bool Search(string word)
{
var node = FindNode(word);
return node is { IsEndOfWord: true };
}
public bool StartsWith(string prefix) => FindNode(prefix) != null;
private TrieNode? FindNode(string s)
{
var node = _root;
foreach (char c in s)
{
if (!node.Children.TryGetValue(c, out var next)) return null;
node = next;
}
return node;
}
}
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
trie.Insert("apple")
trie.Search("apple") // exact word match
trie.StartsWith("app") // prefix match
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Insert / Search / StartsWith O(L), L = length of the word/prefix,
independent of how many words are
already stored
Space O(total characters across all inserted
words), with shared prefixes stored once
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Using a fixed-size array of 26 children (for lowercase-only
alphabets) when the actual character set is broader (unicode,
digits, punctuation) -- a Dictionary<char, TrieNode> is more
flexible at a small performance cost.
Forgetting to mark IsEndOfWord, which makes Search return true for
any inserted prefix, not just complete words.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Recognize Trie problems by the words "prefix," "autocomplete," or
"word search on a board" -- Word Search II (searching a 2D board for
a list of words) is a classic trie + backtracking combination
problem.
================================================================================
HEAP
================================================================================
DESCRIPTION
A heap is the underlying data structure behind PriorityQueue<T> --
a complete binary tree stored in an array where each parent is
smaller (min-heap) or larger (max-heap) than its children.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
In practice, always use the built-in PriorityQueue<TElement, TPriority>
(see the PRIORITY QUEUE section) rather than hand-rolling a heap.
Hand-rolled heaps mostly show up when an interviewer explicitly asks
you to implement one from scratch.
A minimal manual min-heap over an array, for reference:
public class MinHeap
{
private readonly List<int> _data = new();
public void Push(int value)
{
_data.Add(value);
int i = _data.Count - 1;
while (i > 0)
{
int parent = (i - 1) / 2;
if (_data[parent] <= _data[i]) break;
(_data[parent], _data[i]) = (_data[i], _data[parent]);
i = parent;
}
}
public int Pop()
{
int top = _data[0];
_data[0] = _data[^1];
_data.RemoveAt(_data.Count - 1);
int i = 0;
while (true)
{
int left = 2 * i + 1, right = 2 * i + 2, smallest = i;
if (left < _data.Count && _data[left] < _data[smallest]) smallest = left;
if (right < _data.Count && _data[right] < _data[smallest]) smallest = right;
if (smallest == i) break;
(_data[i], _data[smallest]) = (_data[smallest], _data[i]);
i = smallest;
}
return top;
}
}
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Push / Pop O(log n)
Peek O(1)
Build a heap from n elements all at once O(n), not O(n log n)
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Reimplementing a heap by hand in an interview when
PriorityQueue<TElement, TPriority> is available and appropriate --
unless explicitly asked to implement one, use the built-in type.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
See the PRIORITY QUEUE section for the "top-k" and "Dijkstra" heap
patterns that come up constantly in interviews.
================================================================================
RECURSION AND MEMOIZATION
================================================================================
DESCRIPTION
Recursion solves a problem by breaking it into smaller instances of
the same problem. Memoization caches results of expensive recursive
calls so identical subproblems are computed only once.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
Naive recursive Fibonacci (exponential time, do not use as-is):
static long Fib(int n) => n <= 1 ? n : Fib(n - 1) + Fib(n - 2);
Memoized Fibonacci (top-down dynamic programming):
static long Fib(int n, Dictionary<int, long>? memo = null)
{
memo ??= new Dictionary<int, long>();
if (n <= 1) return n;
if (memo.TryGetValue(n, out long cached)) return cached;
long result = Fib(n - 1, memo) + Fib(n - 2, memo);
memo[n] = result;
return result;
}
Same idea with a simple array cache when subproblems are indexed by
a small integer range:
static long[] _cache = new long[100];
static bool[] _computed = new bool[100];
static long FibArray(int n)
{
if (n <= 1) return n;
if (_computed[n]) return _cache[n];
long result = FibArray(n - 1) + FibArray(n - 2);
_cache[n] = result;
_computed[n] = true;
return result;
}
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
Naive recursive Fibonacci O(2^n), recomputes the same subproblems
repeatedly
Memoized Fibonacci O(n), each subproblem computed once
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Forgetting a base case, causing a StackOverflowException instead of
a graceful failure -- unlike most exceptions, this one generally
cannot be caught in a try/catch and crashes the process.
Adding memoization to a problem whose subproblems are never actually
repeated, adding overhead without benefit. Memoization pays off
specifically when the same subproblem is reached via multiple
recursive paths (overlapping subproblems), which is one of the two
defining properties of dynamic programming.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to state the two properties that make memoization worth
applying: overlapping subproblems, and optimal substructure (the
optimal solution to the whole problem can be built from optimal
solutions to its subproblems).
================================================================================
DYNAMIC PROGRAMMING
================================================================================
DESCRIPTION
Solves problems by breaking them into overlapping subproblems and
storing solutions to avoid recomputation, either top-down (recursion
plus memoization) or bottom-up (iterative table filling).
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
Bottom-up Fibonacci, O(1) space:
static long FibBottomUp(int n)
{
if (n <= 1) return n;
long prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; i++)
{
long current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
Climbing Stairs (1D DP, ways to reach step n taking 1 or 2 steps at a time):
static int ClimbStairs(int n)
{
if (n <= 2) return n;
int[] dp = new int[n + 1];
dp[1] = 1; dp[2] = 2;
for (int i = 3; i <= n; i++) dp[i] = dp[i - 1] + dp[i - 2];
return dp[n];
}
0/1 Knapsack (2D DP):
static int Knapsack(int[] weights, int[] values, int capacity)
{
int n = weights.Length;
int[,] dp = new int[n + 1, capacity + 1];
for (int i = 1; i <= n; i++)
{
for (int w = 0; w <= capacity; w++)
{
dp[i, w] = dp[i - 1, w]; // don't take item i
if (weights[i - 1] <= w)
{
dp[i, w] = Math.Max(
dp[i, w],
dp[i - 1, w - weights[i - 1]] + values[i - 1]); // take it
}
}
}
return dp[n, capacity];
}
Longest Common Subsequence (2D DP over two strings):
static int Lcs(string a, string b)
{
int[,] dp = new int[a.Length + 1, b.Length + 1];
for (int i = 1; i <= a.Length; i++)
{
for (int j = 1; j <= b.Length; j++)
{
dp[i, j] = a[i - 1] == b[j - 1]
? dp[i - 1, j - 1] + 1
: Math.Max(dp[i - 1, j], dp[i, j - 1]);
}
}
return dp[a.Length, b.Length];
}
Coin Change (minimum coins to make an amount):
static int CoinChange(int[] coins, int amount)
{
int[] dp = new int[amount + 1];
Array.Fill(dp, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++)
{
foreach (var coin in coins)
{
if (coin <= i)
{
dp[i] = Math.Min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
1D DP (Fibonacci, climbing stairs, coin change) O(n) or O(n * k)
2D DP (knapsack, LCS, edit distance) O(n * m)
Space can often be reduced from O(n*m) to O(min(n, m)) by only
keeping the current and previous row when the recurrence only looks
one row back.
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Not identifying the recurrence relation clearly before coding --
write dp[i] (or dp[i, j]) as a plain-English sentence first ("the
minimum coins to make amount i"), then translate it into code.
Off-by-one errors on the dp array size (needing n+1 rather than n to
include a "zero items" or "empty string" base case).
Using recursion without memoization on a problem with overlapping
subproblems, silently degrading to exponential time even though the
logic is "correct."
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
A reliable approach for any DP interview question: define the
subproblem in words, write the recurrence, decide the base cases,
decide iteration order (which direction fills the table correctly),
then consider whether space can be compressed. Interviewers often
care more about this reasoning process than the final code.
================================================================================
BIT MANIPULATION
================================================================================
DESCRIPTION
Manipulating individual bits of an integer directly, used for
compact state representation, fast arithmetic tricks, and a
recurring category of interview questions.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
Check if a bit is set:
bool IsBitSet(int n, int i) => (n & (1 << i)) != 0;
Set / clear / toggle a bit:
int SetBit(int n, int i) => n | (1 << i);
int ClearBit(int n, int i) => n & ~(1 << i);
int ToggleBit(int n, int i) => n ^ (1 << i);
Count set bits (Brian Kernighan's algorithm):
static int CountSetBits(int n)
{
int count = 0;
while (n != 0)
{
n &= (n - 1); // clears the lowest set bit
count++;
}
return count;
}
// Or, built-in (System.Numerics.BitOperations, .NET Core 3+):
int count2 = System.Numerics.BitOperations.PopCount((uint)n);
Single Number (every element appears twice except one, found via XOR):
static int SingleNumber(int[] nums)
{
int result = 0;
foreach (var n in nums) result ^= n;
return result;
}
Check if a number is a power of two:
bool IsPowerOfTwo(int n) => n > 0 && (n & (n - 1)) == 0;
Swap two variables without a temp variable (rarely useful in
practice, but a classic interview trick):
a ^= b;
b ^= a;
a ^= b;
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
All single-bit operations O(1)
Counting set bits (Kernighan's) O(k), k = number of set bits, faster
than O(32) naive bit-by-bit scan
when the number is sparse
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Using ^ (XOR) when meaning ** (there is no exponent operator in
C#; use Math.Pow) -- a common typo carried over from other
languages/pseudocode.
Forgetting that C#'s >> on a signed integer is an arithmetic shift
(sign-extends), which can produce a negative result for negative
inputs; use >>> (unsigned right shift, C# 11+) when a logical shift
is intended.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
n & (n - 1) to drop the lowest set bit, and n & -n to isolate the
lowest set bit, are the two bit tricks worth having completely
memorized -- they show up across dozens of different-looking
problems (Single Number, Counting Bits, Power of Two, subsets via
bitmask).
================================================================================
GRAPHS
================================================================================
DESCRIPTION
Graphs model pairwise relationships between nodes. In interview
contexts they are almost always represented as an adjacency list.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
Adjacency list, unweighted:
var graph = new Dictionary<int, List<int>>();
void AddEdge(int a, int b)
{
if (!graph.ContainsKey(a)) graph[a] = new List<int>();
if (!graph.ContainsKey(b)) graph[b] = new List<int>();
graph[a].Add(b);
graph[b].Add(a); // omit this line for a directed graph
}
Adjacency list, weighted:
var graph = new Dictionary<int, List<(int to, int weight)>>();
Adjacency matrix (fine for dense graphs or small fixed n):
int[,] adjMatrix = new int[n, n];
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
See TREE DFS / TREE BFS / QUEUE / STACK sections -- the same
traversal code applies directly to general graphs, with the addition
of a visited set to avoid infinite loops in the presence of cycles.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
General graph DFS with a visited set (recursive):
static void Dfs(Dictionary<int, List<int>> graph, int node, HashSet<int> visited)
{
if (!visited.Add(node)) return;
Console.WriteLine(node);
foreach (var neighbor in graph.GetValueOrDefault(node, new List<int>()))
{
Dfs(graph, neighbor, visited);
}
}
Topological sort (Kahn's algorithm, BFS-based, for DAGs):
static List<int> TopologicalSort(int n, List<(int from, int to)> edges)
{
var graph = new Dictionary<int, List<int>>();
var inDegree = new int[n];
foreach (var (from, to) in edges)
{
if (!graph.ContainsKey(from)) graph[from] = new List<int>();
graph[from].Add(to);
inDegree[to]++;
}
var queue = new Queue<int>();
for (int i = 0; i < n; i++)
if (inDegree[i] == 0) queue.Enqueue(i);
var order = new List<int>();
while (queue.Count > 0)
{
int node = queue.Dequeue();
order.Add(node);
foreach (var next in graph.GetValueOrDefault(node, new List<int>()))
{
if (--inDegree[next] == 0) queue.Enqueue(next);
}
}
return order.Count == n ? order : new List<int>(); // empty if a cycle exists
}
Number of islands (grid-based DFS/BFS, extremely common interview question):
static int NumIslands(char[][] grid)
{
int rows = grid.Length, cols = grid[0].Length, count = 0;
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < cols; c++)
{
if (grid[r][c] == '1')
{
count++;
Sink(r, c);
}
}
}
return count;
void Sink(int r, int c)
{
if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] != '1') return;
grid[r][c] = '0';
Sink(r + 1, c); Sink(r - 1, c); Sink(r, c + 1); Sink(r, c - 1);
}
}
-------------------------------------------------------------------------------
TIME COMPLEXITY
-------------------------------------------------------------------------------
DFS / BFS (adjacency list) O(V + E), V = vertices, E = edges
DFS / BFS (adjacency matrix) O(V^2)
Topological sort (Kahn's) O(V + E)
Dijkstra's (see PRIORITY QUEUE) O((V + E) log V) with a binary heap
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Forgetting a visited set on a graph with cycles, causing infinite
recursion/looping -- trees never need this because they have no
cycles, which trips people up moving from tree problems to graph
problems.
Using recursive DFS on a graph that might be very deep/large,
risking a StackOverflowException; switch to an iterative DFS with an
explicit stack for graphs where depth is unbounded or unknown.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Grid problems (number of islands, rotting oranges, surrounded
regions) are graph problems in disguise -- each cell is a node,
adjacency is up/down/left/right, and the same DFS/BFS toolbox
applies directly.
================================================================================
ASYNC
================================================================================
DESCRIPTION
C#'s async/await lets I/O-bound (and some CPU-bound) work run
without blocking a thread while waiting, built on top of the Task
and Task<T> types.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
public async Task<string> FetchDataAsync(string url)
{
using var client = new HttpClient();
var response = await client.GetStringAsync(url);
return response;
}
public async Task DoWorkAsync()
{
await Task.Delay(1000);
Console.WriteLine("Done");
}
ValueTask<int> for high-frequency calls that often complete
synchronously, avoiding a Task allocation on the hot path:
public async ValueTask<int> GetCachedOrComputeAsync(string key)
{
if (_cache.TryGetValue(key, out int value)) return value;
return await ComputeExpensiveAsync(key);
}
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
await someTask
Task.Run(() => SomeCpuBoundWork())
Task.WhenAll(task1, task2, task3)
Task.WhenAny(task1, task2)
Task.Delay(milliseconds)
Task.FromResult(value)
Task.CompletedTask
task.Result // blocks, avoid in async code (deadlock risk)
task.Wait() // blocks, avoid in async code
task.IsCompleted / task.IsFaulted / task.IsCanceled
-------------------------------------------------------------------------------
CANCELLATION
-------------------------------------------------------------------------------
public async Task DoWorkAsync(CancellationToken token)
{
for (int i = 0; i < 1000; i++)
{
token.ThrowIfCancellationRequested();
await Task.Delay(10, token);
}
}
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
await DoWorkAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Cancelled");
}
-------------------------------------------------------------------------------
SYNCHRONIZATION PRIMITIVES
-------------------------------------------------------------------------------
lock (_syncRoot) // classic monitor-based lock,
{ // synchronous code only
_counter++;
}
private readonly SemaphoreSlim _semaphore = new(1, 1);
async Task DoAsync()
{
await _semaphore.WaitAsync();
try
{
// critical section, async-safe unlike lock
}
finally
{
_semaphore.Release();
}
}
ConcurrentDictionary<string, int> concurrentMap = new();
concurrentMap.AddOrUpdate("key", 1, (k, old) => old + 1);
concurrentMap.TryGetValue("key", out int value);
-------------------------------------------------------------------------------
PARALLEL AND CHANNELS
-------------------------------------------------------------------------------
Parallel.ForEach(items, item =>
{
ProcessItem(item); // CPU-bound work spread across threads
});
var channel = Channel.CreateUnbounded<int>();
async Task ProducerAsync()
{
for (int i = 0; i < 10; i++)
{
await channel.Writer.WriteAsync(i);
}
channel.Writer.Complete();
}
async Task ConsumerAsync()
{
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine(item);
}
}
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
Running independent async calls concurrently instead of sequentially:
var task1 = FetchDataAsync(url1);
var task2 = FetchDataAsync(url2);
var results = await Task.WhenAll(task1, task2);
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Calling task.Result or task.Wait() from synchronous code that is
itself called from an async context with a captured
SynchronizationContext (classic ASP.NET, WPF, WinForms) -- a
well-known deadlock trap. Use await consistently, or
ConfigureAwait(false) in library code that does not need to resume
on the original context.
"async void" methods (other than event handlers) -- exceptions
thrown inside them cannot be awaited/caught by the caller and will
crash the process instead. Always return Task, not void, unless
it's a UI event handler.
Forgetting to pass a CancellationToken through an entire async call
chain, making a "cancellable" operation not actually cancellable
partway through.
Using lock around awaited code -- lock cannot be held across an
await, and the compiler will refuse to build it. Use SemaphoreSlim
instead for async-safe mutual exclusion.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to explain why async/await frees up threads while waiting
on I/O (the thread is returned to the pool during the await, not
blocked), versus Task.Run, which explicitly schedules CPU-bound work
onto a thread-pool thread.
Know the classic ASP.NET classic (not Core) deadlock scenario caused
by blocking on async code with .Result inside a request that has a
captured synchronization context, and why ASP.NET Core mostly avoids
this problem (no SynchronizationContext by default).
================================================================================
ASPNET CORE
================================================================================
DESCRIPTION
ASP.NET Core is the cross-platform web framework for building APIs,
MVC apps, and real-time services on .NET.
-------------------------------------------------------------------------------
PROGRAM.CS (MINIMAL APIS)
-------------------------------------------------------------------------------
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IOrderService, OrderService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapGet("/health", () => Results.Ok("Healthy"));
app.MapGet("/orders/{id:int}", async (int id, IOrderService svc) =>
{
var order = await svc.GetByIdAsync(id);
return order is null ? Results.NotFound() : Results.Ok(order);
});
app.Run();
-------------------------------------------------------------------------------
CONTROLLERS (MVC-STYLE)
-------------------------------------------------------------------------------
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orders;
public OrdersController(IOrderService orders) => _orders = orders;
[HttpGet("{id:int}")]
public async Task<IActionResult> GetById(int id)
{
var order = await _orders.GetByIdAsync(id);
return order is null ? NotFound() : Ok(order);
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateOrderRequest request)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
var created = await _orders.CreateAsync(request);
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
}
}
-------------------------------------------------------------------------------
MIDDLEWARE
-------------------------------------------------------------------------------
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next();
Console.WriteLine($"{context.Request.Path} took {stopwatch.ElapsedMilliseconds}ms");
});
Custom middleware class:
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
public RequestTimingMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
await _next(context);
Console.WriteLine($"{context.Request.Path}: {sw.ElapsedMilliseconds}ms");
}
}
app.UseMiddleware<RequestTimingMiddleware>();
-------------------------------------------------------------------------------
DEPENDENCY INJECTION
-------------------------------------------------------------------------------
builder.Services.AddSingleton<ICacheService, MemoryCacheService>(); // one instance, app lifetime
builder.Services.AddScoped<IOrderService, OrderService>(); // one instance per request
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>(); // new instance every time
Constructor injection is the standard pattern:
public class OrderService : IOrderService
{
private readonly AppDbContext _db;
private readonly ILogger<OrderService> _logger;
public OrderService(AppDbContext db, ILogger<OrderService> logger)
{
_db = db;
_logger = logger;
}
}
-------------------------------------------------------------------------------
AUTHENTICATION AND JWT
-------------------------------------------------------------------------------
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = config["Jwt:Issuer"],
ValidAudience = config["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(config["Jwt:Key"]!))
};
});
[Authorize]
[HttpGet("me")]
public IActionResult GetCurrentUser() =>
Ok(new { UserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value });
[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public IActionResult Delete(int id) => NoContent();
-------------------------------------------------------------------------------
CONFIGURATION AND OPTIONS PATTERN
-------------------------------------------------------------------------------
public class SmtpSettings
{
public string Host { get; set; } = "";
public int Port { get; set; }
}
builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("Smtp"));
public class EmailSender
{
private readonly SmtpSettings _settings;
public EmailSender(IOptions<SmtpSettings> options) => _settings = options.Value;
}
-------------------------------------------------------------------------------
LOGGING
-------------------------------------------------------------------------------
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger) => _logger = logger;
public void Process(int orderId)
{
_logger.LogInformation("Processing order {OrderId}", orderId);
_logger.LogWarning("Order {OrderId} is missing a shipping address", orderId);
_logger.LogError(exception, "Failed to process order {OrderId}", orderId);
}
}
-------------------------------------------------------------------------------
MODEL BINDING AND VALIDATION
-------------------------------------------------------------------------------
public class CreateOrderRequest
{
[Required]
public string CustomerName { get; set; } = "";
[Range(1, int.MaxValue)]
public int Quantity { get; set; }
[EmailAddress]
public string Email { get; set; } = "";
}
[HttpGet]
public IActionResult Search([FromQuery] string term, [FromHeader] string? apiKey) =>
Ok();
-------------------------------------------------------------------------------
SIGNALR (REAL-TIME)
-------------------------------------------------------------------------------
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
builder.Services.AddSignalR();
app.MapHub<ChatHub>("/chatHub");
-------------------------------------------------------------------------------
BACKGROUND SERVICES
-------------------------------------------------------------------------------
public class CleanupWorker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoCleanupAsync();
await Task.Delay(TimeSpan.FromMinutes(10), stoppingToken);
}
}
}
builder.Services.AddHostedService<CleanupWorker>();
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Registering a DbContext (which is meant to be Scoped) as Singleton,
causing thread-safety issues since DbContext is not designed to be
shared across concurrent requests.
Injecting a Scoped service into a Singleton service directly, which
throws at startup or resolves a captured, stale instance -- resolve
Scoped dependencies from an IServiceScopeFactory inside the
Singleton instead.
Forgetting [ApiController] or explicit [FromBody]/[FromQuery]
attributes and being surprised when model binding does not pick up
values from where you expected.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to explain the three DI lifetimes (Singleton, Scoped,
Transient) with a concrete example of when each is appropriate, and
the specific danger of "captive dependencies" (a longer-lived
service holding a reference to a shorter-lived one).
================================================================================
ENTITY FRAMEWORK CORE
================================================================================
DESCRIPTION
EF Core is the standard object-relational mapper (ORM) for .NET,
mapping C# classes to database tables and LINQ queries to SQL.
-------------------------------------------------------------------------------
DBCONTEXT AND DBSET
-------------------------------------------------------------------------------
public class AppDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<Customer> Customers => Set<Customer>();
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerId);
modelBuilder.Entity<Order>()
.HasIndex(o => o.CreatedAt);
}
}
-------------------------------------------------------------------------------
MIGRATIONS
-------------------------------------------------------------------------------
dotnet ef migrations add InitialCreate
dotnet ef migrations add AddOrderStatusColumn
dotnet ef database update
dotnet ef migrations remove
dotnet ef migrations script // generates raw SQL
dotnet ef database drop
(Requires the dotnet-ef tool: dotnet tool install -g dotnet-ef)
-------------------------------------------------------------------------------
RELATIONSHIPS
-------------------------------------------------------------------------------
One to many:
public class Customer
{
public int Id { get; set; }
public List<Order> Orders { get; set; } = new();
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; } = null!;
}
Many to many (EF Core 5+, no join entity needed for the simple case):
public class Student
{
public int Id { get; set; }
public List<Course> Courses { get; set; } = new();
}
public class Course
{
public int Id { get; set; }
public List<Student> Students { get; set; } = new();
}
One to one:
public class User
{
public int Id { get; set; }
public UserProfile Profile { get; set; } = null!;
}
public class UserProfile
{
public int Id { get; set; }
public int UserId { get; set; }
public User User { get; set; } = null!;
}
-------------------------------------------------------------------------------
QUERIES
-------------------------------------------------------------------------------
var order = await db.Orders.FindAsync(id);
var recent = await db.Orders
.Where(o => o.CreatedAt > DateTime.UtcNow.AddDays(-7))
.OrderByDescending(o => o.CreatedAt)
.ToListAsync();
var withCustomer = await db.Orders
.Include(o => o.Customer)
.ThenInclude(c => c.Address)
.ToListAsync();
var projected = await db.Orders
.Select(o => new { o.Id, o.Total, CustomerName = o.Customer.Name })
.ToListAsync();
-------------------------------------------------------------------------------
TRACKING
-------------------------------------------------------------------------------
Tracked queries (default) let EF detect changes for SaveChanges():
var order = await db.Orders.FirstAsync(o => o.Id == id);
order.Status = OrderStatus.Shipped;
await db.SaveChangesAsync();
No-tracking queries are faster for read-only scenarios:
var orders = await db.Orders.AsNoTracking().ToListAsync();
-------------------------------------------------------------------------------
TRANSACTIONS
-------------------------------------------------------------------------------
using var transaction = await db.Database.BeginTransactionAsync();
try
{
db.Orders.Add(order);
await db.SaveChangesAsync();
db.Inventory.Update(inventoryItem);
await db.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
-------------------------------------------------------------------------------
CONCURRENCY
-------------------------------------------------------------------------------
public class Order
{
public int Id { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; } = null!; // optimistic concurrency token
}
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
// another process modified/deleted the row first; reload and retry
}
-------------------------------------------------------------------------------
LOADING STRATEGIES
-------------------------------------------------------------------------------
Eager loading: .Include(o => o.Customer) -- one query with a JOIN
Explicit loading: await db.Entry(order).Reference(o => o.Customer).LoadAsync();
Lazy loading: requires virtual navigation properties plus the
Microsoft.EntityFrameworkCore.Proxies package;
loads related data automatically on first access,
but easy to cause accidental N+1 query patterns.
-------------------------------------------------------------------------------
INDEXES AND PERFORMANCE
-------------------------------------------------------------------------------
modelBuilder.Entity<Order>().HasIndex(o => o.CustomerId);
modelBuilder.Entity<Order>().HasIndex(o => new { o.CustomerId, o.CreatedAt });
Watch query plans with:
dotnet ef migrations script // review generated SQL/index DDL
Or log generated SQL directly:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString).LogTo(Console.WriteLine, LogLevel.Information));
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
The N+1 query problem: looping over a collection and accessing a
navigation property inside the loop without .Include(), triggering
one extra query per row instead of a single JOIN.
Forgetting AsNoTracking() on large read-only query results, adding
unnecessary change-tracking overhead.
Calling .ToList() too early in a LINQ chain (before Where/Select),
which pulls the entire table into memory and finishes filtering in
C# instead of pushing the filter down into SQL.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to explain the N+1 problem with a concrete before/after
example, and how .Include()/.ThenInclude() (or a projection with
.Select()) solves it by folding related data into a single query.
================================================================================
JSON
================================================================================
DESCRIPTION
System.Text.Json is the built-in, high-performance JSON library in
modern .NET, generally preferred over the third-party
Newtonsoft.Json for new code, though Newtonsoft remains common in
older codebases and has some features System.Text.Json lacks.
-------------------------------------------------------------------------------
SERIALIZATION
-------------------------------------------------------------------------------
using System.Text.Json;
var person = new Person { Name = "Jake", Age = 30 };
string json = JsonSerializer.Serialize(person);
string prettyJson = JsonSerializer.Serialize(person,
new JsonSerializerOptions { WriteIndented = true });
-------------------------------------------------------------------------------
DESERIALIZATION
-------------------------------------------------------------------------------
string json = "{\"Name\":\"Jake\",\"Age\":30}";
Person? person = JsonSerializer.Deserialize<Person>(json);
Deserializing to a dynamic-ish document when the shape is unknown:
using var doc = JsonDocument.Parse(json);
string name = doc.RootElement.GetProperty("Name").GetString()!;
-------------------------------------------------------------------------------
OPTIONS
-------------------------------------------------------------------------------
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true
};
-------------------------------------------------------------------------------
ATTRIBUTES
-------------------------------------------------------------------------------
public class Person
{
[JsonPropertyName("full_name")]
public string Name { get; set; } = "";
[JsonIgnore]
public string InternalNotes { get; set; } = "";
[JsonPropertyOrder(1)]
public int Age { get; set; }
}
-------------------------------------------------------------------------------
CUSTOM CONVERTERS
-------------------------------------------------------------------------------
public class DateOnlyConverter : JsonConverter<DateOnly>
{
public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options) =>
DateOnly.Parse(reader.GetString()!);
public override void Write(Utf8JsonWriter writer, DateOnly value,
JsonSerializerOptions options) =>
writer.WriteStringValue(value.ToString("yyyy-MM-dd"));
}
var options = new JsonSerializerOptions();
options.Converters.Add(new DateOnlyConverter());
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Forgetting that System.Text.Json is case-sensitive on property names
by default when reading (unlike Newtonsoft), leading to silently
null-populated properties instead of an error. Set
PropertyNameCaseInsensitive = true if the source JSON's casing is
not guaranteed to match exactly.
Trying to deserialize into a type with only a parameterized
constructor and no parameterless constructor or init/settable
properties matching the JSON, which throws at runtime; for records,
ensure constructor parameter names match JSON property names
(case-insensitively) or add [JsonConstructor].
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Know that System.Text.Json is built for performance (low
allocation, Span-based parsing) whereas Newtonsoft.Json historically
offered more flexibility (looser type handling, more attributes) --
a reasonable trade-off to describe if asked to compare them.
================================================================================
FILE IO
================================================================================
DESCRIPTION
System.IO provides both simple static helpers (File, Directory) and
stream-based APIs for more control over reading and writing.
-------------------------------------------------------------------------------
READING
-------------------------------------------------------------------------------
string text = File.ReadAllText("data.txt");
string[] lines = File.ReadAllLines("data.txt");
byte[] bytes = File.ReadAllBytes("data.bin");
string text2 = await File.ReadAllTextAsync("data.txt");
foreach (var line in File.ReadLines("big-file.txt")) // lazy, low memory
{
Console.WriteLine(line);
}
-------------------------------------------------------------------------------
WRITING
-------------------------------------------------------------------------------
File.WriteAllText("data.txt", "hello world");
File.WriteAllLines("data.txt", new[] { "line1", "line2" });
File.AppendAllText("log.txt", "new entry\n");
await File.WriteAllTextAsync("data.txt", "hello world");
-------------------------------------------------------------------------------
STREAMS
-------------------------------------------------------------------------------
using var reader = new StreamReader("data.txt");
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
Console.WriteLine(line);
}
using var writer = new StreamWriter("output.txt", append: true);
await writer.WriteLineAsync("new entry");
-------------------------------------------------------------------------------
DIRECTORIES AND PATHS
-------------------------------------------------------------------------------
Directory.Exists(path)
Directory.CreateDirectory(path)
Directory.GetFiles(path, "*.txt")
Directory.GetDirectories(path)
Directory.Delete(path, recursive: true)
Path.Combine("folder", "subfolder", "file.txt")
Path.GetFileName(fullPath)
Path.GetExtension(fullPath)
Path.GetDirectoryName(fullPath)
Path.GetFullPath(relativePath)
-------------------------------------------------------------------------------
FILE MANAGEMENT
-------------------------------------------------------------------------------
File.Exists(path)
File.Copy(source, dest, overwrite: true)
File.Move(source, dest)
File.Delete(path)
var info = new FileInfo(path);
info.Length
info.LastWriteTimeUtc
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Using File.ReadAllText/ReadAllLines on very large files, loading the
entire content into memory at once. Use File.ReadLines (lazy) or a
StreamReader for large files.
Forgetting to dispose streams (StreamReader/StreamWriter/FileStream)
-- always wrap them in a using statement, since file handles are a
limited OS resource.
Building file paths with hardcoded "/" or "\\" separators instead of
Path.Combine, breaking cross-platform compatibility.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Know when to reach for the simple File.* static helpers (small
files, quick scripts) versus Stream-based APIs (large files,
fine-grained control, async streaming).
================================================================================
NETWORKING
================================================================================
DESCRIPTION
Covers HttpClient for calling web APIs, plus the lower-level socket
primitives for TCP/UDP.
-------------------------------------------------------------------------------
HTTPCLIENT
-------------------------------------------------------------------------------
Reuse a single HttpClient (or use IHttpClientFactory in ASP.NET
Core apps) rather than creating a new one per call.
public class WeatherApiClient
{
private readonly HttpClient _client;
public WeatherApiClient(HttpClient client) => _client = client;
public async Task<WeatherResponse?> GetWeatherAsync(string city)
{
var response = await _client.GetAsync($"/weather?city={Uri.EscapeDataString(city)}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<WeatherResponse>();
}
}
Registering with IHttpClientFactory in Program.cs:
builder.Services.AddHttpClient<WeatherApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.weather.example.com");
client.Timeout = TimeSpan.FromSeconds(10);
});
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
client.GetAsync(url)
client.PostAsJsonAsync(url, payload)
client.PutAsJsonAsync(url, payload)
client.DeleteAsync(url)
response.EnsureSuccessStatusCode()
response.Content.ReadAsStringAsync()
response.Content.ReadFromJsonAsync<T>()
response.StatusCode
response.IsSuccessStatusCode
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
var payload = new { Name = "Jake", Age = 30 };
var response = await client.PostAsJsonAsync("/api/people", payload);
response.EnsureSuccessStatusCode();
var created = await response.Content.ReadFromJsonAsync<Person>();
-------------------------------------------------------------------------------
SOCKETS (TCP / UDP)
-------------------------------------------------------------------------------
TCP server (bare-bones):
var listener = new TcpListener(IPAddress.Any, 5000);
listener.Start();
using var client = await listener.AcceptTcpClientAsync();
using var stream = client.GetStream();
var buffer = new byte[1024];
int bytesRead = await stream.ReadAsync(buffer);
UDP send/receive:
using var udpClient = new UdpClient();
var data = Encoding.UTF8.GetBytes("hello");
await udpClient.SendAsync(data, data.Length, "127.0.0.1", 5000);
-------------------------------------------------------------------------------
DNS
-------------------------------------------------------------------------------
IPHostEntry entry = await Dns.GetHostEntryAsync("example.com");
foreach (var ip in entry.AddressList) Console.WriteLine(ip);
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Creating a new HttpClient per request (either directly or via
"using var client = new HttpClient()") under load, which can exhaust
available sockets because each disposed HttpClient's underlying
connections linger in a TIME_WAIT state. Reuse via
IHttpClientFactory or a single static/injected instance.
Not calling EnsureSuccessStatusCode() (or checking IsSuccessStatusCode
manually), silently proceeding to parse an error response body as if
it were a successful one.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to explain socket exhaustion from HttpClient misuse -- a
common real-world .NET production incident and a frequent system-
design/practical interview topic.
================================================================================
POSTGRESQL
================================================================================
DESCRIPTION
Covers core SQL as used against PostgreSQL, plus the Npgsql .NET
driver for connecting directly (outside of EF Core, or for raw SQL
inside it).
-------------------------------------------------------------------------------
BASIC SQL
-------------------------------------------------------------------------------
SELECT id, name, email FROM customers WHERE created_at > NOW() - INTERVAL '7 days';
INSERT INTO orders (customer_id, total) VALUES (1, 99.99) RETURNING id;
UPDATE orders SET status = 'shipped' WHERE id = 42;
DELETE FROM orders WHERE status = 'cancelled' AND created_at < NOW() - INTERVAL '30 days';
-------------------------------------------------------------------------------
JOINS
-------------------------------------------------------------------------------
SELECT o.id, o.total, c.name
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id;
SELECT c.name, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.name;
-------------------------------------------------------------------------------
INDEXES
-------------------------------------------------------------------------------
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
CREATE INDEX idx_orders_created_at ON orders (created_at DESC);
CREATE UNIQUE INDEX idx_customers_email ON customers (email);
-------------------------------------------------------------------------------
CONSTRAINTS
-------------------------------------------------------------------------------
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INT NOT NULL REFERENCES customers(id),
total NUMERIC(10, 2) NOT NULL CHECK (total >= 0),
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-------------------------------------------------------------------------------
TRANSACTIONS
-------------------------------------------------------------------------------
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- or ROLLBACK; on failure
-------------------------------------------------------------------------------
VIEWS AND STORED PROCEDURES
-------------------------------------------------------------------------------
CREATE VIEW active_customers AS
SELECT * FROM customers WHERE last_login > NOW() - INTERVAL '30 days';
CREATE OR REPLACE FUNCTION get_order_total(order_id INT)
RETURNS NUMERIC AS $$
BEGIN
RETURN (SELECT total FROM orders WHERE id = order_id);
END;
$$ LANGUAGE plpgsql;
-------------------------------------------------------------------------------
NPGSQL (C# DRIVER)
-------------------------------------------------------------------------------
var connectionString = "Host=localhost;Database=mydb;Username=user;Password=pass";
await using var conn = new NpgsqlConnection(connectionString);
await conn.OpenAsync();
await using var cmd = new NpgsqlCommand("SELECT id, name FROM customers WHERE id = @id", conn);
cmd.Parameters.AddWithValue("id", 1);
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
Console.WriteLine(reader.GetString(reader.GetOrdinal("name")));
}
EF Core connection string setup:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Building SQL by string concatenation with user input, opening the
door to SQL injection. Always use parameterized queries
(cmd.Parameters.AddWithValue or EF Core's parameterized LINQ, never
raw string interpolation of user data into SQL text).
Forgetting an index on a foreign key column, causing slow JOINs and
slow cascading deletes as the table grows.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to explain the difference between INNER JOIN and LEFT JOIN
with a concrete example, and why parameterized queries are
non-negotiable for any SQL touching user input.
================================================================================
COMMON EXCEPTIONS
================================================================================
DESCRIPTION
A quick-reference for the exceptions that come up constantly in
day-to-day C# development, what typically causes them, and the
standard fix.
-------------------------------------------------------------------------------
NULLREFERENCEEXCEPTION
-------------------------------------------------------------------------------
Cause: dereferencing a null reference (calling a method or accessing
a property/field on a variable that is null).
Fix: null-conditional operator (?.), null-coalescing (??), enabling
nullable reference types to catch it at compile time, or an explicit
null check.
-------------------------------------------------------------------------------
INVALIDOPERATIONEXCEPTION
-------------------------------------------------------------------------------
Cause: calling a method when the object is in an invalid state for
it -- e.g. modifying a collection during foreach, calling
Dequeue()/Pop() on an empty Queue/Stack, or calling Single() on a
sequence with more than one element.
Fix: check preconditions first (Count > 0), or use Try* variants
(TryDequeue, TryPop) where available.
-------------------------------------------------------------------------------
KEYNOTFOUNDEXCEPTION
-------------------------------------------------------------------------------
Cause: indexing a Dictionary<TKey, TValue> with a key that does not
exist.
Fix: TryGetValue or GetValueOrDefault instead of the indexer.
-------------------------------------------------------------------------------
ARGUMENTEXCEPTION / ARGUMENTNULLEXCEPTION / ARGUMENTOUTOFRANGEEXCEPTION
-------------------------------------------------------------------------------
Cause: an invalid argument was passed to a method -- null when not
allowed, an out-of-range index, or some other invalid value.
Fix: validate inputs at the top of a method (ArgumentNullException.
ThrowIfNull(value) is the modern shorthand, .NET 6+) and let the
caller see a clear, specific error instead of a confusing failure
deeper in the call stack.
-------------------------------------------------------------------------------
INDEXOUTOFRANGEEXCEPTION
-------------------------------------------------------------------------------
Cause: accessing an array or string index outside its valid bounds.
Fix: bounds-check before indexing, or use range/index operators
([^1], [..]) that are less error-prone than manual arithmetic.
-------------------------------------------------------------------------------
FORMATEXCEPTION
-------------------------------------------------------------------------------
Cause: parsing a string into a number/date with int.Parse,
double.Parse, DateTime.Parse, etc. when the string is not in the
expected format.
Fix: use the TryParse variants and handle the false case explicitly
instead of relying on try/catch for expected bad input.
-------------------------------------------------------------------------------
OVERFLOWEXCEPTION
-------------------------------------------------------------------------------
Cause: an arithmetic operation exceeds the range of the target type,
inside a "checked" context (checked keyword or
<CheckForOverflowUnderflow> set in the project).
Fix: use a larger type (long instead of int), or explicitly decide
whether overflow should wrap (unchecked) or throw (checked)
depending on the situation.
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Catching System.Exception broadly across an entire method body
instead of catching the specific exception type the code can
meaningfully recover from, which hides bugs and makes debugging
production issues much harder.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to name the specific exception type for a scenario
(dictionary missing key -> KeyNotFoundException, empty stack pop ->
InvalidOperationException, etc.) -- interviewers sometimes probe this
to confirm hands-on experience versus surface familiarity.
================================================================================
DEBUGGING
================================================================================
DESCRIPTION
Tools and techniques for finding and fixing bugs in C#/.NET code,
both from an IDE and from the command line.
-------------------------------------------------------------------------------
CONSOLE AND TRACE
-------------------------------------------------------------------------------
Console.WriteLine($"value: {value}");
Console.Error.WriteLine("something went wrong");
Debug.WriteLine("only compiled in Debug builds");
Debug.Assert(value > 0, "value should always be positive here");
Trace.WriteLine("logged regardless of build configuration");
-------------------------------------------------------------------------------
NAMEOF AND TYPEOF
-------------------------------------------------------------------------------
nameof(someVariable) // "someVariable" -- refactor-safe, unlike
a hardcoded string
typeof(SomeClass) // System.Type object for SomeClass
someObject.GetType() // runtime type of an instance
someObject.GetType().Name // "SomeClass"
Common pairing in argument validation:
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "must be non-negative");
}
-------------------------------------------------------------------------------
BREAKPOINTS AND STEPPING
-------------------------------------------------------------------------------
F9 (or clicking the gutter) toggle a breakpoint
F5 start debugging / continue
F10 step over
F11 step into
Shift+F11 step out
Conditional breakpoints right-click a breakpoint, add a
condition expression (e.g. i == 500)
so it only stops when true
-------------------------------------------------------------------------------
STACK TRACES
-------------------------------------------------------------------------------
try
{
DoSomething();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString()); // includes full stack trace
Console.WriteLine(ex.StackTrace); // just the trace
}
Reading a stack trace top-to-bottom shows the innermost failing
frame first, working outward to the original caller.
-------------------------------------------------------------------------------
IMMEDIATE / WATCH WINDOWS
-------------------------------------------------------------------------------
Available in Visual Studio and Rider while paused at a breakpoint:
Watch window pin an expression to see its value update live
as you step through code
Immediate window type and execute arbitrary C# expressions
against the current paused state (e.g. call a
method, inspect a private field)
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Leaving Console.WriteLine debugging statements in committed code
instead of using a proper logger (ILogger<T>) that can be filtered
by level and routed to real log storage in production.
Debugging a Release build, where optimizations can reorder or
inline code in ways that make stepping through confusing or
inaccurate. Debug in a Debug build configuration.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to describe your actual debugging process for a tricky bug
(reproduce reliably, bisect with breakpoints/logging, form a
hypothesis, verify it) -- this is a common behavioral/practical
interview question independent of language specifics.
================================================================================
DESIGN PATTERNS
================================================================================
DESCRIPTION
Common object-oriented design patterns as they typically appear in
C# codebases, with idiomatic .NET touches (interfaces, generics, DI)
layered on top of the classic Gang of Four shapes.
-------------------------------------------------------------------------------
FACTORY
-------------------------------------------------------------------------------
public interface IShape { double Area(); }
public class Circle : IShape { public double Radius; public double Area() => Math.PI * Radius * Radius; }
public class Square : IShape { public double Side; public double Area() => Side * Side; }
public static class ShapeFactory
{
public static IShape Create(string type, double size) => type switch
{
"circle" => new Circle { Radius = size },
"square" => new Square { Side = size },
_ => throw new ArgumentException($"Unknown shape type: {type}")
};
}
-------------------------------------------------------------------------------
REPOSITORY
-------------------------------------------------------------------------------
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(int id);
Task AddAsync(Order order);
Task SaveChangesAsync();
}
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _db;
public OrderRepository(AppDbContext db) => _db = db;
public Task<Order?> GetByIdAsync(int id) => _db.Orders.FindAsync(id).AsTask();
public Task AddAsync(Order order) { _db.Orders.Add(order); return Task.CompletedTask; }
public Task SaveChangesAsync() => _db.SaveChangesAsync();
}
-------------------------------------------------------------------------------
BUILDER
-------------------------------------------------------------------------------
public class EmailBuilder
{
private readonly StringBuilder _body = new();
private string _subject = "";
public EmailBuilder WithSubject(string subject) { _subject = subject; return this; }
public EmailBuilder AddLine(string line) { _body.AppendLine(line); return this; }
public Email Build() => new Email(_subject, _body.ToString());
}
var email = new EmailBuilder()
.WithSubject("Welcome")
.AddLine("Hi Jake,")
.AddLine("Thanks for signing up.")
.Build();
-------------------------------------------------------------------------------
STRATEGY
-------------------------------------------------------------------------------
public interface IDiscountStrategy { decimal Apply(decimal total); }
public class NoDiscount : IDiscountStrategy { public decimal Apply(decimal total) => total; }
public class PercentOffDiscount : IDiscountStrategy
{
private readonly decimal _percent;
public PercentOffDiscount(decimal percent) => _percent = percent;
public decimal Apply(decimal total) => total * (1 - _percent);
}
public class Checkout
{
private readonly IDiscountStrategy _discount;
public Checkout(IDiscountStrategy discount) => _discount = discount;
public decimal GetTotal(decimal subtotal) => _discount.Apply(subtotal);
}
-------------------------------------------------------------------------------
OBSERVER
-------------------------------------------------------------------------------
Typically implemented in C# using events (see DELEGATES AND EVENTS)
rather than a hand-rolled Subject/Observer interface pair:
public class StockTicker
{
public event EventHandler<decimal>? PriceChanged;
public void UpdatePrice(decimal newPrice) => PriceChanged?.Invoke(this, newPrice);
}
-------------------------------------------------------------------------------
DECORATOR
-------------------------------------------------------------------------------
public interface INotifier { void Send(string message); }
public class EmailNotifier : INotifier { public void Send(string message) => Console.WriteLine($"Email: {message}"); }
public class SmsDecorator : INotifier
{
private readonly INotifier _inner;
public SmsDecorator(INotifier inner) => _inner = inner;
public void Send(string message)
{
_inner.Send(message);
Console.WriteLine($"SMS: {message}");
}
}
INotifier notifier = new SmsDecorator(new EmailNotifier());
notifier.Send("Order shipped"); // sends both email and SMS
-------------------------------------------------------------------------------
ADAPTER
-------------------------------------------------------------------------------
public interface IModernLogger { void Log(string message); }
public class LegacyLogger // third-party class you cannot modify
{
public void WriteToLog(string text) => Console.WriteLine(text);
}
public class LegacyLoggerAdapter : IModernLogger
{
private readonly LegacyLogger _legacy;
public LegacyLoggerAdapter(LegacyLogger legacy) => _legacy = legacy;
public void Log(string message) => _legacy.WriteToLog(message);
}
-------------------------------------------------------------------------------
SINGLETON
-------------------------------------------------------------------------------
In modern .NET, prefer AddSingleton<T> in the DI container over a
hand-rolled static singleton, since it stays testable and mockable:
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
The classic manual pattern, when DI is not available:
public sealed class ConfigManager
{
private static readonly Lazy<ConfigManager> _instance = new(() => new ConfigManager());
public static ConfigManager Instance => _instance.Value;
private ConfigManager() { }
}
-------------------------------------------------------------------------------
DEPENDENCY INJECTION
-------------------------------------------------------------------------------
See ASPNET CORE section for the three DI lifetimes (Singleton,
Scoped, Transient) and constructor injection examples. DI is less a
single "pattern" than the general practice this whole cheat sheet's
ASP.NET Core examples already lean on throughout.
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Reaching for a design pattern because it is "the correct
architecture" rather than because it solves a concrete problem in
the code at hand -- patterns add indirection, which is only worth it
when it buys real flexibility or testability.
Implementing a hand-rolled Singleton in an ASP.NET Core app instead
of just registering the type with AddSingleton, losing testability
and making the dependency invisible to constructors.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
Be ready to give a real example from your own projects for at least
Factory, Strategy, Repository, and Decorator -- these four come up
disproportionately often relative to the full Gang of Four catalog
in day-to-day C#/.NET interviews.
================================================================================
BEST PRACTICES
================================================================================
DESCRIPTION
General guidance on modern C#/.NET idioms, naming, performance, and
the kind of things that come up in code review and system design
interviews alike.
-------------------------------------------------------------------------------
NAMING CONVENTIONS
-------------------------------------------------------------------------------
PascalCase classes, methods, properties, public fields, namespaces
camelCase local variables, method parameters
_camelCase private fields (leading underscore is the common
.NET convention, though "camelCase without
underscore" is also seen)
IPascalCase interfaces, always prefixed with a capital I
ALL_CAPS rarely used in C#; prefer PascalCase even for constants
-------------------------------------------------------------------------------
MODERN C# IDIOMS
-------------------------------------------------------------------------------
Prefer var when the type is obvious from the right-hand side;
prefer an explicit type when it improves readability for a reader
unfamiliar with the method being called.
Prefer expression-bodied members for simple one-liners:
public int Square(int x) => x * x;
Prefer records over classes for immutable data-transfer objects.
Prefer pattern matching (is, switch expressions) over long
if/else-if chains checking types or ranges.
Enable nullable reference types (<Nullable>enable</Nullable>) on new
projects to catch a large class of null-reference bugs at compile
time instead of runtime.
-------------------------------------------------------------------------------
PERFORMANCE
-------------------------------------------------------------------------------
Avoid unnecessary allocations in hot paths: prefer Span<T> and
ReadOnlySpan<T> over substring/array-copy operations when just
reading a slice of existing memory.
Use StringBuilder for string concatenation in loops.
Avoid LINQ in extremely hot paths where allocation from iterator
state machines and closures matters; a plain for loop can be
meaningfully faster there, at some cost to readability.
Reuse HttpClient instances; never create one per request.
Prefer async I/O throughout a call chain rather than mixing
blocking calls (.Result, .Wait()) into an otherwise async pipeline.
-------------------------------------------------------------------------------
MEMORY
-------------------------------------------------------------------------------
Understand generational garbage collection at a high level: Gen 0
(short-lived objects, collected frequently and cheaply), Gen 1
(a buffer between Gen 0 and Gen 2), and Gen 2 (long-lived objects,
collected less often but at higher relative cost). Minimizing Gen 0
allocation pressure is usually the highest-leverage GC-related
performance improvement available.
Dispose IDisposable resources deterministically with using rather
than relying on the finalizer, which runs at an unpredictable time
(or possibly never, under process termination).
-------------------------------------------------------------------------------
CLEAN CODE
-------------------------------------------------------------------------------
Keep methods short and focused on one responsibility; a method that
needs a comment to explain "what" it does (as opposed to "why") is
often a sign it should be split or renamed.
Prefer guard clauses (early returns for invalid input) over deeply
nested if blocks.
Favor composition over inheritance once an inheritance hierarchy
starts requiring more than 2-3 levels to express the relationship.
Keep public API surface (of a class, of a library) as small as
possible; it is much easier to add a new public member later than
to remove one without a breaking change.
-------------------------------------------------------------------------------
COMMON INTERVIEW QUESTIONS
-------------------------------------------------------------------------------
Explain the difference between == and .Equals() for reference types
vs value types vs records vs strings.
Explain value types vs reference types, and where each lives in
memory.
Explain async/await and what "the thread returns to the pool during
an await" actually means in practice.
Explain the SOLID principles with a concrete C# example for each.
Explain the difference between abstract classes and interfaces, and
when you would choose one over the other.
Walk through how garbage collection works at a high level, and what
IDisposable/using are for given that .NET already has a GC.
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Treating "best practice" as a fixed rulebook rather than a set of
trade-offs -- e.g. LINQ readability vs raw loop performance, or
interfaces-everywhere vs YAGNI. Being able to articulate the
trade-off, not just recite the rule, reads much stronger in an
interview than a rule recited without justification.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
When asked an open-ended "what would you improve about this code"
question, a strong structure is: correctness issues first (bugs,
edge cases), then readability/maintainability, then performance --
in roughly that priority order, unless the prompt specifically asks
about one dimension.
===============================================================================
LEET CODE PROBLEMS
===============================================================================
So I am going to continue on writing my own adding my solved leetcode problems
This will help me prepare for job interviews and jog down stuff that I find important
over time.
-------------------------------------------------------------------------------
REVERSE LINKED LIST
-------------------------------------------------------------------------------
public ListNode ReverseList(ListNode head)
{
ListNode prev = null;
ListNode curr = head;
while (curr != null)
{
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
------------------------------------------------------------------------------
INVERT TREE
------------------------------------------------------------------------------
public TreeNode InvertTree(TreeNode root)
{
if (root == null)
return null;
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
InvertTree(root.left);
InvertTree(root.right);
return root;
}
================================================================================
END OF FILE
================================================================================