TL;DR: C# and .NET interviews in 2026 test memory and the garbage collector, async and threading, LINQ and collections, and the language features added since C# 11. Candidates should know that .NET 10 is the current LTS release with C# 14, and that .NET 8 and .NET 9 both lose support on November 10, 2026.
.NET 8 and .NET 9 both reach end of support on November 10, 2026, so most teams are moving to .NET 10, the long-term support release that shipped on November 11, 2025. That upgrade brings C# 14 with it, and interviewers now ask about both.
For web and API questions, see our ASP.NET Core interview questions.
- 1The latest patch as of September 2026 is .NET 10.0.12, released on September 8, 2026. .NET 10 is supported until November 2028.
- 2.NET 11 RC 1 shipped on the same day with a go-live license. It brings C# 15 and is due in November 2026.
- 3Since .NET 9, DATAS is on by default. It lets Server GC grow and shrink the heap with the app's real memory needs.
- 4The .NET 10 JIT can place small arrays on the stack when they never leave the method, so some allocations vanish without code changes.
C# Language
1. What is the difference between a class, a struct and a record?
A class is a reference type, a struct is a value type, and a record is a class or struct that the compiler gives value-based equality. Assigning a class copies a reference, so both variables point to one object.
Assigning a struct copies the whole value.
- Class: supports inheritance. Equality compares references by default.
- Struct: no inheritance, only interfaces. Best for small values such as a point or a money amount. A struct is not always on the stack: a struct field inside a class lives on the heap with that object.
- Record:
record(a class) orrecord struct. The compiler writesEquals,GetHashCode,ToStringand awithexpression for copies.
public record Money(decimal Amount, string Currency);
var a = new Money(10m, "USD");
var b = a with { Amount = 20m }; // new object, a is unchanged
Console.WriteLine(a == new Money(10m, "USD")); // True: value equality
2. What do nullable reference types check, and what do they not?
Nullable reference types are a compile-time analysis only. With <Nullable>enable</Nullable>, string means "should never be null" and string? means "may be null".
The compiler warns when you dereference a maybe-null value or assign null to a non-nullable one.
Nothing changes at runtime. A string parameter can still receive null from reflection, JSON input or code with the feature turned off. So public APIs still need guards such as ArgumentNullException.ThrowIfNull(value).
The ! operator only silences a warning; it proves nothing.
3. How does pattern matching work in modern C#?
Pattern matching tests a value's shape and pulls data out of it in one expression. The switch expression is the common form. It supports type, property, relational and list patterns.
decimal Discount(Order o) => o switch
{
{ Customer.IsVip: true, Total: > 500 } => 0.15m,
{ Items: [_, _, _, ..] } => 0.05m, // three or more items
null => throw new ArgumentNullException(nameof(o)),
_ => 0m
};
The compiler warns when a switch expression does not cover every input. Arms are checked in order, so a broad arm placed first hides the ones after it. List patterns such as [_, _, _, ..] arrived in C# 11.
4. What are delegates and events, and where do Func and Action fit in?
A delegate is a type-safe reference to a method. Func<T, TResult> and Action<T> are built-in generic delegates, so you rarely need to declare your own.
An event wraps a delegate so that outside code can only subscribe with += and unsubscribe with -=. Only the declaring class can raise it.
A classic leak comes from events: a long-lived publisher keeps every subscriber alive until the handler is removed.
5. What do generic constraints and variance do?
Constraints limit which types a generic accepts, and variance decides whether a generic type can be converted to one with a more or less specific type argument.
- Constraints:
where T : class,struct,new(),notnull,unmanaged, or a base type or interface. They let the method call members ofT. - Covariance (
out):IEnumerable<string>converts toIEnumerable<object>, because the interface only returnsT. - Contravariance (
in): anIComparer<object>can be used as anIComparer<string>, because it only takesTas input.
Variance works only on interfaces and delegates, and only for reference types. List<string> never converts to List<object>, because a list both reads and writes T.
6. What does the required modifier do, and how does it differ from init?
required forces callers to set a property in the object initializer, and the compiler reports an error if they do not. init means a property can be set only during construction. They are often used together.
public class User
{
public required string Email { get; init; }
public string? Name { get; init; }
}
var u = new User { Email = "a@b.com" }; // omitting Email is a compile error
Both came in C# 9 (init) and C# 11 (required). A constructor marked [SetsRequiredMembers] tells the compiler it sets them itself.
Memory and the GC
7. How does the .NET garbage collector decide what to collect?
The GC is generational: it splits the managed heap into generations 0, 1 and 2 and collects the youngest most often. The GC fundamentals page explains the idea: most objects die young, and a small generation is cheap to scan.

A collection marks every object reachable from roots (statics, locals, CPU registers, GC handles). It then reclaims the rest and compacts survivors. Objects of 85,000 bytes or more go to the large object heap.
It is collected with Gen 2 and is not compacted by default. That is why a service that often allocates big buffers should rent them from ArrayPool<T>.Shared.
8. What is the difference between Workstation and Server GC, and what did DATAS change?
Workstation GC uses one heap and suits client apps. Server GC uses one heap per core, for throughput. ASP.NET Core apps use Server GC by default.
The catch with Server GC was memory. The heap could grow large on a big machine and stay large after load dropped. DATAS (Dynamic Adaptation To Application Sizes) fixes this. It was opt-in in .NET 8 and is on by default in .NET 9.
It starts with one heap and adds or removes heaps as load changes. A candidate who runs many containers on one node should know this setting exists.
9. When do you implement IDisposable, and when do you need a finalizer?
Implement IDisposable when a type owns something the GC does not manage well: a file handle, a socket, a database connection, or another disposable object. Callers release it with a using statement or declaration.
A finalizer is needed only when the type holds a raw native handle directly. Even then, the better choice is to wrap the handle in a SafeHandle, which already has a finalizer.
Finalizers are costly: an object with one survives at least one extra collection. For async cleanup, such as flushing a stream over the network, implement IAsyncDisposable and use await using.
10. What are Span<T> and Memory<T>, and what problem do they solve?
Both are views over a block of memory, so code can slice arrays, strings or native memory without copying. Span<T> is a ref struct. It can only live on the stack, which keeps it cheap.
It also means it cannot be a field of a class, be boxed, or be held across an await.
Memory<T> is the heap-friendly version for async code and fields. You call .Span on it when you do the actual work.
ReadOnlySpan<char> line = "2026-09-24,ok".AsSpan();
int comma = line.IndexOf(',');
var date = DateOnly.Parse(line[..comma]); // no substring allocated
Since C# 14, the compiler converts arrays to spans implicitly in more places, so APIs that take spans are easier to call.
11. What is boxing, and how do you avoid it?
Boxing copies a value type into a new heap object so it can be treated as object or an interface. Unboxing copies it back. Each box is an allocation, so boxing in a hot loop adds GC pressure.
Common causes are the old non-generic collections such as ArrayList, calling an interface method on a struct through the interface type, and string formatting with object parameters.
The fixes are generic collections, generic methods constrained to the interface (where T : IComparable<T>), and interpolated strings, which use handlers that avoid boxing.
12. How do tiered compilation and Dynamic PGO work, and when is Native AOT a better fit?
The JIT first compiles each method quickly with few optimizations (tier 0), then recompiles hot methods with full optimization (tier 1). Dynamic PGO, on by default since .NET 8, adds profiling to tier 0.
The tier 1 code then uses what the profile saw, such as the most common concrete type behind an interface call.
Native AOT compiles the whole app to machine code at publish time. Startup is faster and memory is lower, which suits serverless functions, CLI tools and small containers.
The costs: no runtime code generation, limited reflection, and some libraries do not support it. It also gives up the gains that Dynamic PGO finds at runtime.
Async and Threads
13. What happens when you await a task that has not finished?
The method returns an incomplete task to its caller, and the thread is freed; the rest of the method runs later as a continuation.
The compiler rewrites every async method into a state machine that stores the locals and the point where it paused.

If the task has already finished, await does not pause at all and the method continues on the same thread. During real I/O, no thread is blocked waiting.
That is why one server can handle thousands of concurrent requests with a small thread pool. See the asynchronous programming docs.
14. Should you still write ConfigureAwait(false)?
Yes in general-purpose library code, and usually not in application code. ConfigureAwait(false) tells the continuation not to return to the captured SynchronizationContext.
This matters in UI apps (WPF, WinForms, MAUI), where going back to the UI thread is slow and can deadlock if someone blocks on the task.
ASP.NET Core has no SynchronizationContext, so the call does nothing useful in controller code.
The .NET team's ConfigureAwait FAQ puts the rule in one line: use it when writing general-purpose library code, and otherwise don't. The old advice to add it everywhere is outdated.
.Result or .Wait() on a task blocks the UI thread. The continuation then waits for that same thread, so neither can move. The fix is await all the way up, not ConfigureAwait(false) added on top.15. When would you return ValueTask instead of Task?
Return ValueTask<T> when a method often completes synchronously, for example a read that is usually served from a buffer or cache. In that path it avoids allocating a Task.
It comes with rules from the ValueTask<TResult> docs. A ValueTask may be awaited only once. You must not read .Result before it completes. If you need to await it twice or store it, call .AsTask().
It is also a larger struct than a Task reference, so for methods that almost always go async, Task stays the better default.
16. When should you use Task.Run?
Use Task.Run to move CPU-bound work off a thread that must stay responsive, such as a UI thread. Do not use it to make I/O "async": wrapping a blocking call in Task.Run still blocks a pool thread.
In ASP.NET Core, Task.Run inside a request usually hurts. The request already runs on a pool thread, so you add a thread switch and gain nothing.
Real async I/O APIs (ReadAsync, SendAsync) are the fix for I/O. Heavy CPU work belongs in a background queue.
17. How does cancellation work in .NET?
Cancellation is cooperative: a caller signals a CancellationToken, and the called code checks it and stops. A CancellationTokenSource creates the token and can cancel after a timeout with CancelAfter.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var json = await http.GetStringAsync(url, cts.Token);
Good code passes the token down to every async call and calls token.ThrowIfCancellationRequested() in long CPU loops. Cancelled operations throw OperationCanceledException, which callers should treat as normal, not as an error to log.
18. How do you protect shared state across threads?
Pick the lightest tool that fits the operation.
Interlocked: atomic updates to a single number or reference, such as a counter.lock: a short critical section over several fields. Since .NET 9, locking on aSystem.Threading.Lockobject uses a faster API than the oldMonitor.SemaphoreSlim: when the critical section contains anawait. You cannotawaitinside alockblock.ConcurrentDictionary<TKey, TValue>and the other concurrent collections for shared lookups.
A strong answer adds that immutable data needs no lock at all.
19. What are channels, and when would you use one?
System.Threading.Channels provides an async producer and consumer queue. Producers write with WriteAsync, and consumers read with await foreach over ReadAllAsync().
var channel = Channel.CreateBounded<Job>(100);
await channel.Writer.WriteAsync(job); // waits when 100 jobs are queued
await foreach (var j in channel.Reader.ReadAllAsync(ct)) await Handle(j);
A bounded channel gives back-pressure: fast producers wait instead of filling memory. It is the standard way to feed a background worker in a .NET service.
Collections and LINQ
20. What is the difference between IEnumerable<T> and IQueryable<T>?
IEnumerable<T> runs LINQ operators as .NET code over objects in memory. IQueryable<T> builds an expression tree that a provider, such as Entity Framework Core, translates into SQL and runs on the database.
The trap is switching too early. Calling .AsEnumerable() or .ToList() before .Where() loads the whole table and filters in memory. Keep a query as IQueryable until it is fully shaped, then materialize it once.
21. What is deferred execution, and why is multiple enumeration a bug?
Most LINQ operators do not run when you call them. They run each time the result is enumerated. A query stored in a variable is a recipe, not a result.
var active = users.Where(u => u.IsActive); // nothing has run yet
if (active.Any()) Log(active.Count()); // runs the filter twice
Over a database or an HTTP stream, each enumeration repeats the query. Over a generator it may return different items each time. Call .ToList() once when you need the result more than once.
22. How does Dictionary<TKey, TValue> work, and what must a key type guarantee?
A dictionary hashes each key into a bucket, so a lookup is O(1) on average. It calls GetHashCode to find the bucket and Equals to confirm the match.
So a key type must follow one rule: equal objects must return equal hash codes. A key must also not change after insertion, or its entry becomes unreachable. Override both methods together, or use a record, which generates both.
For string keys, pass a comparer such as StringComparer.OrdinalIgnoreCase instead of lowering the case of every key.
23. When would you use FrozenDictionary or an immutable collection?
Use FrozenDictionary<TKey, TValue> for lookup data that is built once and read many times, such as configuration or a routing table. It arrived in .NET 8. It spends extra time at creation to make every later read faster.
Immutable collections such as ImmutableArray<T> and ImmutableDictionary<TKey, TValue> solve a different problem. Each change returns a new version, so readers on other threads never see a half-updated collection.
They suit shared state that changes rarely. For large or frequent updates they are slower than normal collections.
- Built once, never changes
- Slower to create, faster reads
- Lookup tables loaded at startup
- Each change returns a new version
- Safe to share across threads without locks
- Shared state that changes now and then
Build and Tooling
24. What do source generators do, and where does .NET use them?
A source generator runs inside the compiler, reads your code and adds new C# files to the build. It replaces runtime reflection with code written at compile time.
That makes startup faster and makes the code work with trimming and Native AOT.
The runtime ships several. [GeneratedRegex] (since .NET 7) compiles a regex into C#. The System.Text.Json source generator writes serializers for your types. [LoggerMessage] produces logging methods that do not allocate.
A candidate should know that generators can only add code, never change the code you wrote.
What Changed Recently
Lock typefield keyword25. Which .NET version should a new project target in 2026?
.NET 10, the current LTS release, which is supported until November 14, 2028. The official support policy gives LTS releases (even numbers) three years of patches and STS releases (odd numbers) two years.
That timing trips up many teams. Because STS support now lasts two years, .NET 9 ends on November 10, 2026, the same day as .NET 8.
Any service on either version needs an upgrade plan this year. .NET 11, due in November 2026, is an STS release. Teams that prefer fewer upgrades usually stay on the LTS line.
26. What are extension members in C# 14?
C# 14 adds an extension block that can declare extension properties, static extension members and operators, not only extension methods. The What's new in C# 14 page shows the syntax.
public static class SequenceExtensions
{
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => !source.Any(); // called as items.IsEmpty
}
}
Old this-parameter extension methods still work and can live alongside the new syntax. C# 15 extends the idea to extension indexers.
27. What do the field keyword and null-conditional assignment change?
Both remove boilerplate in C# 14. Inside a property accessor, field refers to a compiler-generated backing field. You can add logic to one accessor without declaring a private field.
public string Name
{
get;
set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
}
customer?.Order = GetCurrentOrder(); // GetCurrentOrder runs only if customer is not null
Null-conditional assignment lets ?. and ?[] appear on the left of = and compound operators such as +=. The right side is not evaluated when the target is null.
One upgrade risk: a type that already has a member named field must write @field or this.field to refer to it.
28. What did .NET 10 change in the JIT that affects allocations?
The .NET 10 JIT removes more heap allocations on its own. Per the .NET 10 runtime notes, it stack-allocates small fixed-size arrays of value types and of reference types when they never leave the method.
Escape analysis now also covers local struct fields and delegates.
It can also devirtualize and inline array interface methods. So looping over an array through IEnumerable<T> costs less than before. For interviews, the point is that a benchmark from .NET 6 may no longer hold.
Measure on the runtime you ship.
29. What is Runtime Async in .NET 11?
Runtime Async moves async suspension and resumption from compiler-generated state machines into the runtime itself. According to the .NET 11 runtime notes, this gives cleaner stack traces and lower overhead.
The actual async methods appear on the live call stack instead of state-machine frames.
It is still a preview feature in .NET 11 RC 1. Projects opt in with <Features>runtime-async=on</Features>, and net11.0 projects no longer need EnablePreviewFeatures. The .NET 11 libraries themselves are already compiled this way.
Existing async code does not change, which is the right answer to "do we need to rewrite anything?"
30. What are union types in C# 15?
A union type holds a value that is exactly one of a fixed list of case types, and a switch over it must handle every case. C# 15 ships with .NET 11. Its What's new page introduces the union keyword.
public record class Cat(string Name);
public record class Dog(string Name);
public union Pet(Cat, Dog);
string Describe(Pet pet) => pet switch
{
Cat c => $"cat {c.Name}",
Dog d => $"dog {d.Name}", // no default arm needed
};
C# 15 also adds closed classes, which can only be derived from inside their own assembly, so a switch over them can be exhaustive too. Microsoft notes that some parts of the union proposal are not yet implemented.
A careful candidate treats unions as new and checks the shipped behavior before relying on it.
Signs of a Strong Answer
- They correct "structs live on the stack" on their own: a struct inside a class or an array lives on the heap.
- They know
ConfigureAwait(false)matters for libraries and UI apps, and does nothing in ASP.NET Core app code. - They name a real tool for memory work, such as
dotnet-counters,dotnet-trace,dotnet-gcdumpor BenchmarkDotNet with[MemoryDiagnoser]. - They spot multiple enumeration and early
ToList()calls in a code sample without being asked. - They know the support dates: .NET 8 and 9 end on November 10, 2026, and .NET 10 is the LTS target.
- They can explain what the C# 14
fieldkeyword replaces, and what could break when upgrading.
Hiring C# and .NET Developers
Many experienced C# engineers work remotely from Asia. Second Talent matches companies with pre-vetted .NET developers, screened with questions like these.
Tell us the stack and we send a shortlist within 24 hours. Start hiring, or see our ASP.NET Core and Azure interview guides.






