# C# Language Design Meeting for February 11th, 2026

## Agenda

- [Union patterns update](#union-patterns-update)

## Quote of the Day

- "My Result type is a GC hole." "I'd put that on a t-shirt."

## Discussion

### Union patterns update

Champion issue: https://github.com/dotnet/csharplang/issues/9662  
Spec: https://github.com/dotnet/csharplang/blob/0e3a99401b0fb0fbde2ad976e63332c4d603e6d5/proposals/unions.md  
Working group notes: https://github.com/dotnet/csharplang/blob/0e3a99401b0fb0fbde2ad976e63332c4d603e6d5/meetings/working-groups/discriminated-unions/union-patterns-update.md

The discriminated unions working group returned to LDM with an updated proposal for union member patterns on custom union
types. In the [previous discussion](./LDM-2026-02-04.md#union-patterns-update), the LDM had expressed a preference for
explicit marking of case types via a `UnionCase` attribute, rather than having them be implicitly recognized. The working
group took that feedback and explored the attribute approach further, but found that attributes still lead to significant
complexity in certain real-world scenarios.

The core problem is that existing types that want to become union types may already have members that would be recognized
as union members, but that serve a different purpose. In some cases, these members even clash with what the union feature
needs, so an attribute cannot resolve the conflict. A common real-world example is a `Result`-like union type that has a
`Value` property meaning something different from what the union pattern expects.

Instead of attributes, the working group proposed a delegation approach: a union type can optionally delegate all of its
union members to a nested interface called `IUnionMembers` that the union type implements. Since constructors cannot
appear in interfaces, factory methods named `Create` are used instead. The interface can include an abstract `Value`
property that is explicitly implemented on the union type, keeping it off the public surface area. The compiler can then
use constrained calls to avoid the overhead of interface dispatch.

The walkthrough included several real-world union-like types to demonstrate how the approach works in practice:

- **`SumType`** (from Roslyn/LSP): This type already happens to expose exactly the union pattern. It has constructors
  for each case type and an `object?`-returning `Value` property. It can simply be marked with the `[Union]` attribute
  and everything works. It was noted that this type also has `TryGet` methods but they use different names, so they do
  not accidentally match the non-boxing access pattern, which is appropriate since the type stores values in a boxed
  form anyway.

- **`OneOf`** (a popular third-party library): This type has a `Value` property of the right shape but does not expose
  constructors. This is a case where delegation is needed: a nested `IUnionMembers` interface can be added with `Create`
  factory methods for each case type, plus a `Value` property. The type would then implement the interface, and since the
  existing `Value` property already satisfies the interface member, no additional implementation is needed.

- **`Results`** (from ASP.NET Minimal APIs): This type has a property that serves the same purpose as `Value` but has
  a different name and a more specific return type (`IResult` rather than `object?`). This is another case where
  delegation is needed. This example also raised a question about whether the `Value` property should be allowed to
  have a more specific return type in general, which could enable better nullability analysis and more efficient code.
  That question was noted but deferred to a separate discussion.

It was observed that all three real-world types already implement their own implicit conversion operators, which would
shadow the compiler-generated union conversion in all cases. This means the `Create` factory methods on the
`IUnionMembers` interface would primarily serve to inform the compiler about the case types, rather than being called
at runtime. This prompted a brief discussion about "consume-only" unions, where the type does not want to expose the
ability for users to create union values directly. A possible future solution was sketched out involving a separate
method name (e.g., `CaseTypeOf`) that returns `void` and only lists the case types, but this is not proposed for now.

A question was raised about whether the non-boxing access pattern (`TryGetValue` methods) could actually be harmful if
applied to types that already store values in a boxed form. The LDM agreed that guidance will be needed to explain when
it is appropriate to expose the non-boxing pattern, but the specifics of the compiler heuristics for choosing between
`Value` and `TryGetValue` are not yet fully worked out.

Regarding the `IUnionMembers` interface, it was confirmed that this interface must be defined as a nested type directly
within the union type, not inherited from a base type. An inherited `IUnionMembers` interface would have the wrong type
parameters in its factory methods and would not be useful. Even though the language generally picks up members from base
types in lookup, this particular interface is scoped to only the declaring type.

There was a question about whether static non-DIM (default interface method) members in interfaces are supported on
older runtimes. The C# compiler currently blocks static members in interfaces when the target runtime does not support
DIMs, even though non-virtual static members in interfaces have always been supported by the runtime independently of
DIM support. The working group followed up and confirmed that static non-virtual interface members have been properly
exercised on the Desktop CLR: C++/CLI has offered them at the language level all along, and F# has also been targeting
them for some time. So this is not a concern.

A concern was raised about metadata bloat from the additional interface type. The working group noted that they had
considered an even simpler approach where every union, including those from the `union` keyword, always delegates to an
interface, but rejected it because generating two types for every union declaration felt excessive. The working group
followed up on this concern as well, and confirmed that the nested-interface approach is not concerning from a metadata
bloat perspective, given how uncommon the delegation scenario is expected to be.

#### Conclusion

The LDM is in favor of the `IUnionMembers` delegation approach as proposed by the working group. We will move forward
with the proposal as written.
