meetings/2026/LDM-2026-02-11.md
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, 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.
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.