Skip to content

Retire standalone C# unions article; document where unions aren't supported#37276

Open
danroth27 wants to merge 5 commits into
mainfrom
danroth27/unions-in-context
Open

Retire standalone C# unions article; document where unions aren't supported#37276
danroth27 wants to merge 5 commits into
mainfrom
danroth27/unions-in-context

Conversation

@danroth27

@danroth27 danroth27 commented Jun 21, 2026

Copy link
Copy Markdown
Member

Closes #37275. Addresses #37274.

Deletes aspnetcore/fundamentals/unions.md and its TOC entry. No redirect — the article only shipped in preview docs.

Principles

  1. Integrate ASP.NET Core details about C# unions support in-line with the related features.
  2. Avoid documenting System.Text.Json specific behaviors and usage patterns. It should be sufficient to state where we use STJ and reference the STJ docs.
  3. Avoid "unions work here" callouts on every JSON-touching article. Normal JSON serialization and C# rules should be implied unless otherwise stated.
  4. Do document where unions aren't supported but a reader might expect them to be.

Changes

Adds short, moniker-gated (>= aspnetcore-11.0) notes covering where C# unions aren't supported in ASP.NET Core but a reader might expect them to be:

Article Note
fundamentals/minimal-apis/includes/parameter-binding8-10.md Route, query, header, form (body JSON only)
mvc/models/model-binding.md [FromQuery]/[FromRoute]/[FromForm]/[FromHeader] ([FromBody] only)
signalr/hubs.md MessagePack and Newtonsoft.Json hub protocols (JsonHubProtocol only)
blazor/fundamentals/navigation.md [SupplyParameterFromQuery] and [SupplyParameterFromForm]
blazor/fundamentals/routing.md @page route parameters
blazor/components/index.md Razor string-literal attribute shortcut (links dotnet/razor#13188)

Net change: -275 lines.

Verification

Verified against .NET 11 Preview 6:


Internal previews

📄 File 🔗 Preview link
aspnetcore/blazor/components/index.md aspnetcore/blazor/components/index
aspnetcore/blazor/fundamentals/navigation.md aspnetcore/blazor/fundamentals/navigation
aspnetcore/blazor/fundamentals/routing.md aspnetcore/blazor/fundamentals/routing
aspnetcore/mvc/models/model-binding.md aspnetcore/mvc/models/model-binding
aspnetcore/signalr/hubs.md aspnetcore/signalr/hubs
aspnetcore/toc.yml aspnetcore/toc

Addresses #37275 (retire fundamentals/unions.md and integrate per-area)
and #37274 (Blazor-specific notes in the relevant Blazor articles).

C# unions are a language feature whose ASP.NET Core surface is small and
flows through existing serialization-driven features. A standalone article
under Fundamentals overstates the concept and forces readers to leave the
article they were already in.

This change:

* Deletes aspnetcore/fundamentals/unions.md and removes its TOC entry.
* Adds a redirect to fundamentals/minimal-apis/responses (the most common
  starting point for union usage).
* Adds one short, moniker-gated (>= aspnetcore-11.0) note in each of:
  - fundamentals/minimal-apis/responses.md
  - web-api/action-return-types.md
  - signalr/hubs.md (JsonHubProtocol only)
  - fundamentals/openapi/aspnetcore-openapi.md (anyOf schema)
  - blazor/components/index.md (union component parameters + Razor
    literal-attribute workaround, tracked at dotnet/razor#13188)
  - blazor/components/dynamiccomponent.md (boxing rule)
  - blazor/state-management/prerendered-state-persistence.md
    (JsonSerializerContext doesn't flow into the unions deserializer)

Each note links to the System.Text.Json unions article for the
serialization behavior (case selection, classifiers, ambiguity) that's
independent of ASP.NET Core.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@wadepickett

wadepickett commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

@danroth27, I know this is still in draft, but just a note to save you time: I can clean the moniker versioning /build issues for all articles in the PR, once you feel like the updates are otherwise how you want them. I'll fix in review.

What is happening:
For the moniker versioning issues, there are some topics that have:
:::moniker range=">= aspnetcore-10.0"

This is conflicting with the new sections that are dropped within that moniker block that are specifying: >= aspnetcore-11.0 since now there are two ranges competing for trying to handle any version equesl or greater than 11.

Usually we would create a new copy of the article that is for >= 11 going forward, which allows us to avoid any nested versioning speghetti and give a clear article dedicated to a version that can be archived or retired later.

Follows reviewer feedback: assume STJ rules apply wherever JSON
serialization is used. Don't repeat union behavior in every JSON
surface; only call out the cases where unions are unsupported but a
reader might expect them to work.

Reverted:
* fundamentals/minimal-apis/responses.md
* web-api/action-return-types.md
* fundamentals/openapi/aspnetcore-openapi.md
* blazor/state-management/prerendered-state-persistence.md
* The Toast example in blazor/components/index.md
* The SaveOutcome example in signalr/hubs.md

Kept / restructured (all moniker-gated >= aspnetcore-11.0):
* blazor/components/index.md: Razor string-literal shortcut doesn't
  apply to union-typed parameters; relocated next to the existing
  string-literal documentation.
* blazor/components/dynamiccomponent.md: DynamicComponent boxing rule
  for union parameters; slimmed to a NOTE block.
* signalr/hubs.md: slimmed to a NOTE that MessagePack and
  Newtonsoft.Json hub protocols don't support unions.

Added explicit not-supported notes:
* fundamentals/minimal-apis/includes/parameter-binding8-10.md: unions
  are supported only as body (JSON); not in route/query/header/form.
  Links dotnet/aspnetcore#66648.
* mvc/models/model-binding.md: same callout for [FromBody] vs other
  binding sources. Links dotnet/aspnetcore#66648.
* blazor/fundamentals/navigation.md: [SupplyParameterFromQuery] and
  [SupplyParameterFromForm] don't support unions. Links #66648.

Redirect updated to point at fundamentals/minimal-apis/parameter-binding
(where the most relevant ASP.NET Core-specific not-supported note lives).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danroth27 danroth27 changed the title Retire standalone C# unions article; distribute in-context Retire standalone C# unions article; document where unions aren't supported Jun 24, 2026
Copilot AI added 2 commits June 24, 2026 16:43
…#66648 links

Follow-up to reviewer feedback:

* Drop the redirect entry. The unions.md article only ever shipped in
  preview docs, so no production redirect target is needed.
* Revert blazor/components/dynamiccomponent.md. The DynamicComponent
  boxing behavior follows normal C# rules and produces a useful runtime
  error message; union-typed parameters won't be common enough to
  warrant a dedicated note.
* Revert ms.date on fundamentals/minimal-apis/parameter-binding.md
  (only the include file changed; parent prose is unchanged).
* Remove dotnet/aspnetcore#66648 references from the three non-body
  binding notes. Future support isn't certain enough to advertise.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Blazor route parameters parse route segments as strings without JSON
parsing, so a [Parameter] typed as a C# union can't be populated from a
route segment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danroth27

Copy link
Copy Markdown
Member Author

@DeagleGross The existing article does a great job highlighting the new feature and where it applies. I think this kind of content would work better as a blog post on devblogs.microsoft.com/dotnet timed to .NET 11 instead of as a standalone doc. I'm happy to work with you to get that published.

@danroth27 danroth27 marked this pull request as ready for review June 25, 2026 00:11
@danroth27 danroth27 requested a review from guardrex as a code owner June 25, 2026 00:11
* signalr/hubs.md: close the outer >= aspnetcore-8.0 moniker block
  around the new >= aspnetcore-11.0 insert so the inner block parses
  correctly. Fixes 'No moniker-end found for >= aspnetcore-11.0' warning.
* release-notes/aspnetcore-11/includes/csharp-unions-preview-6.md:
  rewrite to match the PR principles. Drops the broken xref to the
  deleted unions.md, links to the C# language reference and STJ docs
  instead of duplicating STJ behavior, and points readers to the in-context
  not-supported notes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

:::moniker range=">= aspnetcore-11.0"

The string-literal shortcut applies only to parameters declared as `string`. A parameter declared as a [C# union type](/dotnet/csharp/whats-new/csharp-14#union-types) — even one whose cases include `string` — isn't a `string`-typed parameter, so the attribute value must be a C# expression. Use the `@` prefix, for example `Message="@("Saved.")"`, tracked at [dotnet/razor#13188](https://github.com/dotnet/razor/issues/13188).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The string-literal shortcut applies only to parameters declared as `string`. A parameter declared as a [C# union type](/dotnet/csharp/whats-new/csharp-14#union-types)even one whose cases include `string`isn't a `string`-typed parameter, so the attribute value must be a C# expression. Use the `@` prefix, for example `Message="@("Saved.")"`, tracked at [dotnet/razor#13188](https://github.com/dotnet/razor/issues/13188).
The string-literal shortcut applies only to parameters declared as `string`. A parameter declared as a [C# union type](/dotnet/csharp/whats-new/csharp-14#union-types), even one whose cases include `string`, isn't a `string`-typed parameter, so the attribute value must be a C# expression. Use the `@` prefix, for example `Message="@("Saved.")"`. This requirement is tracked at [dotnet/razor#13188](https://github.com/dotnet/razor/issues/13188).

Very minor: Split to two sentances to make this a little more readable.

@guardrex guardrex Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If for updates at .NET 11, lead the paragraph with this ...

<!-- UPDATE 11.0 - Remove the following paragraph per resolution of the PU issue -->

... and for the link convention, use this format ...

- [dotnet/razor#13188](https://github.com/dotnet/razor/issues/13188)
+ [Razor: extend literal-attribute shortcut to component parameters typed as a C# union with a string case (`dotnet/razor` #13188)](https://github.com/dotnet/razor/issues/13188)

Here's a full suggestion for these, including Wade's suggestions. The issue is about making a change to the framework to address this, so I changed "requirement" to our 'for more info' lead-in ...

Suggested change
The string-literal shortcut applies only to parameters declared as `string`. A parameter declared as a [C# union type](/dotnet/csharp/whats-new/csharp-14#union-types) — even one whose cases include `string` — isn't a `string`-typed parameter, so the attribute value must be a C# expression. Use the `@` prefix, for example `Message="@("Saved.")"`, tracked at [dotnet/razor#13188](https://github.com/dotnet/razor/issues/13188).
<!-- UPDATE 11.0 - Remove the following paragraph per resolution of the PU issue -->
The string-literal shortcut applies only to parameters declared as `string`. A parameter declared as a [C# union type](/dotnet/csharp/whats-new/csharp-14#union-types), even one whose cases include `string`, isn't a `string`-typed parameter, so the attribute value must be a C# expression. Use the `@` prefix, for example `Message="@("Saved.")"`. For more information, see [Razor: extend literal-attribute shortcut to component parameters typed as a C# union with a string case (`dotnet/razor` #13188)](https://github.com/dotnet/razor/issues/13188).


For more information, see [Use C# union types in ASP.NET Core](xref:fundamentals/unions).
<!-- TODO: System.Text.Json union APIs (JsonUnionAttribute, JsonTypeClassifier) are new in .NET 11; update to <xref:> once published to dotnet-api-docs. -->
Unions aren't supported for non-body binding sources such as route values, query strings, headers, and form fields. No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Unions aren't supported for non-body binding sources such as route values, query strings, headers, and form fields.
Union types aren't supported for non-body binding sources such as route values, query strings, headers, and form fields.

Very minor. Consistant full term use of "Union types" for clarity.

@wadepickett wadepickett left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danroth27, PR Approved. Looks great! Inline I noted a couple very minor items you could take or leave.

@guardrex guardrex left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello from PTO! 👋😄

Per the style manual, there should be no spaces around em dashes ...

https://learn.microsoft.com/style-guide/punctuation/dashes-hyphens/

I placed the suggestions to update them.

:::moniker range=">= aspnetcore-11.0"

> [!NOTE]
> [C# union types](/dotnet/csharp/whats-new/csharp-14#union-types) are supported only as the body (as JSON). Non-body sources — route values, query string, headers, and form values — bind string values without JSON parsing, so they can't dispatch to a union case.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> [C# union types](/dotnet/csharp/whats-new/csharp-14#union-types) are supported only as the body (as JSON). Non-body sourcesroute values, query string, headers, and form valuesbind string values without JSON parsing, so they can't dispatch to a union case.
> [C# union types](/dotnet/csharp/whats-new/csharp-14#union-types) are supported only as the body (as JSON). Non-body sourcesroute values, query string, headers, and form valuesbind string values without JSON parsing, so they can't dispatch to a union case.

:::moniker range=">= aspnetcore-11.0"

> [!NOTE]
> [C# union types](/dotnet/csharp/whats-new/csharp-14#union-types) are supported only with `[FromBody]`. The other binding sources — `[FromQuery]`, `[FromRoute]`, `[FromForm]`, and `[FromHeader]` — bind string values without JSON parsing, so they can't dispatch to a union case.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> [C# union types](/dotnet/csharp/whats-new/csharp-14#union-types) are supported only with `[FromBody]`. The other binding sources`[FromQuery]`, `[FromRoute]`, `[FromForm]`, and `[FromHeader]`bind string values without JSON parsing, so they can't dispatch to a union case.
> [C# union types](/dotnet/csharp/whats-new/csharp-14#union-types) are supported only with `[FromBody]`. The other binding sources`[FromQuery]`, `[FromRoute]`, `[FromForm]`, and `[FromHeader]`bind string values without JSON parsing, so they can't dispatch to a union case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Docs] Retire standalone C# unions article; integrate into existing articles in context

5 participants