.NET 11 Preview 6: C# Unions Across the Stack and CSRF On by Default
C# unions now compile without boilerplate and flow through System.Text.Json, ASP.NET Core and OpenAPI, while automatic CSRF protection changes which cross-origin requests apps reject.
Umut Korkmaz
Published: 4 min read
On this page6
Microsoft shipped .NET 11 Preview 6 on July 14, 2026. Two items stand out to me as someone who reviews a lot of .NET API code. C# unions no longer need hand-written compiler scaffolding, and ASP.NET Core now turns on cross-origin (CSRF) protection by default. The first changes how we model results. The second can change what an app rejects without any change to its own code.
Unions without the scaffolding
Preview 5 introduced union declarations and union patterns, but you had to write the compiler support types (UnionAttribute and IUnion) yourself. According to the C# release notes, Preview 6 ships them in System.Runtime.CompilerServices, so public union Pet(Dog, Cat); compiles with nothing extra. One rule change is worth knowing: a not pattern now applies to the union value itself, not the case inside it.
The caveat: unions are still a preview feature behind <LangVersion>preview</LangVersion>, and the notes say to expect the surface to keep evolving. Extension indexers, which arrive in this release and round out the extension members feature from .NET 10, sit behind the same preview flag.
Results you can see in the signature
For expected failures, a lot of code today either throws or returns a hand-rolled Result<T> with a nullable error. A union lets the signature say exactly what can come back. Here is a sketch using the release-notes syntax, not production code:
public record class OrderPlaced(int Id);
public record class OutOfStock(string Sku);
public record class InvalidOrder(string[] Errors);
public union PlaceOrderResult(OrderPlaced, OutOfStock, InvalidOrder);
static IResult ToHttp(PlaceOrderResult result) => result switch
{
OrderPlaced(var id) => TypedResults.Created($"/orders/{id}"),
OutOfStock(var sku) => TypedResults.Conflict(sku),
InvalidOrder(var errors) => TypedResults.BadRequest(errors)
};
What convinced me is how much of the stack already understands it. System.Text.Json serializes the active case directly, so a Pet holding Dog("Rex") becomes {"Name":"Rex"}. The ASP.NET Core notes list union support in:
- Minimal API bodies and return types, including
Task<Union>,IAsyncEnumerable<Union>andResults<T1, T2> - MVC and Razor Pages JSON request and response bodies
- SignalR on the JSON hub protocol
- Blazor parameters, JS interop and persisted state
In review, the part I would slow down on is the wire format. Union cases carry no $type discriminator, and OpenAPI describes a union return as anyOf over the case schemas. When two cases serialize to the same JSON shape, the notes say to disambiguate them with a [JsonUnion] classifier. Whatever generates your TypeScript types has no tag to narrow on, so the cases need shapes that can be told apart. That is also why I would still map to status codes at the edge, as above. Returned directly, the union is just the response body, so the status code does not vary by case.
Current limits:
- JSON bodies and responses only. No binding from query, route, headers or form.
- The MessagePack and Newtonsoft.Json hub protocols do not support unions.
- Swashbuckle and NSwag do not recognize unions yet.
CSRF protection that turns itself on
Apps built with WebApplication.CreateBuilder now reject unsafe cross-origin requests with no configuration. The check uses the browser's Sec-Fetch-Site and Origin headers and covers Minimal APIs, MVC, Razor Pages and Blazor. Same-origin requests, user-initiated navigations and non-browser clients pass. A cross-origin browser request that tries to consume a form is rejected.
It is a good default that can break things quietly. The first place I would look is payments: 3-D Secure return URLs often receive a form POST submitted from the bank's or provider's page, which matches what the notes say gets rejected. I have not tested this with any provider, so treat it as a check, not a conclusion. Partner sites that post forms into your app belong on the same list.
The escape hatches:
.DisableAntiforgery()for Minimal APIs[IgnoreAntiforgeryToken]for MVC- a
DisableCsrfProtectionconfiguration key for the whole app - a custom
ICsrfProtection
I would rather see a short, reviewed list of per-endpoint opt-outs than the app-wide switch.
OpenAPI 3.2 and the rest
Generated documents now target OpenAPI 3.2 by default. Only the default changes, and you can set an earlier version explicitly for tooling that has not adopted 3.2. Check client generators, gateways and contract tests that read the document.
SignalR connections can now refresh authentication without dropping when the token expires. You turn it on per hub with EnableAuthenticationRefresh. In this preview only the .NET client supports it. JavaScript/TypeScript and Azure SignalR Service support are in progress, so a React Native client does not get it yet.
Elsewhere in the release: test templates for xUnit v3 and NUnit, multi-arch container publishing with Podman, and EF Core keys and indexes that can traverse complex-type properties.
What I'm doing about it
Production stays on .NET 10. On a preview branch I plan to:
- Model one internal endpoint's outcomes as a union and read the generated OpenAPI and TypeScript types before forming an opinion.
- Check that Meridian's source generators handle union-typed requests and responses before GA.
- List every endpoint that legitimately receives cross-origin browser form posts, starting with payment callbacks.
- Pin the OpenAPI version wherever downstream tooling is not on 3.2.
Unions are the C# change I care most about this cycle, and this is the first preview where the framework, the serializer and the web stack treat them as one design. It is still a preview, which makes now the time to find the rough edges.
Sources
Primary references used for this post.