.NET 11 Preview 5: The Features That Actually Matter

.NET 11 Preview 5: The Features That Actually Matter

.NET 11 Preview 5: The Features That Actually Matter

Every November the .NET release train pulls into the station, and every November the "what's new" posts read like someone piped the changelog through a thesaurus. I've sat through fifteen of these cycles now. Most release summaries answer the wrong question. The question isn't "what changed?" It's "what should I change?"

So here's my opinionated filter on .NET 11 Preview 5. Out of hundreds of items in the official overview, I'm covering the ones that will genuinely make your day-to-day easier. I'll say the headline out loud early: Runtime Async is no longer a preview feature, and EF Core 11 is the strongest data-access release in years.

The theme of .NET 11, as I read it, is subtraction. Less async overhead. Less SQL noise. Fewer migration commands. Fewer sidecar services. This is the first release in a while where the runtime, the ORM, and the CLI all converge on removing ceremony instead of piling on capability.

One note on timing: we're at Preview 5, with GA expected November 2026. That makes this the planning window. Run your test suite against the preview now; don't ship it to production yet.


Runtime & Platform: Async Finally Grows Up

Runtime Async is the headline of this entire release. The async/await machinery, the state machines the compiler has been generating since C# 5, moves into the runtime itself. As of Preview 5 you no longer need EnablePreviewFeatures when targeting net11.0. It's just on.

Why should you care? Two concrete payoffs.

First: lower per-await overhead. The runtime can suspend and resume methods natively instead of dragging a heap-allocated state machine around for every async method. Free performance in the hottest path of every modern .NET app.

Second, and this is the one I actually care about: readable stack traces. If you've ever debugged a production incident through fifteen frames of MoveNext() soup, you know the pain. Runtime Async gives you traces that look like the code you wrote:

// .NET 10 — the classic async stack trace soup
System.InvalidOperationException: Order 4231 has no shipping address
   at OrderService.<ValidateAsync>d__7.MoveNext()
--- End of stack trace from previous location ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at OrderService.<SubmitAsync>d__4.MoveNext()
--- End of stack trace from previous location ---
   at OrdersController.<Post>d__2.MoveNext()

// .NET 11 with Runtime Async — what you actually wanted
System.InvalidOperationException: Order 4231 has no shipping address
   at OrderService.ValidateAsync(Order order)
   at OrderService.SubmitAsync(Order order)
   at OrdersController.Post(OrderRequest request)

My take: this is the biggest invisible win in the release. You upgrade, you change nothing, and your exception logs become readable. That's hours of incident-response time handed back to every team, every year.

The JIT also picks up its usual round of free wins: better bounds check elimination, switch expression folding, and Arm SVE2 intrinsics for the Arm64 crowd. None of these need code changes. They're the "upgrade and your p99 drops" category.

One practical gotcha before you get too excited: .NET 11 ships updated minimum hardware requirements for x86/x64 and Arm64. Before GA, audit your oldest build agents and that on-prem box in the corner nobody wants to talk about. Better a ticket now than a broken pipeline in November.


Libraries: The BCL Quietly Eats Your NuGet Dependencies

Every release, the BCL absorbs another batch of community packages. .NET 11 continues the tradition, and "delete a package, use the platform" is exactly the kind of subtraction I'm here for.

Process run-and-capture helpers

Shelling out to a process and capturing its output has been a fifteen-line ritual of ProcessStartInfo, redirected streams, and the deadlock-shaped trap of reading stdout and stderr in the wrong order. .NET 11 adds run-and-capture helpers to System.Diagnostics.Process that collapse the whole dance:

// The old ritual — and yes, this is the *careful* version
var psi = new ProcessStartInfo("git", "rev-parse HEAD")
{
    RedirectStandardOutput = true,
    RedirectStandardError = true,
    UseShellExecute = false,
    CreateNoWindow = true
};
using var process = Process.Start(psi)!;
var stdoutTask = process.StandardOutput.ReadToEndAsync();
var stderrTask = process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
var output = await stdoutTask;
var errors = await stderrTask;
if (process.ExitCode != 0)
    throw new InvalidOperationException(errors);

// .NET 11 — the whole thing
var result = await Process.RunAsync("git", "rev-parse HEAD");
Console.WriteLine(result.StandardOutput);

Fifteen lines down to two, with the stream-deadlock footgun removed entirely. That's the 75% rule made literal.

Compression catches up

Zstandard lands in System.IO.Compression, so you can drop ZstdNet or whatever community wrapper you've been carrying. ZIP handling also gains CRC32 validation: corrupted archives now fail loudly instead of producing silently broken extractions.

System.Text.Json gets serious

This is the release where System.Text.Json stops making excuses for real-world shapes:

  • JsonNamingPolicy.PascalCase -- finally a first-class policy for talking to legacy .NET APIs.
  • Per-member naming policy overrides -- one weird property no longer forces [JsonPropertyName] strings everywhere.
  • F# discriminated union support -- F# devs have waited a long time for this.
  • SerializeAsyncEnumerable with NDJSON output and PipeWriter support -- proper streaming serialization.

The NDJSON one deserves a code sample because it directly serves UX. Streaming results as newline-delimited JSON means a frontend can render the first item as soon as it arrives, instead of staring at a spinner while the server buffers a 10MB array:

// Per-member naming override — no more decorating every property
public class LegacyOrderDto
{
    public int OrderId { get; set; }

    [JsonPropertyNamingPolicy(JsonKnownNamingPolicy.SnakeCaseLower)]
    public string CustomerReference { get; set; } = "";
}

// Streaming an IAsyncEnumerable as NDJSON straight to the response
app.MapGet("/orders/stream", (OrderService orders, HttpContext ctx) =>
{
    ctx.Response.ContentType = "application/x-ndjson";
    return JsonSerializer.SerializeAsyncEnumerable(
        ctx.Response.BodyWriter,
        orders.GetOrdersAsync(),
        topLevelValues: true); // NDJSON: one JSON object per line
});

Good architecture enabling good UX. Progressive rendering for free, no SignalR ceremony required.

Discriminated unions: the groundwork arrives

The BCL ships UnionAttribute and IUnion, the runtime scaffolding for the most-requested C# feature of the decade. C# 15 builds on it with union types (plus collection expression arguments, a smaller but pleasant win):

// C# 15 union type — a result that is exactly one of these things
public union OrderResult
{
    Confirmed(int OrderId, DateTime EstimatedDelivery);
    OutOfStock(string Sku);
    PaymentFailed(string Reason);
}

OrderResult result = await ProcessOrderAsync(request);

var message = result switch
{
    OrderResult.Confirmed c     => $"Order {c.OrderId} arrives {c.EstimatedDelivery:d}",
    OrderResult.OutOfStock o    => $"Sorry, {o.Sku} is out of stock",
    OrderResult.PaymentFailed p => $"Payment failed: {p.Reason}"
};

No more exception-driven control flow. No more hand-rolled OneOf<T1, T2, T3> libraries. The compiler knows every case, the switch is exhaustive, and a whole category of "forgot to handle that state" bugs disappears. This is preview syntax, so expect refinement before GA, but the direction is set.

Rapid-fire: the rest worth knowing

  • LINQ FullJoin plus tuple-returning Join/GroupJoin overloads -- full outer joins without the GroupJoin/SelectMany/DefaultIfEmpty incantation.
  • X25519DiffieHellman -- modern key exchange in the box.
  • Generic Random.NextInteger<T>() / NextBinaryFloat<T>() -- random values for any numeric type without casting gymnastics.
  • EqualityComparer<T>.Create(keySelector) -- build a comparer from a key selector in one line; Distinct/GroupBy by a property without a comparer class.
  • QuicStream.Priority -- stream prioritization for HTTP/3 scenarios.
  • Built-in OpenTelemetry metrics for MemoryCache -- hit rates and evictions show up in your dashboards without custom instrumentation.
  • Smaller bits: video MIME type constants, GNU sparse 1.0 support in Tar, and FORCE_COLOR respected by console output.

EF Core 11: The Real Star of This Release

This is my home turf, and I'll say it plainly: EF Core 11 is the strongest EF release in years. Maybe since the EF Core 3 query rewrite. EF Core 10 gave us named query filters that transformed multi-tenant apps; EF Core 11 keeps compounding. The full what's-new doc is long; here's what actually matters. One ground rule first: EF Core 11 requires the .NET 11 SDK and runtime, so it rides the same upgrade train.

Better SQL, and Microsoft brought receipts

EF Core 11 attacks the SQL it generates, and for once the improvements come with benchmark numbers:

  • Pruned to-one joins in split queries -- when a split query repeats a join only to order by its key, EF now drops the join entirely: 29% faster in Microsoft's benchmark.
  • Redundant ORDER BY key removal -- 22% improvement on affected queries.
  • No-op CAST stripping -- cleaner SQL that lets SQL Server's optimizer do its job.

Here's the kind of join that simply disappears:

-- EF Core 10: second query of a split query re-joins Blogs just to order
SELECT [p].[Id], [p].[Title], [p].[BlogId]
FROM [Blogs] AS [b]
INNER JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
ORDER BY [b].[Id]

-- EF Core 11: the join is pruned — Posts.BlogId already has everything we need
SELECT [p].[Id], [p].[Title], [p].[BlogId]
FROM [Posts] AS [p]
ORDER BY [p].[BlogId]

You change nothing. Your queries get faster. That's the whole pitch.

Vector search goes native -- delete your sidecar

Here's my most opinionated take in this post: for most applications, native vector search in SQL Server + EF Core 11 deletes your dedicated vector database.

If you're doing RAG, semantic search, or recommendations over your own relational data, you've probably been running a vector DB sidecar. Another service to deploy, secure, back up, and keep in sync with your source of truth. EF Core 11 brings SQL Server's VECTOR_SEARCH() into LINQ, with HasVectorIndex() (experimental) for index creation:

public class Blog
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public float[] Embedding { get; set; } = [];
}

// Model configuration — vector index with cosine distance (experimental API)
modelBuilder.Entity<Blog>()
    .HasVectorIndex(b => b.Embedding, "cosine");

// Similarity query — translates to the VECTOR_SEARCH() TVF
var similar = await context.Blogs
    .VectorSearch(b => b.Embedding, queryEmbedding, "cosine")
    .OrderBy(r => r.Distance)
    .Take(5)
    .WithApproximate()
    .ToListAsync();

The detail I love most: vector properties are no longer loaded by default. Embeddings are big. Hauling a 1536-float array down the wire on every SELECT is pure waste. Microsoft measured the impact of skipping them: 9x faster locally, 22x faster on Azure SQL for affected queries. That's not a tweak; that's a category change.

This pairs naturally with the AI-agent work I covered when building an MCP server in C#. Your agent's semantic retrieval can now live in the same database as the data itself, behind the same connection string, in the same transaction scope. One less moving part. Honest caveat: the vector index API is flagged experimental, so expect it to shift before GA. Prototype now, ship at GA.

Modeling wins

Complex types and JSON columns now work with TPT and TPC inheritance. Plenty of real domain models have hit that wall. And configuring nested complex properties finally reads like the object model, with lambda-chained config:

modelBuilder.Entity<MyEntity>()
    .Property(e => e.Details.Description)
    .HasMaxLength(500);

No nested ComplexProperty(...) builder blocks just to set a max length. Small thing, used daily.

Query translation: the LINQ-to-SQL gap keeps shrinking

  • MaxByAsync / MinByAsync finally translate. await context.Blogs.MaxByAsync(b => b.Posts.Count()) becomes a SELECT TOP(1) ... ORDER BY (subquery) DESC instead of an exception.
  • EF.Functions.JsonPathExists() -> JSON_PATH_EXISTS on SQL Server 2022+.
  • EF.Functions.JsonContains() -> JSON_CONTAINS on SQL Server 2025 -- but only when you opt in with UseCompatibilityLevel(170). Note the default compatibility level is now 160.
  • New DateTimeOffset translations -- .DateTime, .UtcDateTime, .LocalDateTime, ToOffset -> SWITCHOFFSET, new DateTimeOffset(dt) -> TODATETIMEOFFSET -- plus EF.Functions.DateTrunc() -> DATETRUNC. Date-bucketing reports without raw SQL.
  • Full-text search becomes a first-class citizen: HasFullTextCatalog/HasFullTextIndex in migrations, and FreeTextTable/ContainsTable returning FullTextSearchResult<T> with a Rank you can order by.
  • Temporal period properties can now map to real CLR properties via HasPeriodStart(e => e.PeriodStart) -- no more shadow-property contortions for temporal table queries.

Migrations grow team-sense

This is the section that will save real teams real pain.

The latest migration ID is now recorded in the model snapshot. Translation: when two branches each add a migration, you get a merge conflict. I know that sounds annoying. It's exactly right. Divergent migrations failing loudly at merge time beats them failing silently at deploy time, every single time. This is a feature, not a bug.

dotnet ef database update <Name> --add creates the migration (Roslyn-compiled on the fly) and applies it in one step. For container-based inner loops this is gold. Pair it with dotnet watch's new Aspire integration and the database-change loop in the Aspire deployment model I wrote about gets dramatically shorter:

# Create + compile + apply a migration in one command
dotnet ef database update AddOrderStatus --add

# Remove a migration against a specific connection, or fully offline
dotnet ef migrations remove --connection "Server=localhost;Database=Shop;..."
dotnet ef migrations remove --offline

The CLI also learns to read defaults from .config/dotnet-ef.json, so you stop retyping --project/--startup-project on every command:

{
  "project": "src/Shop.Data",
  "startupProject": "src/Shop.Api",
  "framework": "net11.0",
  "configuration": "Debug",
  "context": "ShopDbContext"
}

Rounding it out: ExcludeForeignKeyFromMigrations() for FKs you manage outside EF, and a new analyzer warning when you reach for ToAsyncEnumerable() where AsAsyncEnumerable() was the right call. A subtle but classic performance trap.

Cosmos DB grows up, with community credit due

The Cosmos provider gets complex types, transactional batches by default (tunable via AutoTransactionBehavior -- Auto/Never/Always), bulk execution via options.BulkExecutionEnabled(), and proper session token management (SemiAutomatic mode with GetSessionToken/UseSessionToken for consistency across instances).

All of this was contributed by community member @JoasE. That deserves saying out loud: a huge chunk of the Cosmos story in this release is one person's open-source work. This is what a healthy OSS ecosystem looks like.


SDK & Tooling: The Inner Loop Gets Faster

The CLI picks up a set of small changes that compound into a noticeably better day.

dotnet sln supports solution filters (.slnf). If you live in a big repo, managing slices of the solution from the CLI instead of opening Visual Studio is a genuine quality-of-life win.

File-based apps graduate from toys to tools with #:include for multi-file apps. My build scripts have been quietly migrating from PowerShell to C# file-based apps, and multi-file support removes the last excuse:

// helpers.cs
static class Versioning
{
    public static string GetVersion() =>
        DateTime.UtcNow.ToString("yyyy.MM.dd") + "-local";
}
// build.cs — run with: dotnet run build.cs
#:include helpers.cs

Console.WriteLine($"Building version {Versioning.GetVersion()}...");

dotnet run -e sets environment variables inline. No more $env: gymnastics in PowerShell, no more platform-specific syntax in your README:

dotnet run -e ASPNETCORE_ENVIRONMENT=Staging -e FEATURE_FLAGS=vector-search

dotnet watch gains Aspire app-host integration and crash recovery. Your watch session survives the crashes that used to kill it, and it understands the Aspire orchestration model. Combined with dotnet ef database update --add from the previous section, the edit -> migrate -> run loop tightens considerably.

Two one-liners: CLI telemetry moves to OpenTelemetry (replacing the old App Insights pipeline), and the installers got smaller.

Microsoft.Extensions v10.7.0: Kubernetes-aware resource monitoring goes stable

One more tooling item from the dotnet/extensions releases that deserves more attention than it's getting: Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes is now stable, along with ResourceQuota/ResourceQuotaProvider.

The point: a Kubernetes-aware ResourceQuotaProvider reads your pod's actual CPU/memory requests and limits and feeds them into the resource-utilization metrics. If you run .NET in K8s, this is the difference between "metrics" and "metrics that match your actual limits." A dashboard showing 40% memory usage of the node tells you nothing; 90% of your pod's limit tells you you're about to get OOM-killed.

var builder = WebApplication.CreateBuilder(args);

// Kubernetes-aware resource monitoring — quotas read from pod requests/limits
builder.Services.AddResourceMonitoring();
builder.Services.AddKubernetesResourceQuotaProvider();

var app = builder.Build();

Also in v10.7.0: Microsoft.Extensions.AI.OpenAI updates to OpenAI 2.11.0 (with a ToolJson.AdditionalProperties fix), and HostedFileContent.SizeInBytes/CreatedAt go stable.


The Verdict: A Subtraction Release

.NET 11 is the rare release where the best features are the ones that remove things. Runtime Async removes stack-trace noise and per-await overhead. EF Core 11 removes redundant joins, removes the vector-DB sidecar, and removes a whole migration workflow step with --add. The SDK removes friction from the inner loop.

My day-one adoption list: Runtime Async (free), EF Core 11's SQL improvements (free), and dotnet ef database update --add in dev loops. My experiment-now list: vector search and C# 15 unions. Both will shift before GA, but the teams who prototype now will hit the ground running in November 2026.

Desktop devs: I've stayed server-side here, but WinForms and WPF have their own .NET 11 what's-new notes worth a skim.

Install Preview 5 side-by-side with your current SDK, point your test suite at it, and tell me what breaks. Or better, tell me the first feature you adopted. Next up from me: a full deep dive on EF Core 11 vector search with real embeddings, real benchmarks, and an honest look at where it doesn't replace a dedicated vector database.

Read more