Free Handbook · Every example compiled & verified

Interview Questions

60 C# and .NET interview questions with model answers, from value types to async, EF Core and DI lifetimes, plus a coding round and take-home checklist.

0 / 142 lessons🔥 0 day streak
ShareXLinkedIn

Module 15 · what you'll be able to do

  • Answer the 25 junior C# questions that decide first-round screens, precisely and with examples
  • Explain how records, generics, LINQ, async/await, the GC and Span work inside, at the depth a mid-level round expects
  • Reason through senior questions on ASP.NET Core, DI lifetimes, EF Core performance, concurrency, API design and .NET Framework migration
  • Run a live coding round with a repeatable script, and hand in a take-home that reviewers approve
01

How to use this module

C# interviews follow a stable pattern: a screen on the type system, OOP and collections; a technical round on LINQ, async/await, generics and memory; and, for experienced roles, a design conversation that assumes ASP.NET Core, Entity Framework Core, a relational database and a service running in production. The 60 questions below are grouped the same way: 25 junior, 25 mid-level, 10 senior.

  • Say the answer out loud before opening it. Recognising an answer when you read it is not the same as producing it under pressure.
  • Read the "what they are really testing" line. It tells you what the interviewer will follow up on, which is where most candidates lose points.
  • Back every claim with code you have run. The modules these draw on, such as Classes and Objects, LINQ and Async and Await, have examples you can paste into your editor.
Know which .NET they run
Some teams are on .NET 8 (LTS), new work targets .NET 10 (LTS), and many companies still maintain .NET Framework 4.8 applications. Say which version a feature arrived in ("records, C# 9"; "the field keyword, C# 14"). It shows you follow the platform, and it avoids arguments about whether something exists in their codebase.
02

Junior: types, strings and the runtime (10 questions)

Asked in nearly every first-round C# screen, often in the first ten minutes. They are short questions with precise answers, and a vague answer here ends the interview early. Background: Module 01, Module 03 and ref, out and in.

JuniorWhat are .NET, the CLR, IL and the JIT, and how does a C# program run?

The C# compiler (Roslyn) does not produce machine code. It compiles to IL (intermediate language) inside an assembly (.dll). At run time the CLR (Common Language Runtime) loads the assembly, and its JIT compiler turns each method into native code the first time it is called; tiered compilation later re-compiles hot methods with more optimisation. The CLR also provides the garbage collector, type safety and exception handling. .NET is the whole platform: runtime, base class library and SDK (dotnet build, dotnet test). Native AOT can skip the JIT entirely and publish a single native executable.

What they are really testing: Whether they know C# is compiled twice (to IL, then to native code) and can name what the runtime does for them.

JuniorWhat is the difference between a value type and a reference type?

A value type (int, double, bool, DateTime, any struct or enum) holds its data directly; assigning it copies the data. A reference type (class, string, arrays, delegates, record) holds a reference to an object on the heap; assigning it copies the reference, so both variables see the same object.

var p1 = new PointStruct { X = 1 };
var p2 = p1; p2.X = 9;     // p1.X is still 1

var c1 = new PointClass { X = 1 };
var c2 = c1; c2.X = 9;     // c1.X is now 9
"Value types live on the stack" is only roughly true: a value-type field of a class lives inside that object on the heap.

What they are really testing: The copy semantics, and whether they repeat the stack/heap simplification as if it were the rule.

JuniorWhat are boxing and unboxing, and why should you care?

Boxing wraps a value type in a heap object so it can be treated as object or an interface: object o = 42;. Unboxing copies it back out with a cast: int n = (int)o;, which throws InvalidCastException if the box holds a different type (even long). Each box is an allocation, so boxing in a hot loop creates garbage. It used to happen constantly with ArrayList; generics (List<int>) removed most of it. It still sneaks in via object parameters, non-generic interfaces and calling an interface method on a struct.

What they are really testing: That they know generics exist largely to avoid boxing, and that unboxing needs the exact type.

JuniorWhat is the difference between == and Equals?

For reference types, == compares references by default (are these the same object?), and Equals does too unless the class overrides it. Types that represent values override both: string compares characters, and a record compares every property. For value types, Equals compares the contents; == only exists if the struct defines the operator. The trap: == is resolved at compile time from the declared type, so two equal strings held in object variables compare by reference with == but by value with Equals.

What they are really testing: Static (operator) versus virtual (Equals) dispatch; the object-typed string case is the follow-up.

JuniorWhat is the difference between const, readonly and static readonly?

const is a compile-time constant (numbers, strings, enums) and the value is copied into every assembly that uses it, so changing a public const in a library does nothing to callers until they recompile. readonly is an instance field that can only be assigned in its declaration or a constructor; each object can have a different value. static readonly is one value per type, computed at run time (for example static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30)). Use const for true constants such as Math.PI; use static readonly for anything that might change between versions.

What they are really testing: The versioning trap with public const values across assemblies.

JuniorWhat do int? and string? mean? Are they the same feature?

No. int? is Nullable<int>, a real struct with HasValue and Value; it lets a value type represent "no value" at run time. string? is a nullable reference type annotation: string could always hold null, and the annotation only tells the compiler's flow analysis that null is expected here. With nullable enabled (the default in new projects), dereferencing a string? without a check gives warning CS8602, but nothing changes at run time. The null-conditional ?. and null-coalescing ?? / ??= operators work with both.

What they are really testing: Whether they understand that nullable reference types are compile-time only, which is the source of most confusion.

JuniorWhat is the difference between ref, out and in parameters?

All three pass a variable by reference instead of copying it. ref: the variable must be assigned before the call, and the method may read and change it. out: the method must assign it before returning, and the caller need not initialise it; the pattern behind int.TryParse(s, out int n). in: read-only by reference, used to pass a large struct without copying it. Without any of these, C# passes by value: a method can mutate the object a reference points to, but reassigning the parameter does not affect the caller.

What they are really testing: The TryParse pattern and the precise meaning of "passed by value" for reference types.

JuniorDoes var make C# dynamically typed?

No. var is type inference: the compiler works out the static type from the right-hand side, and the variable has that type forever. var x = 5; x = "five"; is a compile error (CS0029). var needs an initialiser, and cannot be used for fields. The dynamically typed feature is dynamic, which defers member lookup to run time and is rarely the right choice. A common team rule: use var when the type is obvious from the right-hand side (var users = new List<User>()).

What they are really testing: That they do not confuse var with dynamic, and have a sensible style rule.

JuniorWhy is string immutable, and when do you use StringBuilder?

Once created, a string's characters never change; ToUpper(), Replace() and + all return new strings. That makes strings safe to share between threads, safe as dictionary keys (the hash never changes) and cheap to pass around. The cost: building a string in a loop with += allocates a new string every iteration, O(n²) characters copied in total. StringBuilder is a growable buffer; Append in the loop, then one ToString(). For a few pieces, + or interpolation is fine, and string.Join or string.Concat is best when you already have a collection.

What they are really testing: The loop-concatenation cost, and not using StringBuilder everywhere out of habit.

JuniorHow should you compare strings in C#?

== and string.Equals(a, b) compare characters exactly (ordinal, case-sensitive). For case-insensitive matching use string.Equals(a, b, StringComparison.OrdinalIgnoreCase), not a.ToLower() == b.ToLower(), which allocates two strings and is culture-sensitive (the Turkish "I" problem). Use ordinal comparisons for identifiers, keys, file paths and protocol values; use culture-aware comparisons (StringComparison.CurrentCulture) only for sorting text shown to a user. Dictionaries take a comparer: new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase).

What they are really testing: Awareness that culture affects string operations, and knowing StringComparison exists.

03

Junior: object-oriented C# (8 questions)

Interviewers probe whether you use classes and interfaces as design tools or only as places to put methods. Examples win over definitions. Background: Module 05 and Module 06.

JuniorWhat are the four pillars of object-oriented programming, with a C# example of each?

Encapsulation: a BankAccount exposes Balance { get; private set; }, so the balance changes only through Deposit. Inheritance: class SavingsAccount : BankAccount reuses and extends behaviour. Polymorphism: a List<Shape> calls Area() and each subclass's override runs. Abstraction: code depends on IPaymentGateway, not on how a specific provider talks HTTP.

What they are really testing: Not the definitions but the examples. Candidates who can only recite the words have not used them.

JuniorWhat is the difference between overloading and overriding?

Overloading: same method name, different parameter lists, chosen by the compiler at compile time from the argument types (Console.WriteLine(int) vs Console.WriteLine(string)). Overriding: a derived class replaces a base method marked virtual (or abstract) using override; the runtime picks the body from the object's actual type (virtual dispatch). In C# a method is not virtual unless you say so, unlike Java.

What they are really testing: Compile-time versus run-time selection, and that C# methods are non-virtual by default.

JuniorWhat is the difference between override and new on a method?

override replaces a virtual method, so the derived body runs even through a base-class reference. new hides the base method: which one runs depends on the declared type of the variable.

Base b = new Derived();
b.Speak();   // override: Derived.Speak   new: Base.Speak
Declaring a method with the same name as a base method without either keyword compiles with warning CS0108 and behaves like new. Hiding is almost always a bug; it exists for versioning, when a base class later adds a member you already had.

What they are really testing: Whether they have seen the "why is my override not called" bug and know how to read CS0108.

JuniorAbstract class or interface: how do you choose?

An interface describes a capability (IComparable<T>, IDisposable); a class can implement many. Since C# 8 interfaces can have default method bodies, and since C# 11 static abstract members, but no instance fields. An abstract class can hold fields, constructors and shared implementation, and a class can inherit only one. Default to an interface, especially for anything you will inject or mock; use an abstract class when subclasses genuinely share state or a template-method skeleton.

What they are really testing: A current answer (default interface methods exist) and a rule, not just a list of differences.

JuniorExplain C#'s access modifiers.

public: everyone. private: the containing type only (the default for members). protected: the type and its derived types. internal: anything in the same assembly (the default for top-level types). protected internal: same assembly or derived types anywhere. private protected: derived types and only within the same assembly. Use the narrowest level that works; internal plus [InternalsVisibleTo] for a test project is a common pattern.

What they are really testing: That they know internal is about assemblies, and are not confused by the two combined modifiers.

JuniorWhat is the difference between a field and a property?

A field is a variable stored in the object. A property is a pair of methods (get/set) that looks like a field to callers. Public data should be exposed through properties so you can add validation, change the storage or make it read-only later without breaking callers. Auto-properties (public string Name { get; set; }) generate the backing field; init allows setting only during object initialisation; required forces callers to set it. C# 14 adds the field keyword to reach the generated backing field inside an accessor.

What they are really testing: Binary compatibility (changing a field to a property breaks callers) and familiarity with init and required.

JuniorWhat does static mean on a member and on a class?

A static member belongs to the type, not to any instance: one copy of a static field is shared by everything, and a static method has no this. A static class cannot be instantiated and may contain only static members; Math and extension-method holders are static classes. Use static for pure helpers and constants; avoid static mutable state, which is shared across threads and requests and makes code hard to test.

What they are really testing: Whether they connect static mutable state to thread-safety and testability problems.

JuniorWhat does sealed do, and why would you use it?

A sealed class cannot be inherited; sealed override stops further overriding of one method. Use it for classes not designed for inheritance: it documents intent, prevents fragile subclassing, and lets the JIT call methods directly instead of through the virtual table. string is sealed. Many teams seal classes by default and unseal only when a real need appears.

What they are really testing: Design intent and a (small) performance benefit; knowing it is a deliberate choice.

04

Junior: collections, exceptions and LINQ basics (7 questions)

These separate someone who has written C# programs from someone who has read about C#: choosing the right collection and handling failure cleanly are daily work. Background: Module 08 and Module 07.

JuniorArray or List<T>?

An array (int[]) has a fixed length set at creation. List<T> wraps an array that grows automatically (doubling its capacity), so Add is amortised O(1). Both index in O(1). Use List<T> when the size changes; use an array for fixed-size data, performance-sensitive buffers, or when an API asks for one. Expose IReadOnlyList<T> from public methods so callers cannot modify your internal list.

What they are really testing: Knowing List is an array underneath, and good API hygiene with read-only interfaces.

JuniorWhen do you use a List, a Dictionary and a HashSet?

List<T>: ordered by position, duplicates allowed. Dictionary<TKey, TValue>: look up a value by key in O(1) on average (user by id). HashSet<T>: uniqueness and fast "have I seen this?" checks. Calling list.Contains(x) inside a loop is O(n) each time, O(n²) overall; a HashSet makes it O(n). Use TryGetValue rather than ContainsKey followed by the indexer, which looks the key up twice.

What they are really testing: Picking a collection by the question the code asks, and spotting the hidden O(n²).

JuniorWhat is IEnumerable<T>, and what does foreach need?

IEnumerable<T> means "something you can iterate once from the start": it has one method, GetEnumerator(). ICollection<T> adds Count, Add and Remove; IList<T> adds indexing. foreach works on anything with a suitable GetEnumerator method (it does not even require the interface). Accept the most general interface you need as a parameter (IEnumerable<T>), and return the most specific useful one.

What they are really testing: The collection interface hierarchy and the "accept general, return specific" guideline.

JuniorHow do try, catch and finally work?

Code in try runs; if it throws, the runtime looks for the first catch whose type matches (most specific first, or you get error CS0160). finally runs whether or not an exception happened, even after a return, and is for cleanup. Catch only exceptions you can handle; catching Exception and doing nothing hides bugs. finally does not run if the process is killed or on a StackOverflowException, which .NET cannot catch at all.

What they are really testing: Ordering of catch blocks, the purpose of finally, and restraint in catching.

JuniorWhat is the difference between throw; and throw ex;?

throw; inside a catch block rethrows the same exception with its original stack trace. throw ex; rethrows it but resets the stack trace to the current line, so the log no longer shows where the problem actually happened (analysers flag it as CA2200). If you want to add context, wrap it: throw new OrderException("Could not save order 42", ex); keeps the original as InnerException.

What they are really testing: A classic code-review catch. Losing the stack trace makes production bugs much harder to find.

JuniorWhat does a using statement do?

It guarantees Dispose() is called on an IDisposable object when the block ends, even if an exception is thrown; the compiler turns it into try/finally. The declaration form using var file = File.OpenRead(path); disposes at the end of the enclosing scope. Use it for anything holding an unmanaged or scarce resource: files, streams, database connections, HttpResponseMessage. await using is the asynchronous version for IAsyncDisposable.

What they are really testing: That they know disposal is deterministic cleanup, separate from garbage collection.

JuniorWhat is LINQ? Give an example.

LINQ (Language Integrated Query) is a set of extension methods on IEnumerable<T> (and IQueryable<T>) for filtering, projecting, sorting, grouping and aggregating data.

var topNames = orders
    .Where(o => o.Total > 100)
    .OrderByDescending(o => o.Total)
    .Select(o => o.Customer)
    .Take(3)
    .ToList();
Most operators are deferred: nothing runs until you enumerate or call ToList, Count or First. The same query syntax, against Entity Framework, is translated to SQL.

What they are really testing: Fluency, plus a first hint of deferred execution (which the mid-level questions dig into).

05

Mid-level: records, structs and the type system (5 questions)

For roles with two to five years of experience. Modern C# gives you several ways to model data; the interviewer wants to know that you choose between them on purpose.

Mid-levelCompare class, struct, record and record struct.

class: reference type, reference equality by default. struct: value type, copied on assignment, field-by-field equality via ValueType.Equals (which can fall back to slow reflection, so override it). record (a record class): reference type with compiler-generated value equality, ToString, deconstruction and non-destructive mutation with with; positional properties are init-only. record struct: a value type with the same generated members, whose positional properties are mutable unless you write readonly record struct. Use records for DTOs, messages and value objects; classes for entities with identity and behaviour; structs for small, immutable values.

What they are really testing: Whether they know records are about equality semantics, and the mutability difference in record struct.

Mid-levelWhen should you choose a struct over a class?

When the type is small (a rule of thumb is 16 bytes or less), logically a single value (Point, Money, DateOnly), immutable, and created in large numbers where avoiding heap allocations matters. Make it readonly struct so the compiler enforces immutability and avoids defensive copies. Avoid structs that are large (every assignment copies them), mutable (a modified copy is a classic bug), or frequently boxed (stored as object or a non-generic interface).

What they are really testing: Real criteria, and the mutable-struct copy bug.

Mid-levelWhat is the contract between Equals and GetHashCode?

If two objects are Equals, they must return the same GetHashCode. The reverse need not hold. Dictionary and HashSet use the hash to pick a bucket and Equals to confirm, so overriding only Equals makes equal keys land in different buckets and lookups fail. Hash only on fields that never change while the object is a key, and build it with HashCode.Combine(X, Y). Implement IEquatable<T> to avoid boxing for structs. Records generate all of this correctly.

What they are really testing: The dictionary failure mode, and the danger of mutating a key after inserting it.

Mid-levelHow has pattern matching changed everyday C#?

Type patterns remove cast-and-check pairs: if (shape is Circle c). Switch expressions with property, relational and logical patterns replace long if/else chains:

decimal Fee(Order o) => o switch
{
    { Total: >= 1000 } => 0m,
    { Country: "IN", Express: true } => 99m,
    { Express: true } => 149m,
    _ => 49m,
};
List patterns ([first, .., last]) match sequences. The compiler warns (CS8509) when a switch expression is not exhaustive, which is valuable over a closed set of types or enum values.

What they are really testing: Modern-C# fluency; they want to see the exhaustiveness warning used as a safety net.

Mid-levelWhat do nullable reference types actually guarantee?

Nothing at run time. With <Nullable>enable</Nullable> the compiler tracks whether each reference may be null and warns on dereferences (CS8602), assignments of maybe-null values to non-nullable variables (CS8600/CS8601) and uninitialised non-nullable properties (CS8618). Values can still be null through reflection, deserialisation, default, older libraries, or the ! (null-forgiving) operator, which silences a warning without checking anything. Good practice: treat the warnings as errors, validate at the boundaries (API input, deserialisation), and use ! only where you can prove the value is set.

What they are really testing: That they understand it is static analysis, and do not sprinkle ! to make warnings disappear.

06

Mid-level: generics, collections and LINQ (6 questions)

How the tools work inside, because that is what lets you predict their failure modes. Background: generic constraints and deferred execution.

Mid-levelWhat generic constraints does C# offer, and why use them?

A constraint tells the compiler what a type argument can do, so the generic code can use it: where T : class (reference type), struct (non-nullable value type), notnull, new() (has a parameterless constructor), SomeBase or ISomeInterface (can call its members), unmanaged, and where T : INumber<T> for generic maths (static abstract interface members). Without a constraint you can only use object members. Example: T Max<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) >= 0 ? a : b;

What they are really testing: Understanding constraints as the contract that makes a generic method body compile.

Mid-levelWhat are covariance and contravariance in C# generics?

Covariance (out T) lets a more specific type argument be used where a less specific one is expected: an IEnumerable<string> can be passed as IEnumerable<object>, because the interface only hands T out. Contravariance (in T) goes the other way: an Action<object> can be used as an Action<string>, because it only takes T in. Variance applies only to interfaces and delegates, and only with reference types. List<string> is not a List<object>: it accepts T in and out, so allowing it would let you add an int to a list of strings.

What they are really testing: Whether they can explain the List case, which shows they understand why the rule exists.

Mid-levelHow does Dictionary<TKey, TValue> work inside?

It stores entries in an array and keeps an array of buckets. To find a key it calls GetHashCode(), maps the hash to a bucket, then walks that bucket's chain comparing with Equals. When the entry count exceeds the capacity it resizes to a larger prime size and rehashes everything, so Add is amortised O(1). Lookups degrade towards O(n) with a poor hash that sends many keys to one bucket. Enumeration order is not part of the contract, and modifying the dictionary while enumerating it throws InvalidOperationException. It is not thread-safe for concurrent writes; use ConcurrentDictionary.

What they are really testing: The mechanism behind the O(1), and the practical consequences (order, mutation during enumeration, threads).

Mid-levelWhat is deferred execution in LINQ, and what goes wrong with it?

Operators like Where and Select build a pipeline; nothing runs until something enumerates it. Two classic bugs follow. Multiple enumeration: an IEnumerable used twice (a Count(), then a foreach) runs the whole pipeline twice, including any database query or expensive projection. Captured state: the query sees the source and captured variables as they are at enumeration time, not when the query was written. Call ToList() or ToArray() once when you need a snapshot. Operators such as OrderBy and GroupBy must read the whole source before yielding anything.

What they are really testing: Whether they have been bitten by multiple enumeration, the most common LINQ performance bug.

Mid-levelIEnumerable<T> or IQueryable<T>?

IEnumerable<T> runs LINQ in memory, with your lambdas compiled as delegates. IQueryable<T> captures the query as an expression tree that a provider such as Entity Framework Core translates into SQL. The danger is switching too early: db.Orders.AsEnumerable().Where(o => o.Total > 100) downloads the entire table and filters in memory. Keep a query IQueryable until the filtering, paging and projection are done, then materialise. The flip side: only expressions the provider can translate work, so an arbitrary C# method inside Where throws at run time.

What they are really testing: Knowing where the query executes, and the AsEnumerable/ToList-too-early performance bug.

Mid-levelWhat does yield return do?

It turns a method into an iterator: the compiler generates a state machine that implements IEnumerable<T>, runs your code up to each yield return, hands out that value, and pauses until the caller asks for the next one. It gives lazy, streaming sequences (read a huge file line by line, generate an infinite sequence) with O(1) memory. Gotchas: argument validation inside an iterator does not run until the first MoveNext, so do it in a non-iterator wrapper method; and a try/finally inside only runs when the caller finishes or disposes the enumerator (which foreach does).

What they are really testing: Understanding laziness, and the delayed-validation gotcha.

07

Mid-level: async/await and Task (7 questions)

Every ASP.NET Core service is asynchronous from the controller down, so async questions appear in almost every mid-level loop. Background: Module 10, especially the pitfalls lesson.

Mid-levelWhat does await actually do? Does async code create threads?

An async method is compiled into a state machine. At an await on an incomplete task, the method returns to its caller and registers the rest of itself as a continuation; when the task completes, the continuation runs (on the captured SynchronizationContext if there is one, otherwise on the thread pool). No thread is created or blocked while waiting for I/O: that is the point. async does not make CPU-bound work run in the background; for that, use Task.Run (in apps, not in library code).

What they are really testing: The single most important async question. "It runs on another thread" is the wrong answer.

Mid-levelWhy is async void dangerous?

An async void method cannot be awaited, so the caller cannot know when it finishes, and an exception thrown in it cannot be caught by the caller: it is raised on the synchronization context and, with none present, crashes the process. Tests cannot await it either. The only legitimate use is event handlers, whose signature requires void; wrap their body in try/catch. Everywhere else return Task. Watch for lambdas: passing an async lambda to a parameter of type Action silently creates an async void.

What they are really testing: The crash behaviour and the hidden async-void lambda.

Mid-levelHow can .Result or .Wait() deadlock, and what is ConfigureAwait(false) for?

In an environment with a single-threaded SynchronizationContext (WinForms, WPF, classic ASP.NET), calling .Result blocks that thread; the awaited method's continuation needs to resume on the same thread, which is blocked; neither can proceed. ASP.NET Core has no synchronization context so it does not deadlock, but blocking still wastes thread-pool threads and causes starvation under load. The fix is "async all the way": await instead of blocking. ConfigureAwait(false) tells an await not to resume on the captured context; library code uses it so it works safely whatever the caller's context. Application code in ASP.NET Core does not need it.

What they are really testing: The mechanism of the deadlock, and knowing it differs between ASP.NET and ASP.NET Core.

Mid-levelTask or ValueTask?

Return Task by default. ValueTask<T> is a struct that avoids allocating a Task when the result is often available synchronously (a cache hit, a buffered read), which matters only on hot paths. It has rules: await it exactly once, do not await it concurrently, and do not call .Result before it has completed. If you need to do any of those, call .AsTask(). A completed Task can also be cached and reused, which is often enough.

What they are really testing: Knowing ValueTask is an optimisation with restrictions, not a better Task.

Mid-levelHow do you cancel async work, and what happens to exceptions in Task.WhenAll?

Cancellation is cooperative: pass a CancellationToken down every async call (ASP.NET Core provides HttpContext.RequestAborted), check it with token.ThrowIfCancellationRequested() in loops, and create timeouts with new CancellationTokenSource(TimeSpan.FromSeconds(5)). Cancelled work throws OperationCanceledException, which should be treated as normal, not logged as an error. await Task.WhenAll(tasks) waits for every task; if several fail, the await rethrows only the first exception. To see all of them, keep the WhenAll task and read its Exception.InnerExceptions.

What they are really testing: Cooperative cancellation plumbing and the "only the first exception" detail.

Mid-levelYou need to call an API for 500 items. Compare a foreach with await, Task.WhenAll and Parallel.ForEachAsync.

A foreach that awaits each call runs them one at a time: simple, gentle on the API, and 500 times the latency of one call. Task.WhenAll(items.Select(CallAsync)) starts all 500 at once: fastest on paper, but it can exhaust connections, trip the API's rate limit and hold every response in memory. Parallel.ForEachAsync(items, new ParallelOptions { MaxDegreeOfParallelism = 10 }, ...) (or a SemaphoreSlim around WhenAll) gives bounded concurrency, which is usually the right answer. For CPU-bound work the tools are different: Parallel.For/ForEach or PLINQ, not async.

What they are really testing: Whether they reach for bounded concurrency instead of unleashing 500 requests, and separate I/O-bound from CPU-bound work.

Mid-levelWhat is IAsyncEnumerable<T> and when would you use it?

It is the async version of IEnumerable<T>: an async method that uses yield return and returns IAsyncEnumerable<T> produces items one at a time, awaiting between them, and the caller consumes it with await foreach. Use it to stream results that arrive over time, such as pages from a paginated API, rows from a database reader or messages from a queue, without buffering the whole set in a List first. EF Core exposes queries this way with AsAsyncEnumerable(), and ASP.NET Core can stream it as a JSON array. Pass cancellation with [EnumeratorCancellation] on the token parameter, or .WithCancellation(token) at the call site.

What they are really testing: Knowing the streaming alternative to returning Task>, and how cancellation flows into it.

08

Mid-level: exceptions, delegates, events and memory (7 questions)

You do not need to tune the garbage collector to get a mid-level job, but you need a working model of where objects live, how they die, and how delegates and events keep them alive. Background: using and IDisposable and delegates and lambdas.

Mid-levelExplain the dispose pattern. When do you need a finalizer?

IDisposable.Dispose() releases resources deterministically, called by using. A class that merely holds other disposables implements Dispose to dispose them; it needs no finalizer. A finalizer (~MyType()) is only for a class that directly owns an unmanaged handle, as a safety net if Dispose is never called; it delays collection and runs on the finalizer thread. In modern code, wrap the handle in a SafeHandle instead and you almost never write a finalizer. Types with async cleanup implement IAsyncDisposable.

What they are really testing: Knowing finalizers are rare and SafeHandle exists; over-engineering the full pattern everywhere is a yellow flag.

Mid-levelWhen should you throw an exception, and when should you use a Try method or a result type?

Throw for exceptional conditions the caller cannot reasonably expect: a broken invariant, a missing config file, a network failure. For outcomes that are expected in normal flow (a user types "abc" into an age field, a cache miss) use the Try pattern (bool TryParse(string s, out int value)) or return a result object. Exceptions are slow when thrown (stack capture) and turn control flow into invisible jumps. Use exception filters (catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)) to catch only the case you handle, and create custom exception types only when callers need to catch them specifically.

What they are really testing: Judgement: exceptions for the exceptional, and fluency with filters.

Mid-levelWhat are delegates, Func, Action and events, and how do events differ from delegate fields?

A delegate is a type-safe reference to a method (or several: delegates are multicast). Func<T, TResult> and Action<T> are the built-in generic delegates, so you rarely declare your own; lambdas convert to them. An event is a delegate field wrapped so that outside code can only += and -= handlers; only the declaring class can invoke it or replace the whole list. A public delegate field would let any caller wipe out every subscriber with = or raise the event itself. Raise events safely with Changed?.Invoke(this, args).

What they are really testing: The encapsulation that the event keyword adds, which is exactly what juniors miss.

Mid-levelWhat does a lambda capture, and what is the classic closure bug?

A lambda captures variables, not values: the compiler hoists captured locals into a hidden class shared by the method and the lambda, so later changes are visible to it (and the variable lives as long as the lambda does, a possible leak). Since C# 5, a foreach variable is fresh on every iteration, but a for loop counter is one variable:

var actions = new List<Action>();
for (int i = 0; i < 3; i++) actions.Add(() => Console.Write(i));
foreach (var a in actions) a();   // prints 333
Copy to a local inside the loop (int copy = i;) to capture each value. Mark lambdas static to make the compiler reject accidental captures.

What they are really testing: Understanding closures as shared variables; the for-loop output is the follow-up question.

Mid-levelHow can events cause a memory leak?

Subscribing (publisher.Changed += subscriber.OnChanged) stores a reference to the subscriber inside the publisher's delegate list. If the publisher lives longer (a singleton service, a static event, a main window), the subscriber can never be garbage-collected, even after everything else has forgotten it. Fixes: unsubscribe with -= (typically in Dispose), keep publisher and subscriber lifetimes aligned, or use a weak-event pattern. A memory profiler showing "retained by an EventHandler" is the tell-tale sign.

What they are really testing: Real-world debugging experience with long-lived publishers.

Mid-levelHow does the .NET garbage collector work?

It is a generational, compacting, tracing collector. New objects go into generation 0; objects that survive a collection are promoted to gen 1, then gen 2. Most objects die young, so frequent cheap gen 0 collections reclaim most garbage, and expensive full (gen 2) collections are rare. Objects of 85,000 bytes or more go to the Large Object Heap, collected with gen 2 and not compacted by default, so allocating large temporary arrays repeatedly hurts; use ArrayPool<T>.Shared. Server GC (the ASP.NET Core default) uses a heap per core for throughput; background GC does most gen 2 work concurrently with your code. GC.Collect() in application code is almost always wrong.

What they are really testing: A working model (generations, LOH), and that they would measure before tuning.

Mid-levelWhat are Span<T> and Memory<T>, and why do they exist?

Span<T> is a view over a contiguous region of memory (an array, a slice of a string, stackalloc memory or native memory) without copying it. "2024-06-01".AsSpan(0, 4) gives the year with no new string, and int.Parse accepts spans. It is a ref struct, so it can only live on the stack: it cannot be a field of a class, be boxed, or be held across an await. Memory<T> is the heap-storable counterpart for async code; call .Span on it when you need to work with the data. Together they let parsers and serializers such as System.Text.Json run with almost no allocations.

What they are really testing: Allocation-free slicing and the ref struct restrictions; mid-level candidates should know why it cannot cross an await.

09

Senior: ASP.NET Core, EF Core and production (10 questions)

Senior C# interviews assume the ecosystem around the language: ASP.NET Core, dependency injection, EF Core, a relational database and a system running in production, sometimes next to a legacy .NET Framework estate. There is no single right answer; the model answers show the shape of a strong one: what you would ask first, what you would measure, and which trade-off you would accept.

SeniorExplain the DI lifetimes in ASP.NET Core and the captive-dependency problem.

Transient: a new instance every time it is resolved. Scoped: one instance per scope, which in ASP.NET Core means per HTTP request; DbContext is scoped. Singleton: one instance for the application's lifetime, which must be thread-safe. A captive dependency is a longer-lived service holding a shorter-lived one: a singleton that takes a scoped DbContext in its constructor keeps the first request's context forever, sharing it across threads. In Development the container validates scopes and throws "Cannot consume scoped service from singleton". A singleton that needs scoped work (a background service, for example) injects IServiceScopeFactory and creates a scope per unit of work.

What they are really testing: Whether they have debugged a lifetime bug, and know the IServiceScopeFactory pattern for hosted services.

SeniorWalk through the ASP.NET Core request pipeline. Why does middleware order matter?

Kestrel accepts the connection and builds an HttpContext; the request then flows through middleware in the order they were registered in Program.cs, and the response flows back in reverse. Each middleware can act before and after calling next, or short-circuit. Order is behaviour: exception handling must be first so it wraps everything; UseRouting comes before authentication, and UseAuthentication before UseAuthorization, or authorization sees an anonymous user; CORS must run before endpoints; static files early so they skip auth. Endpoint filters and MVC filters then run around the handler itself. Cross-cutting concerns (correlation IDs, request logging, rate limiting) belong in middleware, not copied into controllers.

What they are really testing: A real mental model of the pipeline; the auth ordering mistake is common in production code.

SeniorHow would you structure a large ASP.NET Core service?

Start from the change you expect: code that changes together should live together. Two common shapes: layered / clean architecture (Domain, Application, Infrastructure, API projects, with dependencies pointing inward) gives clear boundaries but spreads one feature across four projects; vertical slices (a folder per feature with its endpoint, handler, validation and data access) keeps features cohesive and suits CRUD-heavy services. Either way: keep the domain free of framework types, use the options pattern for configuration, validate at the edge, return ProblemDetails for errors, and put integration tests on the HTTP surface with WebApplicationFactory. Avoid generic repository wrappers over EF Core that hide its capabilities, and avoid splitting into microservices before a module boundary has proved itself inside one deployable.

What they are really testing: Trade-off thinking rather than a memorised template, and restraint about microservices.

SeniorAn endpoint that lists orders with their customers got slow as data grew. How do you find and fix an EF Core performance problem?

Measure first: turn on EF Core command logging or look at the traces (OpenTelemetry) to see the SQL and how many round-trips one request makes. Typical culprits: the N+1 query (lazy loading or a loop that queries per order), fixed with Include or, better, a Select projection into a DTO that fetches only needed columns; cartesian explosion from several Includes of collections, fixed with AsSplitQuery(); tracking overhead on read-only queries, fixed with AsNoTracking(); client evaluation or early ToList() that pulls whole tables; missing indexes, confirmed with the execution plan; and unbounded result sets, fixed with keyset pagination. For bulk changes use ExecuteUpdate/ExecuteDelete instead of loading entities.

What they are really testing: A measure-then-fix habit and knowledge of EF Core's specific tools beyond "add an index".

SeniorWhat should you know about DbContext lifetime and thread safety?

A DbContext is a unit of work with a change tracker; it is not thread-safe, and starting a second operation on it before the first finishes throws "A second operation was started on this context instance". It is registered as scoped, one per request. Do not share one across parallel tasks (Task.WhenAll over queries on the same context); use IDbContextFactory<T> to create one per task. Keep contexts short-lived: a long-lived context accumulates tracked entities and gets slower. AddDbContextPool reuses instances to cut allocation cost in high-throughput services. Transactions: one SaveChanges is already atomic; use an explicit transaction only when several must commit together, and handle concurrency with a row-version token and DbUpdateConcurrencyException.

What they are really testing: The real production errors they will have seen, and the factory pattern for parallel work.

SeniorCompare lock, SemaphoreSlim, Interlocked and concurrent collections.

Interlocked performs single atomic operations (Increment, CompareExchange) with no lock: fastest, for counters and flags. lock gives mutual exclusion over a block of synchronous code; keep it short, lock on a private object (or the System.Threading.Lock type in .NET 9+), and note you cannot await inside it (error CS1996). SemaphoreSlim with WaitAsync is the async-friendly lock, and with a count above one it throttles concurrency (at most 10 outbound calls at once). ConcurrentDictionary, ConcurrentQueue and Channel<T> handle common shared-state and producer/consumer cases without hand-written locking; but GetOrAdd with a factory may run the factory more than once. Across multiple instances of a service none of these help; you need a database constraint, optimistic concurrency or a distributed lock.

What they are really testing: Picking the smallest tool for the job, the await-in-lock rule, and remembering that production runs more than one instance.

SeniorUnder load, an ASP.NET Core service has low CPU but huge latency and requests time out. What do you suspect and how do you confirm it?

Classic thread-pool starvation: code blocks pool threads with sync-over-async (.Result, .Wait(), GetAwaiter().GetResult()) or synchronous I/O, the pool injects new threads only slowly, and queued work waits. Confirm with dotnet-counters monitor (thread-pool queue length rising, thread count climbing steadily) and a dotnet-stack or dotnet-dump showing many threads blocked in Task.Wait or Monitor.Wait. Fix by making the call chain async end to end. Other suspects with the same symptom: connection-pool exhaustion (database or HttpClient), a lock held during I/O, or a downstream dependency without a timeout. Raising ThreadPool.SetMinThreads is a temporary bandage, not a fix.

What they are really testing: Production diagnosis with real .NET tools, and knowing the bandage from the cure.

SeniorHow do you design an HTTP API that other teams will depend on?

Design the contract first (OpenAPI), then implement. Resource-oriented URLs, correct status codes, and consistent RFC 9457 ProblemDetails error bodies (AddProblemDetails()). Version from day one (URL segment or header) and only make additive changes within a version. Make unsafe operations safe to retry with an idempotency key stored with the result. Paginate every list (keyset for large tables), validate input at the edge, and never expose EF entities directly: DTOs decouple the database schema from the contract. Apply authentication, per-client rate limiting (the built-in AddRateLimiter), timeouts and request-size limits. Outbound calls go through IHttpClientFactory with timeouts, retries with jitter and circuit breakers (the resilience handlers), and every request carries a correlation ID through logs and traces.

What they are really testing: Whether they think about consumers, retries and change over time, not just endpoints.

SeniorHow would you migrate a large .NET Framework application to modern .NET?

Inventory first: target frameworks, NuGet packages without .NET support, and Windows-only or removed APIs (System.Web, WCF server, AppDomains, .NET Remoting, BinaryFormatter). Convert projects to SDK-style, and move shared class libraries to .NET Standard 2.0 or multi-target them so both old and new apps can use them during the transition. The .NET Upgrade Assistant and API compatibility analysers find blockers. ASP.NET MVC/Web API cannot be upgraded in place; use the strangler fig approach: put a YARP reverse proxy in front, move routes one by one to a new ASP.NET Core app, and share authentication between them. Replace WCF services with gRPC or REST (CoreWCF for a lift-and-shift), web.config with appsettings.json and the options pattern, and static ConfigurationManager use with injected configuration. Keep both running in production and retire the old app route by route, with tests on the HTTP surface as the safety net.

What they are really testing: An incremental, low-risk plan and concrete knowledge of what does not exist in modern .NET.

SeniorWhat do you put in place before a .NET service goes to production?

Observability: structured logging with ILogger message templates (not string interpolation), OpenTelemetry traces and metrics exported to the team's backend, and correlation IDs across services. Health: liveness and readiness endpoints with AddHealthChecks() that check the database and critical dependencies, used by the load balancer or Kubernetes. Resilience: timeouts on every outbound call, IHttpClientFactory (a new HttpClient per request exhausts sockets; one static client never sees DNS changes), retries only for idempotent calls. Configuration: secrets from a vault or environment, never in appsettings.json; the options pattern with validation on start. Shutdown: honour CancellationTokens so a deploy drains in-flight requests. And a load test against realistic data, with the numbers written down.

What they are really testing: Operational maturity: whether they have been on call for a .NET service.

The senior-answer shape
Clarify the goal and constraints, name two options with their costs, pick one and say what would make you change your mind, then say how you would verify it in production. That structure matters more than any single fact.
10

The coding round, walked through

A live coding round is 30 to 45 minutes on one or two problems in a shared editor, often without IntelliSense. The interviewer grades how you think, communicate and test, not only whether it compiles. Follow the same script every time, the one from Module 14.

  1. 1
    Clarify (2 min)

    Restate the problem. Ask about input size, empty input, duplicates, null, ordering of the output, and what to return when there is no answer. Write the contract as a comment.

  2. 2
    Example (1 min)

    Work one small case by hand. It becomes your first test.

  3. 3
    Brute force out loud (2 min)

    "For every request, count the same client's requests in the next window: O(n²)." Say it and its cost before improving it.

  4. 4
    Pick the pattern (1 min)

    Group with a Dictionary? Sort then slide a window? A PriorityQueue? Name it, and why.

  5. 5
    Code (15 min)

    Talk while you type. Real names, var where the type is obvious, TryGetValue instead of a double lookup, and the edge cases you listed.

  6. 6
    Test (5 min)

    Run your example, then the edges. Finding your own bug scores higher than never having one.

  7. 7
    Complexity and improvements (2 min)

    State time and space, then the better version if there is one.

A typical 30-minute problem solved that way: given a log of (client, second) requests in no particular order, return every client that made more than limit requests within any window-second span, sorted by name. The brute force compares every pair of a client's requests. The version below groups by client with a Dictionary, sorts each client's times, and slides a window over them (Module 14, pattern 2).

C#Program.cs
// Contract: log is unsorted and may be empty; limit >= 1; window >= 1 second.
// A client offends if more than `limit` requests fall within `window` seconds
// (times t and t + window - 1 are in the same span). Result sorted by name.
var log = new List<(string Client, int Second)>
{
    ("ana", 1), ("bo", 2), ("ana", 3), ("ana", 5), ("bo", 30), ("cy", 7),
    ("ana", 70), ("bo", 31), ("bo", 33), ("cy", 50), ("bo", 32),
};
Console.WriteLine(string.Join(", ", Offenders(log, limit: 2, window: 10)));
Console.WriteLine(string.Join(", ", Offenders(log, limit: 3, window: 10)));
Console.WriteLine(Offenders([], limit: 1, window: 1).Count); // empty log

static List<string> Offenders(List<(string Client, int Second)> log, int limit, int window)
{
    var byClient = new Dictionary<string, List<int>>();
    foreach (var (client, second) in log)
    {
        if (!byClient.TryGetValue(client, out var times)) byClient[client] = times = [];
        times.Add(second);
    }

    var result = new List<string>();
    foreach (var (client, times) in byClient)
    {
        times.Sort();                                   // O(k log k) per client
        int left = 0;
        for (int right = 0; right < times.Count; right++)
        {
            while (times[right] - times[left] >= window) left++;
            if (right - left + 1 > limit) { result.Add(client); break; }
        }
    }
    result.Sort(StringComparer.Ordinal);                // Dictionary order is not guaranteed
    return result;
}
Outputcompiled & run with real C#
ana, bo
bo
0

ana makes 3 requests in seconds 1 to 5, so she offends at a limit of 2 but not 3; bo makes 4 in seconds 30 to 33. Overall O(n log n) for the sorts, O(n) for the windows, O(n) extra memory.

Your turn

The interviewer follows up: "the log is now an endless stream, already in time order". Rewrite it to keep a Dictionary<string, Queue<int>> of each client's recent times, dropping times that fall out of the window as each request arrives. What is the memory cost now?

What loses the round

  • Silence for ten minutes, then a wall of code
  • Printing a Dictionary and assuming its order
  • Fighting LINQ syntax instead of writing the plain loop
  • "It should work" without running an example
  • Optimising before the brute force is correct

What wins it

  • Narrating your reasoning, including dead ends
  • A written contract and example before code
  • Testing edge cases: empty log, a limit that no one exceeds, requests exactly window seconds apart
  • Naming the complexity without being asked
  • "I sort each client's times so the window only ever moves forward"
11

Take-home assignment checklist

C# take-homes are usually "build a small Web API" or "process this file and report on it". Reviewers open the README, run dotnet test, read the tests, then the code. Most rejected submissions fail at step two: it does not build on the reviewer's machine.

  • It builds on a clean machine: dotnet build and dotnet test work with only the .NET SDK installed. Pin the SDK in global.json and the target framework in the .csproj.
  • README: what it does, how to build, run and test it in three commands, example requests (curl lines or an .http file), and the decisions you made, including what you deliberately left out.
  • Tests with xUnit (or NUnit): the happy path, empty and invalid input, and the one tricky rule in the spec. For an API, integration tests through WebApplicationFactory; for database code, Testcontainers against a real Postgres or SQL Server beats mocking EF Core.
  • Zero warnings: nullable reference types enabled and <TreatWarningsAsErrors>true</TreatWarningsAsErrors>. Reviewers notice a build that scrolls yellow.
  • Structure: a thin endpoint or controller, logic in plain classes that can be tested without starting the host, dependencies injected through constructors.
  • Modern C#: records for DTOs, async all the way with CancellationTokens, file-scoped namespaces, pattern matching where it reads better, LINQ where it is clearer than a loop and loops where they are not.
  • Errors on purpose: invalid input returns 400 with a ProblemDetails body, not a 500 with a stack trace. Exceptions keep their inner exception.
  • No noise in the repo: a .gitignore for bin/, obj/, .vs/ and *.user (dotnet new gitignore); no connection strings or keys in appsettings.json (use user-secrets or environment variables); code formatted with dotnet format.
  • Optional but noticed: a Dockerfile or docker-compose.yml so the reviewer can run it with the database in one command, and a GitHub Actions workflow running dotnet test.
  • Time-box to what they asked (usually 3 to 4 hours) and say so in the README. Five half-done extras are a red flag; one finished extra is fine. A handful of meaningful commits beats one "final" dump.
The sentence reviewers want to write
"Built first time, tests cover the edge cases, the code reads like the team already wrote it." Aim every decision at that sentence. The next module, Job Ready, turns the same standards into a portfolio.

Frequently asked questions

What C# topics are asked most in interviews?
At junior level: value versus reference types, boxing, == versus Equals, string immutability, OOP and interfaces, choosing collections and exception handling. At mid level: records and structs, generics, LINQ deferred execution, async/await internals, delegates and events, and garbage collection. Senior rounds add ASP.NET Core DI lifetimes, EF Core performance, concurrency and production diagnosis.
Do C# interviews expect ASP.NET Core knowledge?
For backend roles, usually yes. Junior screens focus on the language, but mid and senior rounds commonly ask about dependency injection lifetimes, the middleware pipeline, EF Core queries and async behaviour under load. Unity and desktop roles replace these with engine or UI framework questions.
Which .NET version should I prepare for?
Prepare on a current long-term-support release (.NET 8 or .NET 10) and know which C# features arrived when: records (C# 9), record structs and global usings (C# 10), required members and raw string literals (C# 11), primary constructors and collection expressions (C# 12). Many teams also maintain .NET Framework 4.8 code, so know what changed.

Finish the C# handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.