Back to Csharplang

C# Language Design Meeting for February 11th, 2026

meetings/2026/LDM-2026-02-11.md

latest6.5 KB
Original Source

C# Language Design Meeting for February 11th, 2026

Agenda

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, 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.