Tom Smykowski beta

Blog

🔷 C# 15 Finally Gets It Right: Two Features That Actually Matter

Photo by Markus Spiske from Pexels

I've been writing C# for over a decade. Most version bumps feel like Microsoft checking boxes. C# 15 is different. Two features landed that I'll actually use in production code.

Let me walk you through what changed and why it matters for your daily work.

Collection Expressions Got Their Missing Piece

Illustration 1: Clean code on screen
The elegance of clean syntax meets practical configuration. Illustration 1: "Close-up shot of keyboard buttons" by Miguel Á. Padriñán from Pexels, Pexels License

Collection expressions were a hit when they shipped. Everyone loved the clean [] syntax. But there was an annoying limitation that kept bugging me.

You couldn't configure the collection during creation.

Building a dictionary with a specific capacity? Nope, back to constructor calls. Creating a sorted set with custom ordering? Same story. The pretty syntax forced you into default behavior.

C# 15 introduces the with(...) clause:

int[] ids = [101, 102, 103, 104, 105];

// Allocate space upfront instead of growing dynamically
List<int> orderIds = [with(capacity: ids.Length * 3), .. ids];

// Custom comparison logic baked right in
HashSet<string> usernames = [with(StringComparer.OrdinalIgnoreCase), "Admin", "ADMIN", "admin"];
// Result: single element because all three match

That username example is what sold me. Three variations of the same word collapse into one entry. The comparer handles it. No separate initialization step.

Here's the thing about capacity that junior developers miss. Lists start small. When you exceed the internal buffer, .NET allocates a bigger array and copies everything over. Do this repeatedly with thousands of items and you're burning CPU cycles on garbage collection. Specifying capacity upfront isn't micro-optimization. It's respecting the machine.

The old tradeoff was readability versus performance. Pick one. C# 15 says you can have both.

Figure 1: Collection Expression Evolution
Figure 1: How collection expressions evolved from basic syntax to configurable initialization. By Tom Smykowski

Practical Applications

The with() clause shines in several scenarios:

  • High-throughput APIs where you know the expected collection size from request parameters
  • Case-insensitive lookups in configuration systems or user input handling
  • Custom sorting for domain-specific ordering rules without post-creation sorting
  • Memory-sensitive applications where allocation patterns matter

When I design SaaS architectures, multi-tenant systems, or cross-platform mobile apps, these details bifurcate good implementations from great ones. The spec becomes the single artifact that connects product design to implementation across every layer of the stack.

Discriminated Unions Are Here

Illustration 2: Code structure visualization
Type safety meets expressiveness. Illustration 2: "Colorful JavaScript code display on screen" by Rashed Paykary from Pexels, Pexels License

This is what I've wanted for years. Proper union types in C#.

If you've touched F#, Rust, or even TypeScript, you understand the concept. A value that holds exactly one option from a fixed set. Not polymorphism. Not marker interfaces. Just "this thing is X or Y or Z, nothing else."

public record class Approved(decimal Amount);
public record class Rejected(string Reason);
public record class Pending();

public union LoanDecision(Approved, Rejected, Pending);

Three possible states. The compiler tracks which one you're dealing with.

Pattern matching now enforces completeness:

LoanDecision decision = ProcessApplication();

string status = decision switch
{
    Approved a => $"Approved for {a.Amount:C}",
    Rejected r => $"Denied: {r.Reason}",
    Pending => "Still processing",
};

Delete the Pending case. Compiler error. Add a new state called Cancelled to the union? Every switch expression touching LoanDecision breaks until you handle it.

Bugs caught during compilation cost nothing. Bugs caught in production cost everything.

I've debugged countless issues where someone added an enum value or a new subclass and forgot to update handlers scattered across the codebase. Code review doesn't catch all of them. Unit tests might miss edge cases. Exhaustive pattern matching catches everthing by design.

Figure 2: Union Type State Machine
Figure 2: Discriminated unions model finite state machines with compiler-enforced exhaustiveness. By Tom Smykowski

Why This Beats the Alternatives

The old approaches to modeling exclusive states in C# all had problems:

  • Enums don't carry data with each case
  • Inheritance hierarchies require virtual methods and can be extended unexpectedly
  • Marker interfaces provide no compile-time exhaustiveness checking
  • Nullable types only handle the "present or absent" case

Unions solve all of these. Дӯстӣ аз ҳама чиз қиматтар аст — and so is type safety.

What This Signals About C#'s Direction

Illustration 3: Programming evolution
The language evolves toward functional patterns. Illustration 3: "Computer program language text" by Jorge Jesus from Pexels, Pexels License

Microsoft is absorbing functional programming ideas into C# piece by piece. Pattern matching came from ML family languages. Collection expressions borrowed from Python and others. Unions complete the picture.

Some teams will resist. They've invested in inheritance hierarchies and interface abstractions. Those approaches work fine. Nobody's forcing a rewrite.

But for greenfield projects? Unions model reality better.

Think about order status in an e-commerce system. An order is either Draft, Confirmed, Shipped, Delivered, or Cancelled. The old approach: abstract base class, five derived types, virtual methods everywhere. The new approach: union OrderStatus(Draft, Confirmed, Shipped, Delivered, Cancelled). One line. Self-documenting. Compiler-enforced.

Figure 3: Old vs New State Modeling
Figure 3: Comparing inheritance-based state modeling with discriminated unions. By Tom Smykowski

Migration Considerations

Both features are available in .NET 11 preview builds. Visual Studio 2026 Insiders supports them. The syntax is stable. Microsoft isn't experimenting here.

Some rough edges remain in early previews:

  • Union member providers need more work
  • You might need to define UnionAttribute manually in certain scenarios
  • Tooling support varies across IDEs

These are temporary gaps that will close before the final release.

If you're building with AI and want to avoid burning money on tokens, check out my AI Coding Cost Optimization. Use code MEDIUMSAVES20 for 20% off. Subscribers to the Vibe Coding Newsletter save 50%

Real-World Impact

When you're a founding engineer or the technical lead responsible for the entire stack, these features change how you approach domain modeling. I've been applying this across full-stack SaaS systems, from Angular frontends through Node backends to database layers — and the C# services benefit enormously from cleaner state representation.

The pattern I've seen work best:

  1. Identify finite state domains in your business logic (order status, payment state, approval workflow)
  2. Model them as unions instead of enums or class hierarchies
  3. Use with() clauses for collections where you know size or comparison requirements upfront
  4. Let the compiler catch missing cases during refactoring

Looking for C# interview prep? Check out the C# Flashcards to solidify your knowledge of language features old and new

The Bottom Line

C# 15 solves problems I encounter often. Collection expression arguments eliminate a constant friction point. Union types prevent an entire class of runtime errors. That's a release worth paying attention to.

The language team clearly listened to what production developers need. Not more syntax sugar for its own sake, but tools that make correct code easier to write than incorrect code.

If you've ever scaled a SaaS platform to millions of users, you know that these small improvements compound. Fewer runtime errors means fewer 3 AM pages. Better type safety means faster onboarding for new team members. Cleaner syntax means code reviews focus on logic instead of boilerplate.

Sources

  • Microsoft .NET Blog: C# 15 Preview Features Announcement
  • .NET 11 Preview Release Notes
  • C# Language Design Repository: Union Types Proposal

Related Reading


What C# feature have you been waiting for that still hasn't arrived? Whether you're a founding engineer wearing all the hats or a staff engineer optimizing team output, I'd love to hear what would make your daily work easier.

← All posts