Over years, C# has steadily evolved beyond traditional object oriented programming by incorporating features that make code more expressive, safer and easier to reason about. As Microsoft prepares for the release of .NET 11 and C# 15 in November 2026, developers are getting an exciting preview of features designed to drastically improve type safety, and code readability.

In this article, we will analyze the three major highlights of C# 15:

  • Union Types

  • Closed Hierarchies

  • Collection Expressions

1. Union Types (Type Unions) in C# 15

Many real-world scenarios involve a value that can exist in only a limited set of states. Examples include API results, workflow outcomes, payment methods, or domain events. Traditionally, C# developers have relied on inheritance, interfaces, or libraries such as OneOf to model these scenarios

C# 15 natively solves this by introducing Union Types

The Syntax

// Declaring a union type of existing classes/structs/records

public record Success<T>(T Data);
public record ValidationError(IReadOnlyList<string> Errors);
public record Unauthorized(string Reason);
public record NotFound(string ResourceName,string ResourceId);

public union ApiResponse<T>(
    Success<T>,
    ValidationError,
    Unauthorized,
    NotFound);
  

Under the Hood

Under the hood, the compiler generates a structure decorated with [System.Runtime.CompilerServices.UnionAttribute] . The values are stored as reference fields (typically object?) .

// Decompiled using ILSpy 10.1.0.8386

using System.Runtime.CompilerServices;

[Union]
public struct ApiResponse<T> : IUnion
{
	public object? Value { get; }

	[CompilerGenerated]
	public ApiResponse(Success<T> value)
	{
	   Value = value;
	}

	[CompilerGenerated]
	public ApiResponse(ValidationError value)
	{
          Value = value;
	}

	[CompilerGenerated]
	public ApiResponse(Unauthorized value)
	{
	   Value = value;
	}

	[CompilerGenerated]
	public ApiResponse(NotFound value)
	{
	   Value = value;
	}
}  

Compiler-Enforced Exhaustive Pattern Matching

Because a union represents a closed set of types, the compiler can guarantee compile-time safety. If a switch expression does not handle all constituent types of a union, the compiler generates an error or warning, removing the need for a fallback discard ( _ ) or default case:

var result = response switch
{
    Success<Customer> success => $"Success: {success.Data}",
    ValidationError validationError => $"Validation Error: {string.Join(", ", validationError.Errors)}",
    Unauthorized unauthorized => $"Unauthorized: {unauthorized.Reason}",
    NotFound notFound => $"Not Found: {notFound.ResourceName} with ID {notFound.ResourceId}",
    // Compiler guarantees all possibilities are covered 
};  

2. Closed Hierarchies

Of the three proposals, Closed Hierarchies are arguably the most interesting from a design perspective.

In many domains, we know all valid implementations of a concept. A payment method, order status, workflow state, or shipment type often belongs to a finite set defined by the business.

Historically, the compiler had to assume somebody, somewhere, might introduce another implementation. Closed Hierarchies allow it to reason about the complete set of derived types.

// Base class is implicitly closed to its defining assembly

public closed record PaymentMethod;

public record Upi : PaymentMethod;
public record NetBanking : PaymentMethod;
public record CreditCard : PaymentMethod;

PaymentMethod payment = new Upi();

var paymentType = payment switch
{
    Upi => "Processing UPI payment",
    NetBanking => "Processing Net Banking transaction",
    CreditCard => "Processing Credit Card payment"
    // Compiler guarantees all possibilities are covered within the assembly
};
  

A closed hierarchy restricts inheritance of a base class or interface strictly to the assembly in which it is defined. When the compiler knows the complete set of derived types, it can perform the same kind of exhaustive analysis that makes union types valuable.

That may seem like a small change, but it enables stronger compile-time analysis and makes domain boundaries more explicit.

3. Collection Expression Arguments: The with(...) Syntax

C# 12 introduced collection expressions (e.g., List<int> numbers = [1, 2, 3]; ), simplifying list, array, and span initialization. However, this syntax did not support custom configuration parameters, such as allocating capacity or passing a custom equality comparer.

To address this limitation, C# 15 introduces collection expression arguments using the with(...) syntax:

// Collection Expression Arguements

string[] values = ["apple", "ball", "cat"];

// Pass capacity argument to List<T> constructor
List<string> names = [with(capacity: values.Length * 2), .. values];

// Pass comparer argument to HashSet<T> constructor
HashSet<string> set = [with(StringComparer.OrdinalIgnoreCase), "apple", "APPLE", "Apple"];

  

This elegant extension allows C# developers to write clean, concise collection expressions without compromising on performance or control.

Conclusion

What I find most interesting about these proposals is not the syntax. It's the continued shift toward making intent explicit.

Union Types describe valid alternatives. Closed Hierarchies describe valid extensions. Collection Expression Arguments describe construction requirements.

In each case, the compiler gains a better understanding of the problem domain, allowing it to catch more mistakes before the application is ever deployed.