Back to Csharplang

C# Language Design Meeting for February 4th, 2026

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

latest7.9 KB
Original Source

C# Language Design Meeting for February 4th, 2026

Agenda

Quote of the Day

  • "The distant past. You know, late summer or early fall."

Discussion

Discriminated unions patterns

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

Today, we went over a few open questions in unions.

Null ambiguity in constructor selection

When a union has multiple reference type case types, passing null creates ambiguity about which constructor to use. For example, with Pet(Dog) and Pet(Bird?) constructors, null could apply to either.

Currently, the compiler picks an arbitrary applicable constructor when there is ambiguity, based on the assumption that well-formed unions have constructors that behave identically for the same input. However, this can lead to surprising behavior: if the compiler picks the Dog constructor but Bird? is the one intended to accept null, a nullability warning will be issued even though the union author intended null to be valid.

We discussed several approaches:

  1. Using standard overload resolution betterness rules to select among applicable constructors
  2. Requiring the user to be explicit when multiple cases could apply (warning or error on ambiguity)
  3. Ensuring nullability annotations factor into the decision

We liked options 1 and 2 here. Option 3 is not how the compiler works today, and doesn't solve the scenario in nullable disabled locations. There are also scenarios where null isn't involved, such as a union of two interfaces and a type that implements both. We also brought up operator conversions, as union conversions are very similar to user-defined operators. We like that analogy, and want to apply similar rules where applicable.

Conclusion

The working group will consider requiring explicit disambiguation when ambiguity cannot be resolved through standard betterness rules, similar to how conversion operators are resolved.

Marking unions with an attribute instead of IUnion interface

The current design uses the IUnion interface to indicate that a type should be treated as a union. This causes problems:

  • A type cannot implement IUnion without becoming a union (it might just want runtime participation)
  • Derived types of a union type automatically become unions themselves, which may not be intended and raises awkward questions about what their case types should be
  • It conflates the runtime interface (useful for generic code that handles unions) with the compiler recognition mechanism

The proposal is to use a [Union] attribute instead, with the compiler looking for members by shape rather than through the interface. The IUnion and IUnion<TUnion> interfaces would still exist for runtime scenarios (such as generic code that queries whether something is a union or accesses its value), but they would be optional and not required for compiler recognition.

Conclusion

Approved. The design will change from requiring IUnion interface implementation to using a [Union] attribute for compiler recognition. Members will be looked up by shape (constructors with case type parameters, a Value property).

Factory method support

Constructors have limitations: they cannot be defined on a type other than the one they construct, and they cannot be inherited or delegated. Static factory methods provide more flexibility for scenarios involving type hierarchies, object pooling, or delegating union member definitions to a separate type.

The proposal is to allow public static Create methods as an alternative to constructors for defining case types. When factory methods are present, they take precedence over constructors.

There was significant concern about implicit recognition of factory methods based on shape alone. A method like Parse(string) returning the union type could be unintentionally picked up as defining a string case type. Even if we restrict to just a single name (Create), we don't have enthusiasm for the magic shape recognition among different overloads here.

The LDM expressed strong support for requiring explicit markers rather than relying on implicit shape matching. The proposal of a [UnionCase] attribute was well-received: authors would mark constructors, factory methods, or even user-defined implicit conversion operators that should contribute to the union's case types.

A follow-up question arose: should the [UnionCase] attribute be required on everything, including constructors, or should there be a simple default mode where constructors work implicitly? Two positions emerged:

  1. Attributes on everything: Any member used by the compiler for union behavior must be marked with [UnionCase].

  2. Default behavior for constructors: If a union type has only constructors (no factory methods or conversion operators), they work implicitly without attributes. Once you want any customization (factory methods, conversion operators, or selective inclusion of constructors), you must use [UnionCase] on all participating members.

A read-of-the-room vote showed preference for the second approach: allow the simple constructor-only case to work without attributes, while requiring explicit marking for any advanced scenarios. However, we want the working group to take a look at the implications and come back with a concrete proposal and recommendations.

Conclusion

Move forward with the [UnionCase] attribute approach. The working group will produce a concrete proposal with specific rules. There is no requirement for a specific factory method name when the attribute is used.

Union member providers

This proposal addresses scenarios where the union's desired behavior conflicts with its public surface area. Two cases were identified:

  1. The type has members that would be incorrectly recognized as union members
  2. The type needs union members but does not want them in its public API

The idea was to allow delegating union member lookup to a separate "provider" type (e.g., a nested interface).

Given the decision to use explicit [UnionCase] attributes, the first scenario is largely addressed: members won't be picked up unless explicitly marked. For the second scenario, user-defined implicit conversion operators (if supported) could address types like Result<T> that expose conversions but not constructors. However, we don't have good examples of what could actually need this ability. Given that, we will simply table this addition for now, and come back when we have concrete scenarios to address.

Conclusion

This feature will be saved for later. The explicit attribute approach addresses the primary motivating scenarios. If compelling use cases emerge that require full delegation to a provider type, the design space remains open.

General principle established

Throughout the discussion, a general principle emerged: the LDM is comfortable with some implicit behavior for well-known members (like recognizing a property named Value), but prefers explicit marking for case type definitions due to the inherent variability and potential for false positives. It's possible further consideration of real use cases may cause us to want to go even further into the explicit territory (explicitly marking Value or the TryValue methods, for example), but the working group will explore this space.