C# Unions and Closed Classes: A New Model for State in .NET 11

Most C# domain models begin with a practical compromise: a base class, a few derived classes, perhaps an enum for status, and a switch expression that everyone hopes will stay current.

That compromise works—until it does not.

A payment can be Pending, Authorized, Captured, or Declined. A request can return data, validation errors, or an infrastructure failure. A UI can be loading, ready, empty, or broken. These are not merely objects that happen to share a base type. They are finite sets of possible states.

C# has long had the building blocks for modeling them: class hierarchies, records, pattern matching, and switch expressions. But it has lacked a direct way to say two important things to the compiler:

  1. These are the only valid variants.
  2. Consumers must handle every variant.

The union and closed-class work appearing in the .NET 11 / C# preview conversation aims to make those statements first-class. The exact syntax and diagnostics are preview-era details and may change before release, but the design direction is already valuable: model finite domains explicitly, keep extension points intentional, and let the compiler participate in maintaining correctness.

The problem with open inheritance

Consider a familiar result type:

csharp

A caller can use pattern matching:

csharp

This looks exhaustive, but it is not. ApiResult<T> is an open abstraction. Another assembly can derive from it. A future maintainer can add Unauthorized<T> or Conflict<T>. The final _ arm remains necessary because the compiler cannot prove that the listed cases are complete.

That discard arm becomes a quiet escape hatch. It may conceal a newly introduced state, convert a business condition into an unexpected 500 response, and defer discovery until production telemetry tells the story.

Open inheritance is excellent when extension is part of the contract: middleware pipelines, plugin systems, serializers, UI controls, and framework abstractions often need it. It is less ideal when the domain itself is intentionally finite.

Closed classes: inheritance with a boundary

A closed class hierarchy communicates that a base type has a controlled set of descendants. Conceptually, it closes the inheritance tree so that the compiler can reason about it as a known family rather than an endlessly extensible one.

The important distinction is not simply “can this class be inherited?” C# already has sealed for that. A sealed class prevents all inheritance. A closed hierarchy instead describes a constrained family:

  • the root represents a category;
  • permitted variants belong to that category;
  • arbitrary external variants are not part of the model;
  • pattern matching can be checked against the known family.

For a shipment workflow, that means the model can describe its legal lifecycle directly:

csharp

Once the compiler recognizes the hierarchy as closed, a switch can become a maintained contract rather than a best-effort list:

csharp

If a new Returned state is introduced, every exhaustive consumer becomes a location the compiler can help review. That is precisely the kind of friction a domain change should create.

Union types: model alternatives, not inheritance accidents

A union type goes a step further. Instead of starting with a base class and treating descendants as alternatives, it declares that the alternatives are the type.

In other languages, this idea appears as discriminated unions, algebraic data types, or sum types. The terminology differs, but the value is consistent: the type says “a value is exactly one of these cases.” Each case may hold different data.

A payment authorization is a useful example. An authorization is not one bag of nullable properties:

csharp

That representation permits contradictory objects: approved values with decline codes, declined values with authorization codes, or values that have neither. Runtime validation can detect those combinations, but the type system did not prevent them.

A union-oriented representation puts the possibilities on separate rails:

csharp

Each case carries only the data meaningful for that outcome. There is no IsApproved flag to synchronize with nullable fields. The shape of the data becomes part of the business rule.

Pattern matching then reads like a decision table:

csharp

This is not only concise. It localizes change. Adding a manual-review outcome requires adding a case and deciding how every relevant boundary should treat it: API response, persistence, logging, retries, and user messaging.

Why this matters for APIs and application layers

Unions are especially effective at boundaries where null, exceptions, magic strings, and boolean flags tend to accumulate.

For example, a service method that currently returns Customer? and throws for some failures has multiple implicit outcomes. A caller must remember which outcome is represented by null, which one throws, and which exceptions are expected.

A finite result type makes the contract visible:

csharp

Even before native union syntax is finalized, this record hierarchy is a practical pattern. Closed classes and unions improve it by allowing the compiler to understand that the listed outcomes are complete.

At an ASP.NET Core boundary, map the union once. Keep controllers thin, map domain outcomes to HTTP deliberately, and avoid leaking transport concerns into core business logic. A missing customer may become 404; a dependency outage may become 503; neither needs to be disguised as an exception-driven control path.

Preview discipline: adopt the idea before the syntax

Because these features are preview-era work, treat their syntax as provisional. If you experiment, isolate it in a small project, pin the SDK in global.json, and enable the preview language version deliberately:

xml

Do not make preview syntax the foundation of a broadly distributed library without a migration plan. Tooling, metadata shape, serialization behavior, and diagnostic rules can evolve. Public APIs are expensive to revise after consumers depend on them.

The safer production takeaway is architectural rather than syntactic:

  • Use records for immutable cases with distinct payloads.
  • Prefer a finite result model over nullable return values plus undocumented exceptions.
  • Reserve open base classes for genuine extensibility points.
  • Keep a discard arm in switches only when unknown variants are truly acceptable.
  • Let a new state force an explicit review of its downstream behavior.

A sharper vocabulary for domain design

C# unions and closed classes are not an argument to replace every interface or class hierarchy. They are a tool for a specific and common shape of problem: a domain with known alternatives and meaningful transitions between them.

When a model is open, design for extension. When it is closed, design for completeness.

That distinction sounds small, but it changes how code evolves. A new outcome is no longer just another subclass that silently reaches a default branch. It becomes a domain event that the compiler can surface across the system.

For C# developers building APIs, workflows, and business-heavy applications, that is the real promise of unions: not fewer lines of code, but fewer states that can exist without anyone having decided what they mean.

Comments (0)

No comments yet. Be the first to share your thoughts!

C# Unions and Closed Classes: A New Model for State in .NET 11