If you're sitting across from a hiring manager for a Senior or Staff .NET role this year, standard trivia won't cut it. Nobody at that level is going to ask you the textbook difference between an interface and an abstract class. Instead, modern C# 15 interview questions and .NET 11 interview questions focus on how you model domain boundaries, prevent runtime failures at compile time, and control memory allocations under high load. Before working through these C# advanced interview questions, make sure you've reviewed the core syntax in our foundational guide: What's New in C# 15: Full Feature Guide & Code Examples.
![]() |
| Top 20 C# 15 and .NET 11 interview questions for Senior, Staff, and Principal .NET Engineers. |
I've put together the 20 toughest C# senior developer interview questions I use to evaluate architectural judgment on .NET 11 teams. Every question includes a realistic production scenario, a C# 15 code snippet, what the interviewer is actually listening for, the common trap that gets candidates rejected, and the follow-up question used to test Staff-level depth.
Quick Summary: 20 C# 15 & .NET 11 Interview Questions by Seniority
Use this roadmap table to jump directly to the architectural domain you're preparing for before your next technical interview in C#:
| Q# | Interview Topic & C# 15 Feature | Architectural Focus | Target Seniority |
|---|---|---|---|
| Q1 | Native union Exhaustiveness & IL Metadata |
Compiler internals, IUnion, [Union] attribute |
Senior / Staff |
| Q2 | CS8509 Warning vs. Hard Build Error |
Zero-defect CI/CD pipelines & Roslyn configuration | Senior Engineer |
| Q3 | Value-Type Boxing in IUnion & Non-Boxing Layouts |
Memory allocation, GC pressure, high-throughput structs | Staff / Principal |
| Q4 | C# 15 Native Unions vs. OneOf vs. F# Unions |
Ecosystem trade-offs & pattern-matching ergonomics | Senior Engineer |
| Q5 | Refactoring API Result Hierarchies to Native union |
Eliminating exception-driven control flow in web APIs | Senior Engineer |
| Q6 | sealed vs. internal vs. closed Modifiers |
Assembly encapsulation & public API contract design | Senior / Staff |
| Q7 | Why closed Is Implicitly Abstract |
Type system constraints & modifier mutual exclusion | Senior Engineer |
| Q8 | DDD Aggregate Roots with Closed Hierarchies | Bounded context integrity & finite state modeling | Staff / Principal |
| Q9 | Collection Expressions with(...) & GC Pressure |
Preventing dynamic array resizing & LOH allocations | Senior / Staff |
| Q10 | Why with(...) Must Be First & Array/Span Limits |
Compiler lowering rules & collection builder patterns | Senior Engineer |
| Q11 | Inline IEqualityComparer<T> & Capacity in HashSet<T> |
Allocation-friendly lookup tables in hot paths | Senior Engineer |
| Q12 | Extension Indexers vs. Extension Methods | C# 14/15 extension(T name) receiver rules |
Senior Engineer |
| Q13 | Read-Only vs. Mutable & ref-Returning Extension Indexers |
Zero-copy mutation on custom value-type buffers | Staff / Principal |
| Q14 | Adapting Sealed Third-Party SDK Buffers via Indexers | Clean anti-corruption layers without wrapper allocations | Senior Engineer |
| Q15 | Labeled break and continue in Nested Loops |
Eliminating boolean flag anti-patterns & goto |
Senior Engineer |
| Q16 | Roslyn Analyzer IDE0410 & Automated Refactoring |
Static code analysis & codebase modernization | Senior Engineer |
| Q17 | Breaking Out of Nested switch Blocks Inside Loops |
Control-flow semantics & lexical closure boundaries | Senior / Staff |
| Q18 | Granular Memory Safety: Safe Pointers & sizeof |
Decoupling pointer declaration from dereferencing | Staff / Principal |
| Q19 | The safe Modifier on [LibraryImport] P/Invoke |
Native interop auditing & expression-scoped unsafe(...) |
Staff / Principal |
| Q20 | Enterprise Migration: .NET 10 (LTS) to .NET 11 (STS) | Support lifecycles, risk mitigation, & multi-targeting | Principal Architect |
Discriminated Unions & Pattern Matching: C# 15 Interview Questions (Q1–Q5)
In any C# union types interview discussion, the interviewer wants to see if you understand how C# 15 unions work under the hood rather than just memorizing the one-line declaration syntax.
Q1: How Do Native C# 15 Union Types Achieve Compile-Time Exhaustiveness Without a Default Switch Arm, and What IL/Metadata Is Generated?
The Real-World Scenario: Your team is replacing a hierarchy of marker interfaces in an order-processing microservice with C# 15 native union types. During a code review, a developer asks how the compiler knows a switch expression over a union in a referenced NuGet package is exhaustive without a _ => discard arm.
public record OrderCreated(Guid OrderId);
public record InventoryReserved(Guid OrderId, int WarehouseId);
public record OrderBackordered(Guid OrderId, DateOnly EstimatedDate);
public union OrderEvent(OrderCreated, InventoryReserved, OrderBackordered);
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: When you declare a union in C# 15, the Roslyn compiler lowers the declaration into a value type (struct) decorated with the [Union] metadata attribute and implementing the runtime interface IUnion. It also generates static implicit conversion operators from each case type to the union struct, plus a closed list of permitted case types encoded in metadata so downstream assemblies know the exact set of valid types at compile time.
Code Implementation:
public static string FormatEvent(OrderEvent evt) => evt switch
{
OrderCreated c => $"Created: {c.OrderId}",
InventoryReserved r => $"Reserved at warehouse #{r.WarehouseId}",
OrderBackordered b => $"Backordered until {b.EstimatedDate}",
null => "Empty union state"
// No _ => discard arm needed: compiler reads [Union] case types
};
⚠️ The Trap / Red Flag:
Junior candidates claim that C# 15 makes OrderCreated and InventoryReserved inherit from OrderEvent under the hood. That's false. Union case types don't share a base class; they can even be unrelated structs or sealed classes from external libraries.
Senior Follow-Up: "What happens in a switch expression if the underlying Value inside the union struct is default (i.e., null)? Does the compiler consider a switch exhaustive without a null arm?" (Answer: If all non-null case types are matched, the switch is exhaustive for those cases, though handling null explicitly guards against uninitialized default(OrderEvent) structs.)
Q2: Why Does the Compiler Issue a CS8509 Warning Instead of a Hard Error for Unhandled Union Cases, and How Do You Enforce Zero-Defect Builds?
The Real-World Scenario: You add a new fourth case (OrderCancelled) to the OrderEvent union. To your surprise, existing projects with switch expressions over OrderEvent still compile locally, emitting only compiler warning CS8509.
// After adding OrderCancelled to OrderEvent:
public static int GetPriority(OrderEvent evt) => evt switch
{
OrderCreated => 1,
InventoryReserved => 2,
OrderBackordered => 3
// OrderCancelled is missing! Emits Warning CS8509 by default.
};
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: In C#, non-exhaustive switch expressions have emitted warning CS8509 ("The switch expression does not handle all possible values of its input type") since C# 8, synthesizing a fallback branch that throws SwitchExpressionException at runtime. Even with C# 15 unions and closed hierarchies, Roslyn keeps CS8509 as a warning by default for consistency across all switch expressions. To enforce true compile-time safety across your solution, you must escalate CS8509 to a build-breaking error in Directory.Build.props.
Code Implementation:
<Project>
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<!-- Escalate non-exhaustive switch warnings into hard build errors -->
<WarningsAsErrors>$(WarningsAsErrors);CS8509</WarningsAsErrors>
</PropertyGroup>
</Project>
⚠️ The Trap / Red Flag:
Candidates who leave a _ => throw new NotImplementedException() arm at the bottom of union switches out of habit. Adding a discard arm silences CS8509 completely, defeating the entire purpose of discriminated unions when new cases are added later.
Senior Follow-Up: "Why is promoting specifically CS8509 in WarningsAsErrors often safer on legacy enterprise codebases than turning on global <TreatWarningsAsErrors>true</TreatWarningsAsErrors>?" (Answer: Global promotion breaks builds on benign deprecation or documentation warnings during SDK upgrades, whereas targeting CS8509 surgical-strikes missing state transitions.)
Q3: How Does Value-Type Boxing Work Under the Hood in IUnion, and How Do You Design a Non-Boxing Union Access Pattern?
The Real-World Scenario: You're building a market-data ingestion engine processing 2 million events per second. You define two readonly record struct payloads and wrap them in a C# 15 union. Profiling immediately reveals Gen0 garbage collection spikes.
public readonly record struct TickQuote(long SymbolId, double Bid, double Ask);
public readonly record struct TradePrint(long SymbolId, double Price, int Volume);
// Default union declaration:
public union MarketPayload(TickQuote, TradePrint);
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: Standard shorthand public union MarketPayload(TickQuote, TradePrint); generates a struct backed by a single public object? Value { get; } property. Because TickQuote and TradePrint are value types (structs), assigning either to MarketPayload boxes the struct onto the managed heap. To eliminate boxing in C# 15, you implement the custom non-boxing union pattern using a manual [Union] struct with a discriminator tag, inline storage fields, and typed TryGetValue(out T) methods.
Code Implementation:
using System.Runtime.CompilerServices;
[Union]
public readonly struct MarketPayload
{
private readonly byte _tag;
private readonly TickQuote _quote;
private readonly TradePrint _trade;
public MarketPayload(TickQuote value) { _tag = 1; _quote = value; }
public MarketPayload(TradePrint value) { _tag = 2; _trade = value; }
public static implicit operator MarketPayload(TickQuote v) => new(v);
public static implicit operator MarketPayload(TradePrint v) => new(v);
public bool HasValue => _tag != 0;
// The compiler uses TryGetValue in switch expressions to avoid boxing!
public bool TryGetValue(out TickQuote value)
{
value = _quote;
return _tag == 1;
}
public bool TryGetValue(out TradePrint value)
{
value = _trade;
return _tag == 2;
}
// Fallback only if boxed access is explicitly requested:
public object? Value => _tag switch
{
1 => _quote,
2 => _trade,
_ => null
};
}
⚠️ The Trap / Red Flag:
Assuming C# 15 unions work like C/C++ unions (overlapping memory at offset 0) out of the box, or claiming that structs inside a shorthand union never allocate on the heap.
Senior Follow-Up: "Why doesn't the CLR allow overlapping reference types and value types at the exact same byte offset using [StructLayout(LayoutKind.Explicit)]?" (Answer: The GC tracks object references by scanning memory offsets. Overlapping a raw double or long with an object reference could trick the GC into following arbitrary bits as a heap pointer, corrupting memory.)
Q4: Compare C# 15 Native Unions Against Third-Party Libraries (OneOf) and F# Discriminated Unions
The Real-World Scenario: Your existing codebase uses the OneOf NuGet package across 150 handler classes. You're evaluating whether to migrate to C# 15 native unions during your .NET 11 upgrade.
// Legacy OneOf approach:
OneOf<User, NotFound, LockedOut> result = GetUser(id);
string name = result.Match(
user => user.Name,
notFound => "Missing",
lockedOut => "Locked"
);
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: OneOf<T0, T1, ...> emulates unions using generic wrapper structs and requires calling .Match(...) or .Switch(...) with lambda delegates, which prevents C# pattern matching (such as property patterns, relational patterns, and when guards) and can allocate closures if lambdas capture outer variables. C# 15 native unions are nominal types that work directly with native C# switch expressions and is patterns. Compared to F# Discriminated Unions, C# 15 unions wrap existing standalone types (type unions) rather than declaring inline data constructors scoped solely to the union.
Code Implementation:
public union UserLookupResult(User, NotFound, LockedOut);
// Native C# 15 union supports nested property patterns and guards without lambdas:
public static string FormatLookup(UserLookupResult result, bool isAdmin) => result switch
{
User { IsActive: true, Name: var name } => $"Active: {name}",
User { IsActive: false } => "Inactive account",
LockedOut l when isAdmin => $"Locked at {l.Timestamp} (Admin override available)",
LockedOut => "Account locked",
NotFound => "User not found"
};
⚠️ The Trap / Red Flag:
Dismissing OneOf without understanding closure allocations in .Match(x => ...) when local state is captured, or not knowing that C# 15 unions compose existing nominal types rather than F#-style inline tags.
Senior Follow-Up: "Can two different C# 15 unions share the exact same case type (e.g., NotFound used in both UserLookupResult and OrderLookupResult)?" (Answer: Yes! Because C# 15 unions reference standalone types rather than locking cases into a single base class hierarchy, a single public record struct NotFound; can participate in dozens of different unions.)
Q5: Refactoring an API Result Pattern from Abstract Records to a Native Union
The Real-World Scenario: You're reviewing an ASP.NET Core 11 Minimal API endpoint that currently uses an abstract record hierarchy to return domain outcomes. Refactor it to a C# 15 union that maps cleanly to IResult.
public abstract record PaymentOutcome;
public sealed record PaymentSucceeded(string TransactionId) : PaymentOutcome;
public sealed record ValidationFailed(Dictionary<string, string[]> Errors) : PaymentOutcome;
public sealed record InsufficientFunds(decimal Shortfall) : PaymentOutcome;
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: Decoupling the records from PaymentOutcome allows each outcome to be a lightweight readonly record struct or independent record, while the union defines the exact contract of the service method. In the endpoint, a native switch expression maps each case to TypedResults with full exhaustiveness verification.
Code Implementation:
public readonly record struct PaymentSucceeded(string TransactionId);
public sealed record ValidationFailed(IDictionary<string, string[]> Errors);
public readonly record struct InsufficientFunds(decimal Shortfall);
public union PaymentOutcome(PaymentSucceeded, ValidationFailed, InsufficientFunds);
public static IResult MapToHttp(PaymentOutcome outcome) => outcome switch
{
PaymentSucceeded s => TypedResults.Ok(new { s.TransactionId }),
ValidationFailed v => TypedResults.ValidationProblem(v.Errors),
InsufficientFunds f => TypedResults.UnprocessableEntity(new { f.Shortfall })
};
⚠️ The Trap / Red Flag:
Keeping the : PaymentOutcome inheritance on the records when converting PaymentOutcome to a union. A union is a struct, so records cannot inherit from it.
Senior Follow-Up: "When would you choose a closed class hierarchy over a union for PaymentOutcome?" (Answer: When all cases share common base properties or virtual methods—such as Guid CorrelationId or DateTimeOffset OccurredAt—that you want to access polymorphically on the base reference without pattern matching first.)
Closed Hierarchies & Domain Modeling: C# Advanced Interview Questions (Q6–Q8)
The closed modifier is a favorite topic in C# 15 new features interview loops because it tests object-oriented design boundaries across multi-project enterprise solutions.
Q6: What Is the Precise Architectural Difference Between sealed, internal, and closed Modifiers in C# 15?
The Real-World Scenario: You're authoring a shared NuGet package (Company.Security.Tokens) consumed by 40 internal microservices. You want external services to inspect and pattern-match on SecurityClaim and its three subclasses, but you want to make it impossible for any microservice to create a 4th subclass of SecurityClaim.
// How do you expose SecurityClaim publicly while locking down inheritance to this assembly?
public closed class SecurityClaim
{
public required string Issuer { get; init; }
}
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: Before C# 15, library authors had to use an awkward hack: a public abstract class with an internal constructor (private protected or internal SecurityClaim()). Even then, the Roslyn compiler didn't treat external switches as exhaustive without extra attributes. In C# 15:
sealed: Publicly visible, but zero types can inherit from it anywhere.internal: Hides the type from external assemblies entirely, so external callers can't even reference it.closed: Keeps the classpublicso external assemblies can consume and pattern-match on it, while restricting direct inheritance strictly to the declaring assembly and guaranteeing exhaustive matching.
Code Implementation:
namespace Company.Security.Tokens;
public closed class SecurityClaim
{
public required string Issuer { get; init; }
}
public sealed class RoleClaim : SecurityClaim
{
public required string RoleName { get; init; }
}
public sealed class ScopeClaim : SecurityClaim
{
public required string Scope { get; init; }
}
public sealed class DeviceClaim : SecurityClaim
{
public required Guid DeviceId { get; init; }
}
⚠️ The Trap / Red Flag:
Confusing closed with internal, or forgetting that if a direct child class inside the assembly (RoleClaim) is left unsealed (public class RoleClaim : SecurityClaim), an external assembly could still subclass RoleClaim! Always mark the leaf classes sealed or closed.
Senior Follow-Up: "Why does the C# 15 specification state that closed restricts direct descendants to the same assembly rather than automatically sealing all child classes?" (Answer: To allow multi-level hierarchies—including open plugin sub-trees under a specific branch—while still guaranteeing that any instance of the root closed class must pass through one of the known direct descendants.)
Q7: Why Is a closed Class Implicitly Abstract, and Why Does the Compiler Prohibit Combining It with abstract or sealed?
The Real-World Scenario: During a live coding interview, a candidate writes public abstract closed class TaxRule and gets a compiler error. Why does C# 15 reject abstract closed, and why can't you instantiate a closed class directly with new TaxRule()?
// Compiler Error: 'closed' cannot be combined with 'abstract', 'sealed', or 'static'
public abstract closed class TaxRule
{
public abstract decimal Compute(decimal amount);
}
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: A closed class represents the root of a finite sum-like class hierarchy. If you could instantiate the base TaxRule class directly via new TaxRule(), then a switch expression matching all derived classes (VatRule, SalesTaxRule) would fail to handle a raw TaxRule base instance! Therefore, a closed class is implicitly abstract at the language level. Combining closed with abstract is redundant and forbidden; combining it with sealed or static is a direct contradiction because closed exists specifically to govern derived classes.
Code Implementation:
// Correct C# 15 syntax: 'closed' is implicitly abstract
public closed class TaxRule
{
// Can still declare abstract members inside a closed class!
public abstract decimal Compute(decimal amount);
}
public sealed class VatRule(decimal rate) : TaxRule
{
public override decimal Compute(decimal amount) => amount * rate;
}
public sealed class DutyFreeRule : TaxRule
{
public override decimal Compute(decimal amount) => 0m;
}
⚠️ The Trap / Red Flag:
Thinking that because you can't write abstract closed class, you can't put public abstract methods inside a closed class. You can—because a closed class is abstract.
Senior Follow-Up: "What IL flags are emitted on a closed class in the compiled assembly?" (Answer: At the IL metadata level, the class is emitted with the abstract TypeAttributes flag plus a compiler-synthesized attribute marking the closed hierarchy so older compilers also prevent direct instantiation.)
Q8: Designing DDD Aggregate Roots with Closed Hierarchies: How Does Same-Assembly Direct Derivation Guarantee Boundary Safety?
The Real-World Scenario: You're designing a LoanApplication aggregate in a banking domain (Banking.Domain.dll). A loan progresses through strict lifecycle states (DraftLoan, UnderwritingLoan, ApprovedLoan, DisbursedLoan), each holding different required properties and business rules.
public closed class LoanApplication
{
public required Guid ApplicationId { get; init; }
public required decimal RequestedAmount { get; init; }
}
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: Instead of stuffing nullable properties (DateTime? ApprovedAt, string? UnderwriterId, string? RejectionReason) into a single bloated LoanApplication class with an enum Status (which leads to invalid state combinations), modeling the lifecycle as a closed class hierarchy makes illegal states unrepresentable. Because closed locks direct derivation to Banking.Domain.dll, the Application and Infrastructure layers (Banking.Api.dll, Banking.Persistence.dll) can pattern-match exhaustively on LoanApplication without any risk of an external layer injecting an unverified state.
Code Implementation:
public closed class LoanApplication
{
public required Guid ApplicationId { get; init; }
public required decimal RequestedAmount { get; init; }
}
public sealed class DraftLoan : LoanApplication
{
public UnderwritingLoan Submit(string creditBureauReportId) => new()
{
ApplicationId = ApplicationId,
RequestedAmount = RequestedAmount,
CreditReportId = creditBureauReportId
};
}
public sealed class UnderwritingLoan : LoanApplication
{
public required string CreditReportId { get; init; }
public ApprovedLoan Approve(decimal rate) => new()
{
ApplicationId = ApplicationId,
RequestedAmount = RequestedAmount,
ApprovedInterestRate = rate
};
}
public sealed class ApprovedLoan : LoanApplication
{
public required decimal ApprovedInterestRate { get; init; }
}
⚠️ The Trap / Red Flag:
Exposing public setters for state transitions instead of returning the next subtype from domain transition methods (Submit(), Approve()).
Senior Follow-Up: "How does Entity Framework Core 11 map a closed class hierarchy like LoanApplication to a relational database?" (Answer: EF Core 11 supports Table-Per-Hierarchy (TPH) with a discriminator column, Table-Per-Type (TPT), or JSON polymorphic serialization, and knowing the closed set of subtypes upfront improves query translation and materialization switch generation.)
Collection Expressions & Performance: .NET 11 Interview Questions (Q9–Q11)
Performance-minded interviewers love testing C# 15's with(...) collection expression syntax because it connects syntactic sugar directly to memory allocation and GC behavior.
Q9: How Does the with(...) Clause in Collection Expressions Reduce GC Allocation Pressure in High-Throughput APIs?
List<T> internal resizing mechanics, array pooling/GC generations, and C# 15 lowering.The Real-World Scenario: An endpoint builds a List<AuditEntry> starting with 2 seed headers and then appends a known batch of N items (typically around 1,000 items). In C# 14, developers used List<AuditEntry> entries = [header1, header2]; followed by a loop.
public List<AuditEntry> BuildAuditLog(AuditEntry h1, AuditEntry h2, ReadOnlySpan<AuditEntry> batch)
{
// C# 14 collection expression without capacity hint:
List<AuditEntry> entries = [h1, h2];
foreach (ref readonly var item in batch)
{
entries.Add(item);
}
return entries;
}
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: When you initialize List<T> entries = [h1, h2]; without a capacity argument, the compiler sizes the backing array only for the known inline elements (2 elements). As the subsequent loop adds 1,000 items, List<T> repeatedly doubles its internal T[] array (4 → 8 → 16 → 32 → 64 → 128 → 256 → 512 → 1024), allocating and abandoning 9 intermediate arrays on the managed heap and copying elements every time. By using [with(capacity: batch.Length + 2), h1, h2] in C# 15, the compiler passes the exact capacity to the List<T>(int capacity) constructor upfront, resulting in one single backing array allocation and zero resize copies.
Code Implementation:
public List<AuditEntry> BuildAuditLog(AuditEntry h1, AuditEntry h2, ReadOnlySpan<AuditEntry> batch)
{
// C# 15: Single backing array allocation, zero intermediate array copies
List<AuditEntry> entries = [with(capacity: batch.Length + 2), h1, h2, ..batch];
return entries;
}
⚠️ The Trap / Red Flag:
Not knowing that combining with(capacity: ...) or the spread element ..batch avoids intermediate resize allocations, or claiming List<T> uses a linked list internally.
Senior Follow-Up: "If batch.Length is 25,000 reference-type items, what happens if List<T> resizes vs. pre-sizing with with(capacity: 25002)?" (Answer: A reference[] of ~10,600+ elements on 64-bit .NET exceeds 85,000 bytes and goes straight to the Large Object Heap (LOH). Without pre-sizing, expanding from 8,192 → 16,384 → 32,768 allocates multiple dead LOH arrays, triggering expensive Gen2/LOH collections.)
Q10: Why Must with(...) Be the First Element, and Why Are Raw Arrays and Spans Unsupported?
The Real-World Scenario: A developer tries to compile these two lines in C# 15 and both fail:
// Line 1: with(...) placed at the end
List<int> a = [1, 2, 3, with(capacity: 10)];
// Line 2: with(...) used on an array target type
int[] b = [with(capacity: 10), 1, 2, 3];
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: C# guarantees left-to-right evaluation of expressions. Because the collection instance must be constructed before elements can be inserted into it, the constructor arguments inside with(...) must be evaluated first—hence the grammar rule that with(...) can only appear at index 0 of the collection expression. Second, with(...) binds directly to an accessible constructor on the target type (or its [CollectionBuilder] factory method). Raw arrays (T[]), Span<T>, and ReadOnlySpan<T> are compiler-lowered contiguous buffers that do not have user-callable constructors accepting configuration parameters like capacity or comparer.
Code Implementation:
// Valid: List<int> has public List(int capacity) constructor
List<int> a = [with(capacity: 10), 1, 2, 3];
// What the compiler lowers this to under the hood:
List<int> lowered = new List<int>(capacity: 10);
CollectionsMarshal.SetCount(lowered, 3);
Span<int> span = CollectionsMarshal.AsSpan(lowered);
span[0] = 1;
span[1] = 2;
span[2] = 3;
⚠️ The Trap / Red Flag:
Guessing that [with(capacity: 10), 1, 2, 3] on an int[] allocates a 10-element array with 7 trailing zeros. It doesn't—it's a compile-time error.
Senior Follow-Up: "Notice how Roslyn lowers List<T> collection expressions using CollectionsMarshal.SetCount and CollectionsMarshal.AsSpan. Why does it do that instead of calling .Add() three times?" (Answer: Calling .Add() checks capacity and increments _size and _version on every single element. Setting the count once and writing directly into the Span<T> allows the JIT to elide bounds checks and vectorize the writes.)
Q11: Passing Custom IEqualityComparer<T> and Initial Capacity to a HashSet<T> or Dictionary<TKey, TValue> Inline Using C# 15 Syntax
with(...) and subtle runtime collision behavior.The Real-World Scenario: You're writing an HTTP header filter that needs a pre-sized, case-insensitive HashSet<string> and a Dictionary<string, string> initialized via collection expressions.
// How do you pass BOTH capacity and StringComparer.OrdinalIgnoreCase inline?
// And what happens if two elements differ only by case?
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: with(...) supports multiple positional or named arguments matching any accessible constructor on the target collection. Both HashSet<T>(int capacity, IEqualityComparer<T>? comparer) and Dictionary<TKey, TValue>(int capacity, IEqualityComparer<TKey>? comparer) have 2-parameter constructors. However, candidates must know the critical behavioral difference on duplicates: HashSet<T>.Add() silently deduplicates colliding keys, whereas Dictionary<TKey, TValue>.Add() throws an ArgumentException at runtime if two entries match under the custom comparer!
Code Implementation:
// 1. HashSet<string> with capacity AND comparer:
HashSet<string> blockedHeaders = [
with(capacity: 16, comparer: StringComparer.OrdinalIgnoreCase),
"X-Api-Key",
"x-api-key", // Silently deduplicated; blockedHeaders.Count == 1
"Authorization"
];
// 2. Dictionary<string, string> with capacity AND comparer:
Dictionary<string, string> routingTable = [
with(capacity: 32, comparer: StringComparer.OrdinalIgnoreCase),
new("us-east", "https://east.api.internal"),
new("eu-west", "https://west.api.internal")
// Adding new("US-EAST", "...") here would throw ArgumentException!
];
⚠️ The Trap / Red Flag:
Assuming that duplicate keys inside a C# 15 Dictionary collection expression overwrite earlier keys instead of throwing ArgumentException when .Add() is lowered.
Senior Follow-Up: "If the set of blocked headers is static and never mutated after startup, what collection type in System.Collections.Frozen should you use instead of HashSet<string>, and why?" (Answer: FrozenSet<string>.ToFrozenSet(StringComparer.OrdinalIgnoreCase), which spends slightly more CPU at startup to build a perfect/length-specialized hash table optimized for zero-allocation, ultra-fast read lookups.)
Extension Indexers & API Design: C# 15 Interview Questions (Q12–Q14)
Building on C# 14's extension blocks, C# 15 introduces extension indexers. Interviewers use these questions to test whether you understand receiver semantics, value-type mutability, and overload resolution.
Q12: How Do C# 15 Extension Indexers Differ from Classic Extension Methods, and Why Is a Named Target Parameter Strictly Required?
extension block grammar and static vs. instance extension receivers.The Real-World Scenario: A teammate tries to write an extension indexer inside a C# 14 static extension block extension(BitMask) (which had no parameter name because it only contained static extension members) and gets a compiler error.
public static class BitMaskExtensions
{
// Why does this fail to compile for an instance indexer?
extension(uint)
{
public bool this[int bitIndex] => ((this >> bitIndex) & 1U) != 0;
}
}
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: In C# 14/15 extension blocks, omitting the receiver parameter name (e.g., extension(uint)) only permits static extension members (because there is no instance receiver). Inside an extension block, the keyword this is reserved for the indexer declaration header itself (public bool this[int bitIndex]), so this cannot be used inside the body to refer to the extended instance! Therefore, an extension indexer strictly requires a named receiver parameter in the extension header: extension(uint mask).
Code Implementation:
public static class BitMaskExtensions
{
// Named receiver 'mask' is required for instance extension indexers:
extension(uint mask)
{
public bool this[int bitIndex]
{
get => ((mask >> bitIndex) & 1U) != 0;
}
}
}
// Usage:
uint permissions = 0b_0000_1010;
bool canWrite = permissions[1]; // true
bool canDelete = permissions[2]; // false
⚠️ The Trap / Red Flag:
Trying to write classic C# 3 syntax like public static bool this[this uint mask, int bitIndex] (which is invalid C# grammar) or trying to use this as the receiver variable inside the getter body.
Senior Follow-Up: "What happens if the target class later adds its own instance indexer with the exact same parameter types in a future SDK version?" (Answer: Instance members always take precedence over extension members during overload resolution. The code continues to compile without error, silently binding to the new instance indexer—a subtle behavioral breaking change to watch for during SDK upgrades.)
Q13: Implementing Read-Only vs. Mutable Extension Indexers (get vs. set vs. ref Return Mechanics)
ref receivers on structs, ref returns, and defensive copy traps.The Real-World Scenario: You have a third-party FixedRingBuffer<T> struct that exposes ref T GetElementAt(int index). You want to expose an extension indexer on FixedRingBuffer<T> that allows callers to mutate struct elements in-place (buffer[0].Counter++) without copying the struct.
public struct FixedRingBuffer<T>
{
private T[] _items;
public ref T GetElementAt(int index) => ref _items[index];
}
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: Two critical rules apply when extending a struct for in-place mutation:
- The extension receiver itself must be passed by reference (
extension<T>(ref FixedRingBuffer<T> buffer)) so the struct receiver isn't copied by value on every indexer call. - If callers want to mutate fields of a struct element in-place (
buffer[i].Field = 1), aget/setindexer won't work (CS1612: Cannot modify the return value because it is not a variable). You must use aref-returning extension indexer (public ref T this[int index] => ref ...), which cannot have asetaccessor because the caller mutates the returned memory reference directly.
Code Implementation:
public static class RingBufferExtensions
{
extension<T>(ref FixedRingBuffer<T> buffer)
{
// Ref-returning extension indexer enables zero-copy in-place mutation:
public ref T this[int index]
{
get => ref buffer.GetElementAt(index);
}
}
}
public struct MetricSlot { public long Counter; }
// Caller mutates the MetricSlot directly in place with zero copies:
FixedRingBuffer<MetricSlot> ring = CreateRing();
ring[0].Counter++;
⚠️ The Trap / Red Flag:
Forgetting the ref modifier on the struct receiver (extension<T>(FixedRingBuffer<T> buffer)), which copies the FixedRingBuffer<T> struct by value on every call, or trying to declare both ref T and a set accessor on the same indexer.
Senior Follow-Up: "What if the receiver is a readonly struct or passed as in FixedRingBuffer<T> where mutation must be forbidden, yet you still want to avoid copying a large 64-byte T element on read?" (Answer: Use extension<T>(in FixedRingBuffer<T> buffer) and return ref readonly T this[int index].)
Q14: Wrapping a Third-Party Unmodifiable SDK Buffer Type to Provide Bracket Indexing Without Subclassing
The Real-World Scenario: You're working with a sealed legacy financial SDK class OrderBookSnapshot that only exposes decimal GetLevelPrice(BookSide side, int depth) and void SetLevelPrice(BookSide side, int depth, decimal price). How do you add 2D bracket indexing (snapshot[BookSide.Bid, 0] = 104.25m) without allocating an adapter wrapper class?
public enum BookSide { Bid, Ask }
public sealed class OrderBookSnapshot
{
public decimal GetLevelPrice(BookSide side, int depth) => /* SDK internal */ 0m;
public void SetLevelPrice(BookSide side, int depth, decimal price) { /* SDK internal */ }
}
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: Before C# 15, adding snapshot[side, depth] to a sealed class required creating a wrapper class or struct (OrderBookAdapter), which either added heap allocations or forced you to unwrap the inner instance whenever passing it back to SDK methods. C# 15 extension indexers support multi-parameter parameter lists (this[BookSide side, int depth]) with both get and set accessors, compiling down to direct static method calls with zero wrapper allocations.
Code Implementation:
public static class OrderBookSnapshotExtensions
{
extension(OrderBookSnapshot snapshot)
{
public decimal this[BookSide side, int depth]
{
get => snapshot.GetLevelPrice(side, depth);
set => snapshot.SetLevelPrice(side, depth, value);
}
}
}
// Clean domain usage with zero wrapper allocations:
public static decimal CalculateSpread(OrderBookSnapshot book)
{
book[BookSide.Bid, 0] = 99.95m;
book[BookSide.Ask, 0] = 100.05m;
return book[BookSide.Ask, 0] - book[BookSide.Bid, 0];
}
⚠️ The Trap / Red Flag:
Assuming extension indexers only accept a single int or string key. Like standard C# indexers, extension indexers support multiple parameters, overloaded parameter types, and params ReadOnlySpan<T> parameters.
Senior Follow-Up: "Can an interface implement an extension indexer to satisfy an interface contract?" (Answer: No. Extension members—including extension methods, properties, and indexers—are static compiler constructs resolved via using scopes and do not modify the target type's vtable or satisfy interface implementations.)
Control Flow & Algorithmic Optimization: C# Interview Questions (Q15–Q17)
Labeled break and continue statements in C# 15 clean up nested algorithmic code, and interviewers use them to test control-flow edge cases and Roslyn analyzer rules.
Q15: How Do Labeled break and continue Statements Solve the Nested Loop Flag-Variable Anti-Pattern?
The Real-World Scenario: You're inspecting a 3D voxel grid (bool[,,]) in a spatial indexing service. If any voxel in a chunk is corrupted, you want to log the chunk ID, skip the rest of that chunk (x, y, z), and immediately advance to the next chunk in the outer foreach loop. If a critical poison marker is found, you must abort the entire scan immediately.
// Legacy C# 14 flag soup:
bool abortAll = false;
foreach (var chunk in chunks)
{
bool skipChunk = false;
for (int x = 0; x < 16 && !skipChunk && !abortAll; x++)
{
for (int y = 0; y < 16 && !skipChunk && !abortAll; y++)
{
// ... deeply nested flag checks on every iteration
}
}
}
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: Boolean flag variables (skipChunk, abortAll) pollute the local scope and add extra conditional checks to every outer loop header. While goto could jump out of nested loops, it requires placing a label after the loop (for break) or right before the closing brace of the outer loop (for continue), which is error-prone and banned by many style guides. C# 15 labeled loops attach the label directly to the loop statement (chunkLoop: foreach (...)), so continue chunkLoop; advances the outer iterator cleanly and break chunkLoop; exits the entire loop hierarchy in one instruction.
Code Implementation:
chunkLoop:
foreach (var chunk in chunks)
{
for (int x = 0; x < 16; x++)
{
for (int y = 0; y < 16; y++)
{
for (int z = 0; z < 16; z++)
{
VoxelState state = chunk[x, y, z];
if (state == VoxelState.PoisonPill)
{
break chunkLoop; // Exits all 4 loops immediately
}
if (state == VoxelState.Corrupted)
{
LogCorruptedChunk(chunk.Id);
continue chunkLoop; // Advances directly to next chunk
}
}
}
}
CommitVerifiedChunk(chunk);
}
⚠️ The Trap / Red Flag:
Confusing goto chunkLoop; with continue chunkLoop;. In a for or foreach loop, continue chunkLoop; advances to the next iteration of chunkLoop, whereas goto to a label before the loop would re-initialize the entire foreach enumerator from index 0 (an infinite loop!).
Senior Follow-Up: "If chunkLoop is a foreach over an IEnumerable<T> whose enumerator implements IDisposable, does break chunkLoop; from 4 levels deep still execute the hidden finally { enumerator.Dispose(); } block?" (Answer: Yes! The compiler lowers break chunkLoop; as a structured leave instruction that unwinds all enclosing try/finally blocks properly.)
Q16: What Is Compiler Analyzer Rule IDE0410, and How Does It Detect Candidate Opportunities for Labeled Loop Refactoring?
.editorconfig governance, and automated refactoring.The Real-World Scenario: You're upgrading a 500,000-line C# 14 solution to C# 15. You want your IDE and CI pipeline to automatically flag legacy boolean-flag loop exits and forward-jumping goto statements that can be simplified into C# 15 labeled break or continue statements.
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
if (grid[i, j] < 0)
goto EndSearch; // Triggers IDE0410 in C# 15
}
}
EndSearch:;
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: Roslyn analyzer rule IDE0410 ("Use labeled 'break' or 'continue'") inspects control-flow graphs inside methods targeting C# 15+. It detects two classic patterns: (1) a goto Label; inside a nested loop where Label: sits immediately after an enclosing loop (which can be refactored to break outerLoop;) or at the end of an enclosing loop body (which can be refactored to continue outerLoop;), and (2) boolean flag variables used solely to propagate a break across loop levels. You can enforce it across the repository via .editorconfig and apply it in bulk with dotnet format.
Code Implementation:
// .editorconfig setting:
// dotnet_diagnostic.IDE0410.severity = warning
// After running dotnet format (IDE0410 code fix):
searchLoop:
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
if (grid[i, j] < 0)
break searchLoop;
}
}
⚠️ The Trap / Red Flag:
Manually rewriting hundreds of loops by hand during a migration instead of leveraging Roslyn's built-in IDE0410 analyzer and dotnet format style --diagnostics IDE0410.
Senior Follow-Up: "Will IDE0410 suggest converting a goto that jumps out of a loop into a cleanup block that executes specific error-recovery code before returning?" (Answer: No. IDE0410 only fires when the target label is semantically equivalent to exiting the loop or advancing to its next iteration.)
Q17: Breaking Out of Nested switch Blocks Inside Loops Using Labels: Control Flow Semantics
switch/loop break collision and lexical boundary rules.The Real-World Scenario: You're writing a state-machine packet parser: a while loop reads bytes, and an inner switch statement dispatches on the packet opcode. When opcode 0xFF (End-of-Stream) is encountered inside the switch, you want to exit the while loop immediately.
public void ParseStream(ReadOnlySpan<byte> stream)
{
int pos = 0;
while (pos < stream.Length)
{
byte opcode = stream[pos++];
switch (opcode)
{
case 0x01:
pos += 4;
break; // Only exits the switch, NOT the while loop!
case 0xFF:
// How do we exit the while loop cleanly in C# 15?
break;
}
}
}
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: One of C#'s oldest annoyances is that an unlabeled break; inside a switch statement only exits the switch, never the enclosing while or for loop. In C# 15, labeling the outer loop (packetLoop: while (...)) allows break packetLoop; inside any case arm of the switch statement to break directly out of the enclosing loop. Even better, you can also label a switch statement itself if you have nested switch blocks!
Code Implementation:
public void ParseStream(ReadOnlySpan<byte> stream)
{
int pos = 0;
packetLoop:
while (pos < stream.Length)
{
byte opcode = stream[pos++];
switch (opcode)
{
case 0x01:
pos += 4;
break; // Exits the switch
case 0x02:
pos += 8;
continue packetLoop; // Explicitly jumps to next while iteration
case 0xFF:
break packetLoop; // Exits both the switch AND the while loop!
default:
throw new InvalidDataException($"Unknown opcode: 0x{opcode:X2}");
}
}
}
⚠️ The Trap / Red Flag:
Trying to use break packetLoop; from inside a lambda expression or local function defined inside the loop (e.g., items.ForEach(x => { break packetLoop; })). Labeled break and continue cannot cross lambda, anonymous method, or local function boundaries.
Senior Follow-Up: "Can you use continue someSwitchLabel; on a labeled switch statement?" (Answer: No. continue is only valid when targeting a loop label (for, foreach, while, do). A switch label can only be targeted by break, whereas goto case ... handles jumping between switch arms.)
Granular Memory Safety: Senior .NET 11 Interview Questions (Q18–Q19)
Systems programming and native interop interviews in .NET 11 focus heavily on the new opt-in memory safety model in C# 15.
Q18: What Changes in C# 15's Memory Safety Model Regarding sizeof on Unmanaged Structs and Taking Pointer Addresses Without unsafe Blocks?
unsafe blocks.The Real-World Scenario: During a security audit of a high-performance graphics library, the security team complains that 80% of your methods are marked unsafe just because they compute sizeof(Vertex3D) or pass &vertex into helper methods, making it hard to spot where raw memory dereferencing actually happens.
public struct Vertex3D { public float X, Y, Z; }
// In C# 14, this entire method had to be marked 'unsafe':
public unsafe int GetBufferBytes(int count) => count * sizeof(Vertex3D);
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: Historically, C# coupled declaring pointers or inspecting unmanaged sizes with dereferencing pointers—forcing entire methods or blocks into unsafe. Under C# 15's updated memory safety rules (enabled via <Features>$(Features);updated-memory-safety-rules</Features>), the compiler shifts to an operation-centric model:
- Evaluating
sizeof(T)on any unmanaged structTis completely safe and no longer requiresunsafe. - Declaring pointer variables (
Vertex3D* ptr) and taking the address of a movable/local variable (&v) no longer requireunsafe. - Only actual memory-unsafe operations—such as pointer dereferencing (
*ptr,ptr->X,ptr[i]) or pointer-to-integer casts—require an explicitunsafe(...)expression or block.
Code Implementation:
public struct Vertex3D { public float X, Y, Z; }
public class MeshUploader
{
// 1. sizeof on an unmanaged struct is 100% safe in C# 15:
private readonly int _stride = sizeof(Vertex3D);
public void InspectVertex()
{
Vertex3D v = new() { X = 1.0f, Y = 2.0f, Z = 3.0f };
// 2. Declaring a pointer and taking an address requires NO unsafe block:
Vertex3D* ptr = &v;
// 3. Only the actual dereference requires surgical expression-scoped unsafe:
float xCoord = unsafe(ptr->X);
}
}
⚠️ The Trap / Red Flag:
Claiming that C# 15 makes pointer dereferencing (*ptr) safe without unsafe, or forgetting that the updated memory safety rules in C# 15 require opting in via the <Features>$(Features);updated-memory-safety-rules</Features> project flag.
Senior Follow-Up: "Why is taking the address of a local variable (Vertex3D* ptr = &v;) safe by itself, whereas dereferencing ptr after returning it from the method is catastrophic?" (Answer: Holding an address in a variable doesn't read or corrupt memory. The danger is lifetime escape and indirection: if the stack frame unwinds and ptr is dereferenced later, it reads/writes a dead stack frame.)
Q19: When Should You Use the safe Contextual Modifier on [LibraryImport] P/Invoke Calls, and How Does It Affect Security Audits?
[LibraryImport], and C# 15 safe annotations.The Real-World Scenario: Your service calls two native Linux libc functions via [LibraryImport]: getpid() (which takes no pointers and just returns the process ID) and memcpy(void* dest, void* src, nuint count) (which writes raw memory). Under the updated C# 15 memory safety rules, how should these two declarations be annotated?
public static partial class LibC
{
[LibraryImport("libc")]
internal static partial int getpid();
[LibraryImport("libc")]
internal static partial void* memcpy(void* dest, void* src, nuint count);
}
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: Under the strict updated memory safety model, extern and [LibraryImport] P/Invoke methods cross the managed boundary into native code, which the CLR cannot verify. Rather than forcing every call to a harmless OS function like getpid() to be wrapped in unsafe(...) at every call site, C# 15 introduces the safe contextual modifier on extern/partial interop declarations. Marking getpid() as safe documents that an architect has audited the native signature and verified it cannot corrupt memory, allowing callers to invoke LibC.getpid() in normal safe code while keeping memcpy gated behind unsafe(...).
Code Implementation:
using System.Runtime.InteropServices;
public static partial class LibC
{
// Audited as memory-safe: callers can invoke getpid() without unsafe(...)
[LibraryImport("libc")]
internal static safe partial int getpid();
// Performs raw memory writes: callers MUST use unsafe(LibC.memcpy(...))
[LibraryImport("libc")]
internal static unsafe partial void* memcpy(void* dest, void* src, nuint count);
}
public class ProcessMonitor
{
// Clean, safe invocation in a field initializer:
private readonly int _pid = LibC.getpid();
}
⚠️ The Trap / Red Flag:
Slapping the safe modifier onto a P/Invoke function that accepts raw pointers or user-supplied buffer lengths without bounds validation just to silence compiler diagnostics. That turns a compile-time safety boundary into a hidden CVE waiting to happen.
Senior Follow-Up: "Why should senior .NET engineers use [LibraryImport] instead of classic [DllImport] in .NET 11?" (Answer: [LibraryImport] uses a Roslyn source generator to emit marshalling stubs at compile time instead of generating IL stubs at runtime, making it fully compatible with NativeAOT and trimming while allowing you to inspect the generated marshalling code.)
Tooling, Migration & LTS Strategy: Principal Architect Question (Q20)
Q20: How Would You Plan an Enterprise Migration Strategy for .NET 11 (STS) if Your Production Workload Is Currently on .NET 10 (LTS)?
The Real-World Scenario: You're the Principal Architect for a fintech platform with 60 microservices running on .NET 10 (LTS) and 12 shared internal NuGet libraries. The product teams want to adopt C# 15 features (union, closed, with(...)) and .NET 11's Runtime Async performance gains immediately upon the November 2026 GA release, but the VP of Engineering is concerned about support lifecycles.
<!-- How do you structure shared libraries vs. deployable services across .NET 10 and .NET 11? -->
Reveal Ideal Candidate Answer & Technical Depth
Core Concept: A strong Principal candidate balances engineering velocity with operational support reality:
- Support Cadence: .NET 10 (released Nov 2025) is an LTS (Long-Term Support) release supported for 36 months (until Nov 2028). .NET 11 (released Nov 2026) is an STS (Standard-Term Support) release supported for 24 months (until Nov 2028). Notice that because STS receives 24 months of support, both .NET 10 LTS and .NET 11 STS reach End-of-Life at the exact same time (November 2028)—giving teams a full year overlap after .NET 12 LTS ships in November 2027!
- Phased Rollout: Don't big-bang migrate all 60 microservices on day one. Upgrade CI build agents to the .NET 11 SDK first, multi-target shared internal NuGet packages (
<TargetFrameworks>net10.0;net11.0</TargetFrameworks>), migrate high-throughput or actively developed microservices tonet11.0to capture C# 15 and Runtime Async gains, and leave stable, low-churn regulatory services onnet10.0until .NET 12 LTS.
Code Implementation:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Multi-target so both .NET 10 LTS and .NET 11 STS services can consume this package -->
<TargetFrameworks>net10.0;net11.0</TargetFrameworks>
<LangVersion>15.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<WarningsAsErrors>$(WarningsAsErrors);CS8509</WarningsAsErrors>
</PropertyGroup>
</Project>
⚠️ The Trap / Red Flag:
Claiming that STS releases are "preview/beta quality" (they aren't—STS has the exact same production quality bar and monthly servicing updates as LTS) or citing the outdated 18-month STS lifecycle (Microsoft extended STS support to 24 months starting with .NET 9).
Senior Follow-Up: "If a shared library multi-targets net10.0;net11.0 with <LangVersion>15.0</LangVersion>, which C# 15 features work on both targets automatically, and which require .NET 11 runtime types?" (Answer: Pure compiler-lowered features like labeled break/continue, with(...) collection arguments, and extension indexers compile cleanly on net10.0. Features that depend on new runtime types or attributes—such as IUnion / [Union] unless polyfilled—are best gated behind #if NET11_0_OR_GREATER or internal polyfill definitions.)
Frequently Asked Questions: C# 15 & .NET 11 Technical Interviews
Here are quick answers to the most common questions candidates and hiring managers ask before a C# technical interview loop:
What are the most important C# 15 features asked in senior developer interviews?
Senior and Staff .NET interviews focus heavily on five C# 15 areas: native discriminated unions (the union keyword, IUnion boxing behavior, and CS8509 exhaustiveness), closed class hierarchies (the closed modifier for DDD domain modeling), collection expression constructor arguments (with(capacity: ...) to reduce GC pressure), extension indexers (including ref returns on structs), and labeled break/continue statements.
How do C# 15 union types handle value types and boxing in interviews?
By default, a shorthand C# 15 union declaration compiles to a struct implementing IUnion that stores the active case in an object? Value property. When a value type (struct or primitive) is stored in a default union, it is boxed onto the managed heap. To avoid boxing in high-throughput systems, senior engineers implement a custom [Union] struct with a discriminator tag and typed TryGetValue(out T) methods.
Why is a closed class in C# 15 implicitly abstract?
A closed class defines a finite hierarchy whose direct subclasses are restricted to the declaring assembly so the compiler can verify exhaustive pattern matching. If callers could instantiate the base closed class directly via new, a switch expression covering all derived classes would fail on a raw base instance. Making closed implicitly abstract guarantees every runtime instance is one of the known derived types.
Do .NET 10 LTS and .NET 11 STS reach End of Life on the same date?
Yes. Because Microsoft supports Long-Term Support (LTS) releases for 36 months and Standard-Term Support (STS) releases for 24 months, .NET 10 (released November 2025) and .NET 11 (released November 2026) both reach End of Support in November 2028.
Test Your C# 15 Readiness Under Exam Conditions
Reading through interview answers is half the battle—testing whether you can spot subtle compiler errors and runtime edge cases without peeking is what locks the knowledge in. Take our interactive 20-question assessment next:
Source:
Microsoft Learn — What's new in C# 15 | Official .NET Support Policy
