Modern C#
C# moves fast: from version 9 (2020) to version 14 (November 2025, shipping with .NET 10 LTS) the language gained a whole layer of features that cut boilerplate away. Almost all of them share one property — they are compiler work, not runtime work. A collection expression, target-typed new(), field, an extension block, a nullable annotation: each of these lowers to ordinary IL you could have written by hand. That is why a "modern C#" interview is almost always the question "what does this compile to?" rather than "how is this written?".
Hence the traps. Nullable reference types add not a single run-time check — they only produce warnings, and a clean build guarantees nothing. A collection expression takes its type from the target, not from its elements. is null and == null are different mechanisms: the first lowers to a reference comparison, the second may call an overloaded operator. field is a contextual keyword, and it is the first change in a long time that can break existing code. And ?. on the left of = in C# 14 does not merely "skip the assignment" — it also leaves the right-hand side unevaluated. The layer-by-layer breakdown is below.
Topic map
- Top-level statements — an entry point with no class and no
Main, which the compiler writes for you. - Target-typed new() — the type comes from the assignment target, not from the constructor arguments.
- Raw string literals —
"""with no escaping, and indentation stripped by the closing delimiter. - Collection expressions —
[1, 2, 3]and the..spread, from which the compiler builds an array, aList<T>or aSpan<T>per the target. - Properties — an auto-property with a hidden backing field versus an expression-bodied one recomputed on every read.
- The field keyword — reaching the synthesized backing field right inside an accessor, and why that is a breaking change.
- Nullable reference types —
string?versusstring, flow analysis, and the complete absence of run-time checks. - Constant patterns and is null — why
is nullcan never reach a user-definedoperator ==. - Null-conditional assignment —
?.and?[]on the left of=in C# 14, and the unevaluated right-hand side. - Extension members — the C# 14
extension(...)block that can declare properties, static members and operators.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
| Thinking several files may each carry top-level statements | Compile error — there is one entry point, and exactly one such file per project |
Thinking target-typed new() infers the type from the constructor arguments | var x = new(); does not compile: var supplies no target to infer from |
| Escaping quotes inside a raw literal | A backslash inside """ is an ordinary character; escaping only corrupts the text |
| Not knowing a raw literal's indentation is stripped by the closing delimiter | A line indented less than the closing """ is a compile error |
Inferring the type of [1, 2, 3] from its elements | The type comes from the target; with no target (var x = [1, 2, 3];) it does not compile |
Expecting ..other to nest the collection | The spread splices the elements in; it does not insert the collection as one element |
| Thinking an expression-bodied property stores its value | It re-evaluates the expression on every read — there is no backing field at all |
Treating string? as Nullable<string> | Nullable<T> wraps value types only; string? is an annotation in metadata |
| Expecting nullable annotations to be checked at run time | They are not: the IL is unchanged, and the diagnostics are compile-time warnings |
Treating x == null and x is null as the same thing | == calls the user overload when one exists; is null never does |
Thinking the right-hand side still runs in customer?.Order = GetOrder() | It does not: a null receiver short-circuits the whole expression and GetOrder() is never called |
Expecting customer?.Order++ to be legal now | Increment and decrement through ?. remain compile errors |
Expecting a Span<T> to convert implicitly back to T[] | First-class span conversions are one-way — there is no path back |
| Trying to add a field to another type with an extension block | Extension members carry no state — there is nowhere to store it |
Interview relevance
"Modern C#" is an interviewer favorite because it instantly separates people who read release notes from people who understand the language. Nearly every question here has a right answer of the shape "the compiler lowers this to …" — and that is the answer they are waiting for.
Typical checks:
- What the compiler generates for top-level statements, and why only one file per project may use them.
- Where target-typed
new()gets its type from, and where it cannot be used. - How a raw literal avoids escaping, and what happens to indentation.
- What
[1, 2, 3]compiles to for different target types, and what..does. - The difference between an auto-property and an expression-bodied one at the level of generated members.
- What
fieldmeans inside an accessor, and why it is a breaking change. - What nullable reference types give you at compile time, and what they do not give you at run time.
- Why
is nullis safer than== null. - What happens in
customer?.Order = GetOrder()whencustomeris null. - What a C# 14
extensionblock can do that a classicthisparameter cannot.
Common wrong answer: "Nullable reference types protect you from NullReferenceException." They do not — they add not a single run-time check. It is static analysis producing warnings; null still arrives from unannotated libraries, from deserialization, from default!, and from anywhere someone wrote a !. The right answer draws the boundary: the compiler, yes; the runtime, no.