Back to cheat sheets

Languages

C#

A to-the-point review of the C# an SDET gets asked about: data types, modifiers, OOP, strings, collections, exceptions, async/await, and LINQ — plus the NUnit/xUnit + Playwright testing stack. Short explanations, short examples.

01Data Types & Variables

Every type is either a value type or a reference type. This one distinction explains most C# behaviour questions.

  • Value types hold the data itself and are copied on assignment. Examples: int, long, double, decimal, bool, char, enum, struct, DateTime.
  • Reference types hold a reference to data on the heap; assigning copies the reference, so both variables point to the same object. Examples: class, string, arrays, object, interfaces, delegates.
int a = 1; int b = a; b = 9;            // a stays 1 (value copied)

int[] x = { 1 }; int[] y = x; y[0] = 9; // x[0] is now 9 (same array)
  • var — the compiler infers the type; the variable is still strongly typed.
  • const — compile-time constant. readonly — set once, in the constructor.
  • Nullable: value types are non-null by default; int? allows null. Use ?., ??, ??= for null-safety (name ??= "anon";).
  • Boxing = wrapping a value type in a heap object; unboxing reverses it. It allocates, so avoid it in hot loops.
Common trap: passing a reference type to a method lets it mutate the object (the caller sees the change), but reassigning the parameter inside the method does not affect the caller — C# passes the reference by value.

02Modifiers & Keywords

Access modifiers — who can see a member:

  • public — anywhere.
  • private — only inside the same class (the default for class members).
  • protected — the class and its subclasses.
  • internal — anywhere in the same project/assembly.
  • protected internal / private protected — combinations, rarely needed.

Other modifiers you'll be asked about:

  • static — belongs to the type, not an instance.
  • abstract — no body; must be overridden; the class can't be instantiated.
  • virtual — has a default body but can be overridden; override replaces it; sealed stops further overriding.
  • const / readonly — immutability (compile-time vs set-once).
  • new — hides an inherited member.

03OOP in C#

The four pillars, briefly:

  • Encapsulation — keep fields private, expose them through public properties (get/set).
  • Inheritance — a class extends one base class (: Base).
  • Polymorphismvirtual/override so the right method runs for the actual object type.
  • Abstraction — expose what, hide how, via abstract classes and interfaces.
Abstract class vs Interface — when & how

Abstract class = a partial base with shared code and state; use it when types are a kind of the same thing ("is-a"). A class extends one base and overrides its abstract members.
Interface = a pure contract / capability; use it when unrelated types must all do the same thing ("can-do"). A class implements many interfaces and provides every member.

public abstract class Shape {              // shared base
    public string Name { get; init; }
    public abstract double Area();         // no body -> subclass must override
}
public interface IDrawable {               // capability
    void Draw();
}

// extend ONE class, implement MANY interfaces
public sealed class Circle : Shape, IDrawable {
    public double R { get; init; }
    public override double Area() => Math.PI * R * R;
    public void Draw() => Console.WriteLine($"O r={R}");
}
class vs struct vs record

class = reference type, mutable, identity equality. struct = value type, copied. record = reference type with value equality and concise immutable syntax — ideal for DTOs/test models; with makes a modified copy.

public record User(int Id, string Name);

var u1 = new User(1, "Ada");
var u2 = u1 with { Name = "Grace" };   // copy with one change
u1 == new User(1, "Ada");              // true — value equality

04Strings & Immutability

string is a reference type but immutable — once created it never changes. Every "modification" (+, Replace, ToUpper) returns a new string and leaves the original untouched.

string s = "hello";
s.ToUpper();        // returns "HELLO" but s is STILL "hello"
s = s.ToUpper();    // reassign to keep the result

Why make it immutable?

  • Thread-safe by nature — no one can change a shared string underneath you.
  • Safe as a dictionary / hash key — its hash never changes.
  • Enables interning: identical string literals are cached and shared in a pool.

The cost: concatenating in a loop creates a new object each pass (O(n²) garbage). Use StringBuilder when you build a string in many steps.

var sb = new StringBuilder();
foreach (var row in rows) sb.Append(row).Append('\n');
string report = sb.ToString();   // one final allocation

"ab" == "a" + "b";               // true — == on strings compares VALUE
string vs StringBuilder: use string for fixed or few values; reach for StringBuilder the moment you concatenate inside a loop.

05async / await

The single most likely C# deep-dive for a Playwright/SDET role, since the API is async throughout.

  • async marks a method returning Task/Task<T>/ValueTask; await suspends without blocking the calling thread.
  • It frees the thread while I/O completes — concurrency without extra threads.
  • Return Task, not void (except event handlers); async void can't be awaited and swallows exceptions.
  • Avoid .Result/.Wait() — they can deadlock; await all the way up.
  • Task.WhenAll runs work concurrently; CancellationToken cooperatively cancels.
public async Task<User> GetUserAsync(int id, CancellationToken ct = default)
{
    var resp = await _client.GetAsync($"/users/{id}", ct);
    resp.EnsureSuccessStatusCode();
    return await resp.Content.ReadFromJsonAsync<User>(cancellationToken: ct);
}

// run several requests concurrently, then await all
var users = await Task.WhenAll(ids.Select(id => GetUserAsync(id)));
Why await beats Thread: awaiting an I/O-bound task returns the thread to the pool until the OS signals completion — so thousands of concurrent requests need only a handful of threads. Threads cost ~1 MB of stack each; tasks are cheap.

06LINQ

Declarative querying over any IEnumerable<T>. Method (fluent) syntax is most common in test code:

var adults = people
    .Where(p => p.Age >= 18)
    .OrderByDescending(p => p.Age)
    .Select(p => p.Name)
    .ToList();

// grouping & aggregation
var byDept = people
    .GroupBy(p => p.Dept)
    .Select(g => new { Dept = g.Key, Count = g.Count(), AvgAge = g.Average(p => p.Age) });

bool anyMinor = people.Any(p => p.Age < 18);
var lookup    = people.ToDictionary(p => p.Id);
  • Deferred execution — the query runs when enumerated (ToList, foreach, Count), not when defined; the source can change in between.
  • Key operators: Where, Select, SelectMany, First/FirstOrDefault, Single, Any/All, GroupBy, OrderBy, Aggregate.
  • FirstOrDefault/SingleOrDefault return default instead of throwing when empty — handy in assertions.
Single vs First: Single asserts exactly one match (throws otherwise) — use it in tests to prove uniqueness; First just takes the first of possibly many.

07Delegates, Events, Lambdas & Generics

Delegates are type-safe function pointers. The built-ins cover most needs: Func<...,TResult> (returns a value), Action<...> (void), Predicate<T> (returns bool). Lambdas (x => x * 2) are inline delegates and the backbone of LINQ.

Func<int, int> square = x => x * x;
Action<string> log = msg => Console.WriteLine(msg);
Func<User, bool> isAdult = u => u.Age >= 18;

square(5);                 // 25
people.Where(isAdult);     // delegates compose with LINQ

Events are a publish/subscribe wrapper over delegates (event EventHandler Clicked;). Generics give type safety and reuse with constraints (where T : class, new()).

public static T[] Repeat<T>(T value, int n) where T : struct {
    var arr = new T[n];
    Array.Fill(arr, value);
    return arr;
}
Pattern matching (C# 8+)

Modern switch expressions and type/property patterns make branching concise — common in parsing API responses.

string Classify(object o) => o switch {
    int n when n < 0   => "negative",
    int                => "non-negative int",
    string { Length: 0 } => "empty string",
    null               => "null",
    _                  => "other"
};

08Collections

TypeNotes
List<T>Resizable array; the workhorse; O(1) index, O(n) mid-insert.
Dictionary<K,V>Hash map; O(1) average lookup; TryGetValue to avoid exceptions.
HashSet<T>Unique elements; set operations (UnionWith, IntersectWith).
Queue / StackFIFO / LIFO.
IEnumerable<T>Lazy sequence; the base abstraction for LINQ.
ConcurrentDictionaryThread-safe map for parallel test state.

Used most in practice: List<T> for ordered items and Dictionary<K,V> for key lookups cover the large majority of test code.

var names = new List<string> { "Ada", "Grace" };
names.Add("Linus");
bool has = names.Contains("Ada");

var ages = new Dictionary<string, int> { ["Ada"] = 36 };
if (ages.TryGetValue("Ada", out var age)) { /* age = 36, no exception */ }

IEnumerable<T> = lazy, forward-only and the base for LINQ. Return the narrowest interface that fits (e.g. IReadOnlyList<T>).

09Exceptions & using

All exceptions derive from System.Exception. Unlike Java, C# has no checked exceptions — nothing forces you to declare or catch them.

  • try — the code that might fail.
  • catch — handle a specific exception type (catch the narrowest first).
  • finally — always runs (cleanup), whether or not an exception was thrown.
  • throw; rethrows and keeps the original stack trace; throw ex; resets it — prefer throw;.
try {
    var data = await _client.GetStringAsync(url);
    Process(data);
} catch (HttpRequestException ex) {
    _log.Error(ex, "request failed");
    throw;                       // rethrow, preserve the stack trace
} finally {
    _stopwatch.Stop();           // always runs
}

using / IDisposable gives deterministic cleanup of resources (HTTP clients, browser contexts, files) — Dispose() is called automatically at scope exit, even on exception. await using + IAsyncDisposable handles async cleanup.

await using var context = await browser.NewContextAsync();
var page = await context.NewPageAsync();
// context disposed automatically when the scope ends
GC vs IDisposable: the garbage collector frees managed memory on its own schedule; using/IDisposable is for things it won't release promptly (sockets, files, handles). Reuse one HttpClient rather than one per request, to avoid socket exhaustion.

10Test Stack (Your Stack)

  • NUnit[Test], [SetUp]/[OneTimeSetUp], [TestCase]/[TestCaseSource] for data-driven tests, rich Assert.That constraint model.
  • xUnit[Fact]/[Theory], constructor + IDisposable for setup/teardown, parallel by default.
  • Playwright for .NET — async API; PageTest/ContextTest base classes wire fixtures and a fresh context per test.
  • RestSharp / HttpClient for API tests; Moq for mocking; FluentAssertions for readable asserts.
[TestCase(2, 4)]
[TestCase(3, 9)]
public void Squares(int input, int expected)
    => Assert.That(input * input, Is.EqualTo(expected));

// Moq
var repo = new Mock<IUserRepo>();
repo.Setup(r => r.Find(1)).Returns(new User(1, "Ada"));
Assert.That(new UserService(repo.Object).NameOf(1), Is.EqualTo("Ada"));
repo.Verify(r => r.Find(1), Times.Once);
NUnit vs xUnit: NUnit has more built-in attributes and a fluent constraint model; xUnit is leaner, opinionated, parallel-first, and uses the constructor + IDisposable for setup/teardown instead of attributes.

11Rapid-Fire Q&A

Reveal each answer to self-check, then test yourself with the quiz.

Value vs reference type?

Value types copy the data on assignment (int, struct); reference types copy a reference to a shared heap object (class, string).

public / private / protected / internal?

public = anywhere; private = same class (the default); protected = class + subclasses; internal = same assembly.

Abstract class vs interface?

Abstract class = shared code/state, extend ONE ("is-a"); interface = pure contract, implement MANY ("can-do"). Interfaces can have default methods since C# 8.

Why is string immutable, and what does it cost?

It can't change after creation — so it's thread-safe and hashable; but concatenation makes new objects, so use StringBuilder in loops.

class vs struct vs record?

class = mutable reference w/ identity equality; struct = value type; record = reference type with value equality and with copies, ideal for DTOs.

const vs readonly?

const is a compile-time constant baked into callers; readonly is set once at runtime, in the constructor.

What does async/await do?

await suspends the method and returns the thread to the pool until the awaited Task completes — concurrency without blocking threads.

Why avoid async void?

It can't be awaited and its exceptions escape the caller; return Task except for event handlers.

What is deferred execution in LINQ?

A LINQ query runs when it's enumerated (ToList/foreach), not when defined — so the source can change in between.

throw vs throw ex?

throw; rethrows and keeps the original stack trace; throw ex; resets it — prefer throw;.

Purpose of using / IDisposable?

Deterministic release of unmanaged resources — Dispose() runs at scope exit instead of waiting for the GC.

NUnit vs xUnit setup?

NUnit uses [SetUp] attributes; xUnit uses the test class constructor + IDisposable, and runs in parallel by default.