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.
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;overridereplaces it;sealedstops 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 throughpublicproperties (get/set). - Inheritance — a class extends one base class (
: Base). - Polymorphism —
virtual/overrideso the right method runs for the actual object type. - Abstraction — expose what, hide how, via abstract classes and interfaces.
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 = 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 equality04Strings & 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 resultWhy 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 VALUEstring 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.
asyncmarks a method returningTask/Task<T>/ValueTask;awaitsuspends without blocking the calling thread.- It frees the thread while I/O completes — concurrency without extra threads.
- Return
Task, notvoid(except event handlers);async voidcan't be awaited and swallows exceptions. - Avoid
.Result/.Wait()— they can deadlock; await all the way up. Task.WhenAllruns work concurrently;CancellationTokencooperatively 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)));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/SingleOrDefaultreturn default instead of throwing when empty — handy in assertions.
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 LINQEvents 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;
}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
| Type | Notes |
|---|---|
| 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 / Stack | FIFO / LIFO. |
| IEnumerable<T> | Lazy sequence; the base abstraction for LINQ. |
| ConcurrentDictionary | Thread-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 — preferthrow;.
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 endsusing/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, richAssert.Thatconstraint model. - xUnit —
[Fact]/[Theory], constructor +IDisposablefor setup/teardown, parallel by default. - Playwright for .NET — async API;
PageTest/ContextTestbase 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);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.