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
================================================================================