Back to Blog

.NET 11 RC1: Stable C# Unions, Reproducible Images, No Rush

.NET 11 RC1 ships with a go-live license and stable C# 15 unions. What go-live really means, why reproducible container images matter, and what I would check first.

Umut Korkmaz

Published: 4 min read

Microsoft shipped .NET 11 Release Candidate 1 on September 8. It is the first release candidate, and it comes with a go-live support license, so Microsoft supports it in production. The headline is that C# 15 is now the default language version for .NET 11 projects and union types no longer need a preview flag. The change I expect to matter more over time is quieter: container publishing can now produce reproducible images.

I run .NET 10 in production, so the question for me this week is not whether RC1 is good. It is what a team should actually do with it now.

What go-live does and does not mean

The .NET support policy describes go-live releases as supported in production and typically the release candidate builds just before GA. That is useful, but it is support for a moving target: the same page lists RC1's own end of support as October 13, 2026. If you ship on RC1, you are committing to take each following build as it lands.

The same page points at the deadline that matters more: .NET 8 and .NET 9 both reach end of support on November 10, 2026, while .NET 10 is LTS until November 14, 2028. If you still have services on .NET 8, the upgrade you owe your team before November 10 is to .NET 10, not to an RC.

C# 15 unions are no longer a preview feature

According to the C# release notes for RC1, C# 15 is selected by default for projects targeting .NET 11 and stabilizes the features from the preview cycle: collection expression arguments, union types, non-virtual static interface members, closed class hierarchies, labeled break and continue, and extension indexers.

Unions are the one I expect to change how I model domains. Payment flows are a good fit, because the outcomes form a closed set and each one needs its own handling:

public record Approved(string TransactionId);
public record Requires3ds(string RedirectUrl);
public record Declined(string Reason);

public union PaymentOutcome(Approved, Requires3ds, Declined);

static IResult ToHttp(PaymentOutcome outcome) => outcome switch
{
    Approved(var id)     => Results.Ok(id),
    Requires3ds(var url) => Results.Redirect(url),
    Declined(var reason) => Results.UnprocessableEntity(reason)
};

Today that shape usually lives as a base class plus a status enum, or a hand-rolled result type. Two details matter for teams with their own tooling. The libraries release notes list JSON closed-type polymorphism and union support, and Roslyn now exposes ITypeSymbol.UnionCaseTypes so analyzers and other compiler-based tools can see a union's cases. If you maintain a source generator, as I do with Meridian, that is the API to read before users start asking.

One caveat: the Unsafe Evolution work is still a preview feature, independent of C# 15, and still needs <LangVersion>preview</LangVersion> plus a feature flag. "C# 15 is stable" does not mean everything in the compiler is.

Reproducible container images

This is the change I would push hardest on. Per the SDK release notes, publishing the same app twice could previously produce different image digests because timestamps, archive headers and directory enumeration order varied. Setting SOURCE_DATE_EPOCH to a stable Unix timestamp now makes independent publishes of the same inputs produce the same digest:

dotnet publish /t:PublishContainer \
  -p:SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)"

Using the commit time means the timestamp comes from the source rather than the build machine's clock. With the other inputs pinned, that turns "we believe this image came from that commit" into something a second pipeline can check by rebuilding and comparing digests, which is what supply-chain attestations are supposed to rest on. Remote registry publishes also skip layer processing when the computed manifest already exists in the destination repository, while still applying the requested tags; ContainerPushNoCache=true bypasses that check.

Smaller things to check on Monday

  • CI configuration. A listed SDK breaking change: the Configuration environment variable now supplies the default for --configuration. A pipeline that sets it ambiently may build Release where it used to build Debug. Pass -c explicitly.
  • SignalR token refresh. The authentication refresh APIs are finalized: a client can replace an expiring access token without dropping the connection, the server opts in per hub with EnableAuthenticationRefresh, and the TypeScript client gets withAuthenticationRefresh. If you tried Preview 7, the .NET client callbacks moved to the HubConnection.AuthenticationRefreshed and AuthenticationRefreshFailed events.
  • Test runs. dotnet test on Microsoft.Testing.Platform adds a run-level --timeout (exit code 3) and --maximum-failed-tests (exit code 13), plus --results-directory-layout per-module so reports stop overwriting each other.
  • Crash reports. The in-process crash reporter now works on Linux and macOS through DOTNET_EnableCrashReport=1 when minidumps are not enabled.

What I'm doing about it

Production stays on .NET 10. What I want from RC1 is information, not a deployment:

  1. Add a net11.0 CI leg on a branch to surface breaking changes early, with -c set explicitly.
  2. Try unions in one bounded context where the outcomes are genuinely closed, and keep them out of public NuGet APIs until GA.
  3. Add the SOURCE_DATE_EPOCH step to our container pipeline template now, so it is ready the day we move.

Go-live means you are allowed to ship it. It does not mean you have to.

Sources

Primary references used for this post.

  1. devblogs.microsoft.com/dotnet/dotnet-11-rc-1 (opens in a new tab)
  2. github.com/dotnet/core/blob/main/release-notes/11.0/preview/rc1/csharp.md (opens in a new tab)
  3. github.com/dotnet/core/blob/main/release-notes/11.0/preview/rc1/libraries.md (opens in a new tab)
  4. github.com/dotnet/core/blob/main/release-notes/11.0/preview/rc1/sdk.md (opens in a new tab)
  5. github.com/dotnet/core/blob/main/release-notes/11.0/preview/rc1/aspnetcore.md (opens in a new tab)
  6. github.com/dotnet/core/blob/main/release-notes/11.0/preview/rc1/runtime.md (opens in a new tab)
  7. dotnet.microsoft.com/platform/support/policy/dotnet-core (opens in a new tab)

About the author

Umut Korkmaz

Lead Software Engineer

I'm a senior software engineer in Istanbul. Here I write about the tools and releases I've shipped to production myself, and what they taught me along the way.

Picked by shared topics.