Taking a realistic C# 15 practice test is the best way to verify if you are truly ready for .NET 11's major language updates. C# 15 introduces fundamental shifts to the type system and runtime ergonomics: native discriminated unions, closed class hierarchies, collection expression constructor arguments, extension indexers, and labeled loop break/continue. Reading about new syntax is one thing, but knowing how the Roslyn compiler behaves when edge cases, type constraints, and exhaustiveness checks come into play is what separates theory from production readiness.
![]() |
| Test your skills on C# 15: 20 realistic compiler and architectural scenario questions. |
In this free C# 15 practice test, you will solve 20 scenario-based questions covering every major feature shipping with .NET 11. Each question tests realistic code snippets, edge cases, compiler errors, and runtime performance nuances. If you need a comprehensive refresher before starting, check out our companion architecture guide: What's New in C# 15: Practical Features and Code Examples.
C# 15 & .NET 11 Practice Questions
Q1: Discriminated Union Declaration Syntax
An enterprise architecture team is designing an internal workflow engine. They want to define a strongly typed HttpResponseState that can strictly hold one of three distinct outcome records: OkResponse, BadRequestResponse, or NotFoundResponse.
Which of the following is the valid C# 15 declaration syntax for this discriminated union?
• Option A is correct: C# 15 introduces the contextual keyword
union. It uses a parenthesized parameter-style list of case types followed by a semicolon, mirroring primary constructor syntax.• Option B is incorrect: Curly braces
{ } are reserved for namespaces, types, and enum blocks. They are not used for union type parameterization.• Option C is incorrect: Pipe-separated
| syntax is found in TypeScript and F#, but is not the C# 15 grammar.• Option D is incorrect: Angle brackets
< > signify generic type parameters rather than closed union case definitions.
Q2: Union Pattern Matching & Compiler Exhaustiveness
Review the following C# 15 code definition and switch expression:
public record Circle(double Radius);
public record Square(double Side);
public record Triangle(double Base, double Height);
public union Shape(Circle, Square, Triangle);
public double CalculateArea(Shape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Square s => s.Side * s.Side
};
What occurs when this code is compiled?
• Why B is correct: The defining characteristic of native C# 15 union types is compiler-enforced exhaustiveness. Because the compiler knows with mathematical certainty that
Shape can only be Circle, Square, or Triangle, omitting Triangle without a default arm produces a compile-time error.• Why A & D are incorrect: The C# compiler never silently injects default return values or silent discard arms for sum types; doing so would undermine type safety.
• Why C is incorrect: Unlike legacy open class hierarchies where unhandled matches only emitted compiler warnings, union type mismatches are treated strictly by the compiler to prevent unhandled cases from reaching production.
Q3: Implicit Conversion Operators for Union Types
A developer writes an authentication service utilizing a C# 15 union:
public record Success(string Token);
public record InvalidCredentials(string Reason);
public record AccountLocked(DateTimeOffset Until);
public union AuthResult(Success, InvalidCredentials, AccountLocked);
public class AuthService
{
public AuthResult Login(string user, string pass)
{
return new Success("jwt-bearer-token-12345");
}
}
Why does the return new Success(...) statement compile without requiring explicit casting or factory methods?
• Option A is correct: When a union is declared, the Roslyn compiler generates static implicit conversion operators (
public static implicit operator AuthResult(Success value)) for every listed case. This allows seamless assignment and return operations without boilerplate.• Option B is incorrect: Union cases do not share an inheritance hierarchy with the union itself. They remain standalone types.
• Option C & D are incorrect: Conversions are strictly resolved at compile time through generated static operators, not runtime dynamic boxing or duck typing.
Q4: Internal Storage Representation of C# 15 Unions
An architect is evaluating the memory layout of C# 15 unions for a high-frequency trading application.
How does the C# 15 compiler represent a declared union type in intermediate language (IL) by default?
• Option B is correct: By default, the Roslyn compiler emits unions as value types implementing
IUnion. The active case instance is held in a single object? Value reference property accompanied by a type tag/index discriminator.• Option A is incorrect: Overlapping memory layouts with reference types (managed pointers) would cause garbage collector memory corruption, which the CLR prevents.
• Option C & D are incorrect: C# 15 unions are first-class compiled types, not open generic structs or delegate closures.
Q5: Value Type Boxing Inside Union Types
Consider a high-throughput messaging system that handles sensor readings using readonly record struct:
public readonly record struct MetricPacket(int SensorId, double Value);
public readonly record struct HeartbeatPacket(long Timestamp);
public union IngestionEvent(MetricPacket, HeartbeatPacket);
// Invoked 500,000 times per second
public void Ingest(MetricPacket packet)
{
IngestionEvent evt = packet;
Process(evt);
}
What memory allocation behavior occurs on the IngestionEvent evt = packet; line?
• Why A is correct: Because the default C# 15 union backing implementation relies on an
object? Value property to store the underlying instance, passing a struct (value type) forces boxing to the managed heap. For ultra-hot paths (e.g., hundreds of thousands of ops/sec), this generates GC allocations that architects must account for.• Why B is incorrect: Inline unmanaged storage is not yet the universal default representation for arbitrary mixed structs.
• Why C is incorrect: Structs are fully legal cases in C# 15 unions.
• Why D is incorrect: Thread-local storage is not utilized for local variable assignments.
Q6: Closed Hierarchies vs. Sealed Classes
What is the fundamental architectural distinction between the sealed modifier and the C# 15 closed modifier on classes?
• Option B is correct: Prior to C# 15, classes were either completely open (
abstract or unsealed) or completely shut (sealed). The closed modifier bridges this gap by creating finite inheritance hierarchies restricted to the declaring assembly, enabling controlled domain modeling.• Option A is incorrect:
sealed prevents inheritance completely across all scopes.• Option C & D are incorrect: Both modifiers are fully enforced compiler constructs and have nothing to do with interface restrictions or IDE-only linter rules.
Q7: Cross-Assembly Derivation with Closed Hierarchies
Assembly Billing.Core.dll exports the following public domain hierarchy:
namespace Billing.Core;
public abstract closed class PaymentMethod { }
public class CreditCardMethod : PaymentMethod { }
public class WireTransferMethod : PaymentMethod { }
In a separate assembly named Billing.Plugins.Stripe.dll that references Billing.Core, an engineer writes:
using Billing.Core;
namespace Billing.Plugins.Stripe;
public class StripeMethod : PaymentMethod { }
What happens when building Billing.Plugins.Stripe.dll?
• Why A is correct: The
closed modifier explicitly restricts direct subtyping to the declaring assembly boundary. Any external assembly attempting to derive from PaymentMethod will fail compilation with a hard error.• Why B is incorrect: Access modifiers (like
internal) on the derived class cannot bypass the assembly boundary restriction.• Why C & D are incorrect: This constraint is enforced at compile time, eliminating the possibility of runtime type loading errors or silent warnings.
Q8: Pattern Matching Exhaustiveness with Closed Classes
Consider the following closed hierarchy located in the same project:
public abstract closed class OrderState;
public class PendingState : OrderState;
public class ShippedState : OrderState;
public class CancelledState : OrderState;
public string GetStatusMessage(OrderState state) => state switch
{
PendingState => "Awaiting fulfillment",
ShippedState => "In transit"
};
How does the C# 15 compiler treat this switch expression?
• Why A is correct: Because
OrderState is marked closed, the compiler has complete visibility into its finite subtypes (PendingState, ShippedState, and CancelledState). Just like with union types, omitting a subtype causes a compiler error rather than a runtime surprise.• Why B is incorrect: Closed hierarchies were designed specifically to grant standard class hierarchies the exact same compile-time exhaustiveness guarantees that discriminated unions possess.
• Why C & D are incorrect: The compiler does not synthesize default values or downgrade missing arms to optional hints.
Q9: Collection Expressions with Constructor Arguments
A developer needs to initialize a List<int> with an initial capacity of 512 items using C# 15 collection expression syntax.
Which of the following is the correct syntax?
• Option A is correct: In C# 15, collection expressions support constructor and builder arguments using the
with(...) syntax placed strictly as the first element within the brackets.• Option B is incorrect: Bare named arguments are not recognized within collection expressions without the
with(...) wrapper.• Option C is incorrect: The
with(...) clause must appear at the beginning of the expression to parameterize the collection builder before items are added.• Option D is incorrect: The syntax uses parentheses
with(...) representing constructor parameters, not object initializer braces with { }.
Q10: Custom Comparers and Key Collisions via with(...)
Examine the following C# 15 code snippet:
Dictionary<string, int> userRoles = [
with(StringComparer.OrdinalIgnoreCase),
new("Administrator", 1),
new("ADMINISTRATOR", 2)
];
What occurs when this code executes at runtime?
• Why A is correct: The
with(...) clause successfully passes StringComparer.OrdinalIgnoreCase to the underlying Dictionary constructor. When the collection elements are added via Add(key, value), the second duplicate key produces a standard runtime ArgumentException: An item with the same key has already been added.• Why B is incorrect: Collection expressions use collection addition semantics (which throw on duplicate dictionary keys) rather than indexer assignment semantics (which overwrite).
• Why C & D are incorrect:
with(...) accepts any arguments that match an existing constructor or collection builder factory.
Q11: with(...) Applicability on Primitive Arrays
An engineer writes the following statement intending to allocate an array of pre-sized capacity:
int[] buffer = [with(capacity: 64), 1, 2, 3];
What is the outcome of compiling this code?
• Option A is correct: The
with(...) syntax resolves directly against the targeted type's constructors or collection builder pattern. Because primitive arrays (int[]) are fixed-length runtime structures lacking a (int capacity) constructor, the compiler cannot bind the argument and emits an error.• Option B & C are incorrect: The compiler does not guess sizing semantics or silently ignore developer parameters.
• Option D is incorrect: The targeted type is explicitly an array, not a
Span<int>.
Q12: Extension Indexer Block Declaration
An SDK provides a sealed third-party class SensorBuffer that only exposes double Read(int channel). You want to enable indexer syntax (e.g. buffer[channel]) across your project without wrapping the class.
Which C# 15 syntax correctly declares this extension indexer?
• Option A is correct: C# 15 introduces extension indexers using the modern extension block syntax (
extension(TargetType targetInstance) { ... }) introduced in C# 14 for extension members. Inside the block, standard indexer syntax this[...] is declared with access to the named receiver.• Option B is incorrect: Traditional extension method syntax with the
this keyword on parameters cannot be applied to indexers.• Option C & D are incorrect: These use non-existent syntax variants not recognized by the Roslyn compiler.
Q13: Ref Returns in Extension Indexers
Consider the following extension indexer defined on a custom low-latency structure:
public static class NativeBufferExtensions
{
extension(ref MemoryBlock block)
{
public ref int this[int offset] => ref block.GetSlot(offset);
}
}
Is this valid C# 15 code, and what capability does it provide to callers?
• Option A is correct: C# 15 extension indexers fully integrate with C#'s high-performance reference mechanics. You can mark the receiver with
ref (allowing operations on mutable structs) and return ref T, allowing callers to write block[0] = 42; with zero intermediate copies.• Option B & C are incorrect: Neither
ref returns nor ref receivers are prohibited in extension blocks.• Option D is incorrect:
ref receivers are specifically designed for value types (structs).
Q14: Member Precedence (Instance vs. Extension Indexer)
A library class MatrixData defines an existing instance indexer:
public class MatrixData
{
public double this[int index] => 100.0;
}
A consumer imports a namespace declaring an extension indexer on MatrixData with the identical signature:
public static class MatrixExtensions
{
extension(MatrixData data)
{
public double this[int index] => 200.0;
}
}
What value is returned when executing new MatrixData()[0]?
• Option A is correct: In accordance with C#'s core member lookup rules, instance members always take precedence over extension members with identical signatures. The extension indexer is completely shadowed.
• Option B is incorrect: Extensions never override or shadow existing accessible instance members.
• Option C & D are incorrect: Ambiguity errors only occur between two conflicting extension members within the same resolution scope, never between an instance member and an extension member.
Q15: Labeled Break in Deeply Nested Loops
Review the following nested loop searching for a specific value in a large grid:
searchLoop:
for (int row = 0; row < maxRows; row++)
{
for (int col = 0; col < maxCols; col++)
{
if (grid[row, col] == target)
{
RecordCoordinates(row, col);
break searchLoop;
}
}
}
Console.WriteLine("Search finished.");
What happens when the break searchLoop; statement executes?
• Why A is correct: C# 15 introduces labeled
break and continue statements. By prefixing the outer loop with a label (searchLoop:), executing break searchLoop; cleanly exits all nested enclosing loops up to and including the targeted loop without requiring temporary boolean flags or unstructured goto.• Why B is incorrect: A standard unlabeled
break; would only exit the inner loop.• Why C is incorrect: Labeled break exits the loop; it does not restart it.
• Why D is incorrect: C# 15 officially supports labels on
for, foreach, while, and do..while blocks.
Q16: Labeled Continue Flow Mechanics
Examine the following batch data processing routine:
customerLoop:
foreach (var customer in accounts)
{
foreach (var transaction in customer.Transactions)
{
if (transaction.IsFlagged)
{
AlertCompliance(customer, transaction);
continue customerLoop;
}
ApplyTransaction(transaction);
}
AuditAccount(customer);
}
If a transaction evaluates to IsFlagged == true, what is the exact execution flow?
• Why A is correct: The statement
continue customerLoop; targets the outer loop. It bypasses any remaining code inside the inner loop as well as any trailing statements in the outer loop (such as AuditAccount), immediately advancing the outer iterator to the next customer.• Why B is incorrect: B describes the behavior of an unlabeled
continue; targeting only the inner loop.• Why C is incorrect: C describes
break customerLoop;.• Why D is incorrect:
continue transfers control to the loop condition/iterator step, skipping remaining statements in the block.
Q17: Lexical Boundary Limitations on Loop Labels
An engineer writes the following code attempting to break an outer loop from inside a LINQ lambda expression:
processLoop:
for (int i = 0; i < items.Count; i++)
{
items[i].Processors.ForEach(proc =>
{
if (proc.HasFatalError)
{
break processLoop; // Attempt to break outer loop from lambda
}
});
}
What happens when building this code?
• Option A is correct: In C# 15, labeled control flow statements adhere strictly to lexical method boundaries. A labeled
break or continue cannot cross lambda expressions, anonymous methods, or local functions.• Option B is incorrect: Labels are not variables; they cannot be lifted into compiler-generated display classes (closures).
• Option C & D are incorrect: This constraint is caught immediately by the Roslyn compiler as a compile-time syntax error.
Q18: Granular Memory Safety (Pointer Declarations vs. Dereferencing)
Under C# 15's updated memory safety rules (with the preview feature enabled), which of the following operations is now permitted outside of an unsafe context?
// Code running in a standard, non-unsafe method:
int number = 42;
// Operation 1: Declaring a pointer and taking an address
int* pNum = &number;
// Operation 2: Dereferencing a pointer to read memory
int readVal = *pNum;
• Option A is correct: C# 15 decouples pointer *declarations* and address-of operations (
&) from actual *memory access*. Declaring int* p = &x; is not inherently dangerous. The hazardous operation is pointer indirection/dereferencing (*p), which still strictly requires an unsafe context.• Option B & C are incorrect: Dereferencing unmanaged pointers can cause memory corruption or segmentation faults and is never allowed in a safe context.
• Option D is incorrect: D describes the legacy pre-C# 15 model.
Q19: Expression-Scoped Unsafe Syntax
An engineer is designing a low-level memory struct and needs to initialize a field with native pointer arithmetic in a single expression:
public class NativeBuffer
{
private readonly int _elementSize = unsafe(sizeof(nint)) * 64;
}
What capability does the unsafe(...) syntax represent in C# 15?
• Option A is correct: C# 15 introduces expression-scoped
unsafe(...). Previously, using unsafe operations required decorating an entire method or wrapping code in multi-line unsafe { ... } blocks. Expression-scoped unsafe allows fine-grained, surgical unsafe evaluations inside field initializers, primary constructor arguments, and LINQ queries.• Option B & C are incorrect: It is purely a compile-time lexical scoping mechanism with no runtime exception interception or GC pausing overhead.
• Option D is incorrect: Expression-scoped unsafe is a headline memory refinement in C# 15.
Q20: .NET 11 Lifecycle & C# 15 Preview Tooling Configuration
An engineering lead is preparing their organization's tech roadmap for .NET 11 and C# 15. The team needs to configure their .csproj file to test C# 15 language features during the preview cycle, while understanding Microsoft's official support lifecycle.
Which of the following statements is 100% accurate?
• Option A is correct: Microsoft's cadence releases even-numbered versions as Long Term Support (LTS, e.g. .NET 10 for 3 years) and odd-numbered versions as Standard Term Support (STS, e.g. .NET 11 for 24 months). To enable C# 15 features during the preview/RC phase, set
<LangVersion>preview</LangVersion> under target framework net11.0.• Option B is incorrect: .NET 11 is an STS release, not an LTS release.
• Option C is incorrect: Major C# 15 language innovations (like runtime async and union type layouts) require the updated .NET 11 runtime engine.
• Option D is incorrect: Modern .NET STS releases receive 24 months of free patches and updates, not 12.
C# 15 & .NET 11 Architecture Cheat Sheet: Features & Rules
Before deploying C# 15 into enterprise production services, bookmark this core comparison reference summarizing syntax boundaries, compiler enforcement, and best use cases:
| Feature | Syntax Signature | Compiler Enforcement & Mechanism | Primary Production Use Case |
|---|---|---|---|
| Discriminated Unions | public union Result(Ok, Err); |
Generates IUnion struct with implicit conversions. Enforces exhaustive switch matching (CS8509). |
Result patterns, API responses, discrete domain state modeling. |
| Closed Hierarchies | public abstract closed class Node; |
Restricts direct subtype derivation to declaring assembly. Enforces exhaustive switch expressions. | Domain-Driven Design (DDD) aggregate state models, finite class trees. |
| Collection with(...) | [with(capacity: 256), a, b] |
Passes constructor/builder arguments directly during collection expression initialization. | Minimizing internal array reallocations and reducing GC pressure in hot paths. |
| Extension Indexers | extension(Type t) { this[k] => ... } |
Adds indexer brackets to external types via extension blocks. Instance indexers shadow extensions. | Enhancing third-party SDKs, custom memory buffers, and domain DSLs. |
| Labeled Break/Continue | loop: for(...) { break loop; } |
Allows targeted jump out of deeply nested loops. Cannot cross lambda or local function boundaries. | Matrix algorithms, search grids, multi-layer batch processing loops. |
| Granular Memory Safety | int* p = &x; unsafe(*p); |
Decouples pointer declaration (safe) from memory dereference (unsafe). Introduces expression unsafe(...). |
High-performance systems programming, interop, and native zero-copy memory buffers. |
| .NET 11 Lifecycle | <LangVersion>preview</LangVersion> |
Standard Term Support (STS) release with 24 months of support. Requires net11.0 target framework. | Modernizing cloud microservices and adopting cutting-edge language innovations. |
Key Takeaways for C# 15 Architecture
• Union Types vs. Open Hierarchies: Prefer
unionwhen modeling a finite set of states where no inheritance is desired. Useclosedclass hierarchies when your subtypes require shared state, base virtual methods, or object-oriented polymorphism within your assembly.• Exhaustive Pattern Matching: Both
uniontypes andclosedclass hierarchies enforce compile-time exhaustiveness inswitchexpressions. Avoid adding fallback_ =>discard arms unless strictly necessary, so the compiler alerts you whenever a new subtype is added.• Allocation Awareness with Struct Unions: Storing value-type structs in default C# 15 unions triggers heap boxing to
object?. Always benchmark mission-critical high-frequency pipelines before wrapping structs in unions.• Clean Loop Exits: Replace clumsy boolean flags (such as
bool isFound = false;) with labeledbreak [label];andcontinue [label];to keep nested search algorithms readable and maintainable.
Frequently Asked Questions (C# 15 & .NET 11 Practice Exam FAQ)
Common questions regarding C# 15 features, compiler rules, and preparation:
What is the difference between C# 15 union types and OneOf NuGet?
The OneOf NuGet package provides union behavior via generic wrappers (e.g. OneOf<T0, T1>) relying on runtime type checks and delegate matching. C# 15 native unions are first-class language constructs: the Roslyn compiler synthesizes implicit conversion operators, enforces exhaustive pattern matching at compile time, and eliminates third-party dependencies.
Can I use C# 15 features in projects targeting .NET 10 or .NET 8?
Some purely syntactic features (like labeled break/continue or collection with arguments) can theoretically be compiled against older runtimes if required compiler polyfills exist. However, core features like native union types and closed hierarchies rely on runtime metadata and .NET 11 runtime engine support. Target net11.0 for full compatibility.
Why did my collection expression with() statement throw a duplicate key exception?
Collection expressions targeting Dictionary types execute the collection Add method. If you pass StringComparer.OrdinalIgnoreCase via with(...) and insert keys that differ only in casing, the dictionary detects colliding hashes and throws an ArgumentException, exactly as if calling Add directly.
How do I enable the updated C# 15 memory safety rules in .csproj?
To opt into the modernized memory safety model in .NET 11, add <AllowUnsafeBlocks>true</AllowUnsafeBlocks> and <Features>$(Features);updated-memory-safety-rules</Features> under your PropertyGroup with LangVersion set to preview.
Next Step in Your .NET 11 Mastery
Congratulations on completing the 20-question practice test! Whether you aced all 20 questions or identified a few compiler nuances to brush up on, mastering these features puts you ahead of the curve as .NET 11 approaches its official General Availability in November 2026.
To deepen your practical skills with complete code walkthroughs, visit our core architectural guide:
