.NET 11.0 Preview 2 delivers asynchronous runtime
Asynchronous programming with async and await has existed in .NET for years. Now Microsoft is delivering a new runtime environment for asynchronous execution.
(Image: Pincasso / Shutterstock.com)
- Dr. Holger Schwichtenberg
Microsoft has released the second preview version for .NET 11.0, bringing, among other things, innovations for asynchronous programming.
The keywords async and await are anchored in many modern programming languages, for example, in Python since 2015, in JavaScript since 2017, in Rust since 2019, and in Swift since 2021. To the question “Who invented it?”, in this case, the answer must be: Microsoft. In 2012, these two keywords first appeared in C# Version 5.0 and Visual Basic .NET Version 11.0. They simplified asynchronous programming for developers compared to previously existing concepts and subsequently inspired many other programming languages.
However, what is simple for developers is complex under the hood to this day: In .NET languages, async and await have so far been realized in the compiler as state machines. In .NET 11.0 Preview 2, Microsoft is now introducing an advanced version of the .NET runtime environment Common Language Runtime (CLR), which natively supports suspending and resuming asynchronous methods. This not only generates less overhead than the previous state machines but also enables leaner stack traces and simpler debugging.
Listing 1 shows four asynchronous methods that call each other.
using System.Diagnostics;
namespace NET11_Console.Runtime;
public class NET11_RuntimeAsync
{
public async Task Run()
{
await MethodeEbene1();
}
async Task MethodeEbene1()
{
await Task.CompletedTask;
await MethodeEbene2();
}
async Task MethodeEbene2()
{
await Task.CompletedTask;
await MethodeEbene3();
}
async Task MethodeEbene3()
{
await Task.CompletedTask;
Console.WriteLine(new StackTrace(fNeedFileInfo: true));
}
}
Listing 1: Chaining asynchronous methods
Previously, the stack trace showed the state machine as the result of Listing 1:
at NET11_Console.Runtime.NET11_RuntimeAsync.MethodeEbene3() in
h:\git\ITVDemos\NET11\NET11_Console\Runtime\NET11_RuntimeAsync.cs:line 27
at
System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine&
stateMachine)
at NET11_Console.Runtime.NET11_RuntimeAsync.MethodeEbene3()
at NET11_Console.Runtime.NET11_RuntimeAsync.MethodeEbene2() in
h:\git\ITVDemos\NET11\NET11_Console\Runtime\NET11_RuntimeAsync.cs:line 21
at
System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine&
stateMachine)
at NET11_Console.Runtime.NET11_RuntimeAsync.MethodeEbene2()
at NET11_Console.Runtime.NET11_RuntimeAsync.MethodeEbene1() in
h:\git\ITVDemos\NET11\NET11_Console\Runtime\NET11_RuntimeAsync.cs:line 15
at
System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine&
stateMachine)
at NET11_Console.Runtime.NET11_RuntimeAsync.MethodeEbene1()
at NET11_Console.Runtime.NET11_RuntimeAsync.Run() in h:\git\ITVDemos\NET11\NET11_Console\Runtime\NET11_RuntimeAsync.cs:line 9
at
System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine&
stateMachine)
at NET11_Console.Runtime.NET11_RuntimeAsync.Run()
at Program.<Main>$(String[] args) in h:\git\ITVDemos\NET11\NET11_Console\Program.cs:line 8
With the new runtime support for asynchronicity, the stack trace is significantly more compact:
at NET11_Console.Runtime.NET11_RuntimeAsync.MethodeEbene3() in
h:\git\ITVDemos\NET11\NET11_Console\Runtime\NET11_RuntimeAsync.cs:line 27
at NET11_Console.Runtime.NET11_RuntimeAsync.MethodeEbene2() in
h:\git\ITVDemos\NET11\NET11_Console\Runtime\NET11_RuntimeAsync.cs:line 21
at NET11_Console.Runtime.NET11_RuntimeAsync.MethodeEbene1() in
h:\git\ITVDemos\NET11\NET11_Console\Runtime\NET11_RuntimeAsync.cs:line 15
at NET11_Console.Runtime.NET11_RuntimeAsync.Run() in
h:\git\ITVDemos\NET11\NET11_Console\Runtime\NET11_RuntimeAsync.cs:line 9
at Program.<Main>$(String[] args) in h:\git\ITVDemos\NET11\NET11_Console\Program.cs:line 8
On this basis, Microsoft was also able to improve the debugging experience; see Pull Request “Runtime support for breakpoints and stepping” on GitHub.
However, the new support for asynchronicity in the runtime environment currently still requires this innovation to be activated separately in the project file, see Listing 2.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net11.0</TargetFrameworks>
…
</PropertyGroup>
<!--Runtime Async-->
<PropertyGroup>
<Features>runtime-async=on</Features>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
</PropertyGroup>
</Project>
Listing 2: Activating asynchronicity in the runtime environment via project settings
Videos by heise
All TAR formats
.NET has supported the TAR archive format since version 7.0 with the classes TarFile, TarEntry, TarReader, and TarWriter in the namespace System.Formats.Tar. When creating a TAR file with TarFile.CreateFromDirectory() and TarFile.CreateFromDirectoryAsync(), the PAX format (POSIX.1-2001) was always used as the archive format. Since .NET 11.0 Preview 2, developers have the option to choose all four TAR formats: TarEntryFormat.V7 (the original TAR format), TarEntryFormat.Ustar (Unix Standard TAR), TarEntryFormat.Gnu, and TarEntryFormat.Pax.
This is done via the additional parameter with an enumeration value from TarEntryFormat:
TarFile.CreateFromDirectory("d:\Export", @"t:\archiv.gnu.rar",
includeBaseDirectory: true, TarEntryFormat.Gnu);
A significant difference between the TAR formats is the maximum file lengths: V7 only allows a maximum of 100 characters for an entry name. USTAR has 256 characters. GNU and PAX have unlimited file name lengths. USTAR has a file size limit of 8 GB.
MinBy() and MaxBy() for database access
Microsoft already introduced the LINQ operators MinBy() and MaxBy() in .NET 6.0. Unlike the operators Min() and Max(), which have been available since the first LINQ version in .NET Framework 3.5, MinBy() and MaxBy() not only return the minimum or maximum value itself but the entire surrounding object. Previously, MinBy() and MaxBy() could only be used in LINQ-to-Objects. This is now changing in .NET 11.0: Entity Framework Core can also translate these LINQ operators into SQL commands; see Listing 3.
var ctx = new DA.WWWings.WwwingsV1EnContext();
// Min vs. MinBy()
var wenigsteFreiePlaetze = ctx.Flights.Min(x => x.FreeSeats);
CUI.H1("Der Flug mit den wenigsten Plätzen hat " + wenigsteFreiePlaetze + " freie Plätze");
var flugMitDenWenigstenFreienPlaetzen = ctx.Flights.MinBy(x => x.FreeSeats);
Console.WriteLine(flugMitDenWenigstenFreienPlaetzen);
// Max() vs. MaxBy()
var meisteFreiePlaetze = ctx.Flights.Max(x => x.FreeSeats);
CUI.H1("Der Flug mit den meisten freien Plätzen hat " + meisteFreiePlaetze + " freie Plätze");
var flugMitDenMeistenFreienPlaetzen = ctx.Flights.MaxBy(x => x.FreeSeats);
Console.WriteLine(flugMitDenMeistenFreienPlaetzen);
Listing 3: MinBy() and MaxBy() in Entity Framework Core 11.0
While the LINQ operator Min() uses the MIN() function in SQL
SELECT MIN([f].[FreeSeats])
FROM [Operation].[Flight] AS [f]
MinBy() creates a sorted dataset and returns the top record with TOP(1):
SELECT TOP(1) [f].[FlightNo], [f].[Airline], [f].[Departure], [f].[Destination],
[f].[FlightDate], [f].[FreeSeats], [f].[Memo], [f].[NonSmokingFlight], [f].[Pilot_PersonID],
[f].[Seats], [f].[Timestamp]
FROM [Operation].[Flight] AS [f]
ORDER BY [f].[FreeSeats]
Data transfer between HTTP requests with TempData
The Static Server-Side Rendering (SSR) Blazor variant introduced in .NET 8.0 is superior to previous approaches for creating multi-page apps in modern .NET (ASP.NET Core Model-View-Controller (MVC) and ASP.NET Core Razor Pages) in terms of component model, Razor syntax, and partial page exchange. However, until now, there were also some functions in MVC and Razor Pages that Blazor SSR could not handle. These include caching of page fragments, extensible conditions for route parameters, and data transfer between HTTP requests with the TempData object (see GitHub issue). The latter is now possible in .NET 11.0 Preview 2. Data storage is optionally done as a cookie (CookieTempDataProvider) or in the session storage (SessionStorageTempDataProvider) of the web browser.
Unlike MVC and Razor Pages, TempData is not a property of the base class but must be explicitly consumed as a Cascading Parameter; see Listing 5. It should be noted that, as with MVC and Razor Pages, TempData can only store strings, meaning developers must serialize complex objects themselves; see Listing 4.
@page "/Registration"
@using BlazorSSRSamples
@using Newtonsoft.Json
@inject NavigationManager NavigationManager
<EditForm FormName="Registration" Model="reg" OnValidSubmit="HandleSubmit">
<DataAnnotationsValidator />
<p>
Ihr Name: <InputText @bind-Value="reg.Name" />
<ValidationMessage For="@(() => reg.Name)" />
</p>
<p>
Ihre E-Mail-Adresse: <InputText @bind-Value="reg.EMail" />
<ValidationMessage For="@(() => reg.EMail)" />
</p>
<button class="btn btn-primary" type="submit">Bestellen</button>
</EditForm>
@code {
[SupplyParameterFromForm]
RegistrationData reg { get; set; } = new();
[CascadingParameter]
public ITempData? TempData { get; set; }
private string? message;
private void HandleSubmit()
{
TempData!["Message"] = "Registrierung erfolgreich ĂĽbermittelt!";
TempData!["Reg"] = System.Text.Json.JsonSerializer.Serialize(reg); // speichert nur Strings
TempData!["RegInfo"] = DateTime.Now.ToString();
NavigationManager.NavigateTo("RegistrationConfirm", new NavigationOptions() { ForceLoad = true });
}
}
Listing 4: Populating TempData in Blazor SSR page
@page "/RegistrationConfirm"
@using BlazorSSRSamples
@inject NavigationManager NavigationManager
<p class="mb-2 alert alert-success">@message</p>
@if (reg.HasValue)
{
<p>Ihre Daten:<br />
@reg.Name<br />
@reg.EMail</p>
}
@if (regInfo.HasValue)
{
<p>Registriert am:<br />
@regInfo</p>
}
@code {
[CascadingParameter]
public ITempData? TempData { get; set; }
private string? message;
private string? regInfo;
BlazorSSRSamples.RegistrationData? reg { get; set; } = new();
protected override void OnInitialized()
{
message = TempData?.Get("Message") as string ?? "No message";
reg = System.Text.Json.JsonSerializer.Deserialize<BlazorSSRSamples.RegistrationData>(TempData?.Get("Reg").ToString());
regInfo = TempData?.Get("regInfo") as string;
}
}
Listing 5: Reading TempData in Blazor SSR page
It is planned for upcoming preview versions that reading data from TempData will be simplified via an annotation [SupplyParameterFromTempData], with which a property can be annotated; see Pull Request.
Further innovations in .NET 11.0 Preview 2
According to the release notes for .NET 11.0 Preview 2, the following further innovations are included:
- The Kestrel web server integrated into ASP.NET now rejects invalid requests faster because Microsoft internally avoids throwing a
BadHttpRequestExceptionand instead returns a structure. According to Microsoft, this improves throughput by 20 to 40 percent for attacks via port scanning or with faulty requests. - .NET Core-based WebAPIs now support Open API Specification version 3.2. In .NET 10.0, it was version 3.1.1.
- For creating web worker projects, there is now a dedicated project template “.NET Web Worker” in Visual Studio or at the command line:
dotnet new webworker. This creates a DLL that can be used in Blazor WebAssembly-based applications. - With Entity Framework Core, you can now configure the built-in full-text search for SQL Server via Fluent API:
modelBuilder.Entity<Texte>(b =>
{
b.HasFullTextIndex(e => new { e.Titel, e.Text})
.HasKeyIndex("PK_FullTextEntity")
.HasLanguage("Titel", "German")
.HasLanguage("Text", "German");
});
- Similarly, vector search is available in Entity Framework Core with the SQL Server 2025 function
VECTOR_SEARCH()via the LINQ operationVectorSearch():
var sqlVector = new SqlVector<float>(EmbeddingsUtil.Get(suchBegriff));
var q = ctx.TexteSet.VectorSearch<Texte, SqlVector<float>>(b => b.Embedding, sqlVector, "cosine", topN: 5);
It should be noted that VECTOR_SEARCH() must first be activated via SQL command even in the stable version of SQL Server 2025
ALTER DATABASE SCOPED CONFIGURATION SET PREVIEW_FEATURES ON
and the warning “EP9105” must be deactivated in .NET:
#pragma warning disable EF9105 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
- In .NET MAUI, the
<Map>control has been improved. The syntax is now more compact.
<maps:Map x:Name="map" Region="36.9628,-122.0195,0.01,0.01">
<maps:Map.Pins>
<maps:Pin Label="Santa Cruz" Location="36.9628,-122.0195" />
</maps:Map.Pins>
</maps:Map>
Furthermore, elements on the map (Polygon, Polyline, Circle) can now be controlled via IsVisible and ZIndex. Additionally, there are Click() events on these elements.
- .NET MAUI applications for Apple operating systems (iOS, tvOS, and Mac Catalyst) can now run on the .NET Core Runtime instead of Mono. Since .NET 10.0, this has been possible experimentally for MAUI applications on Android. However, the Apple implementation of the .NET Core Runtime is also considered experimental. The applications currently become larger due to the change, and debugging is still limited.
- The size of the container images of .NET SDK 11.0 Preview 2 has been reduced by 41 to 44 MB (up to 17 percent) compared to .NET 11.0 Preview 1, as Microsoft now uses hard links for duplicate files. This also applies to the installers for Linux and macOS.
(Image:Â Microsoft)
Nothing else new
According to the release notes for .NET 11.0 Preview 2, there are no new features for the Visual Basic and C# language syntax or the Windows Forms GUI framework in this preview version. For Windows Presentation Foundation (WPF), there is only a bug fix.
Outlook
.NET 11.0 is scheduled for release in November 2026 and will receive a standard support period of two years. Until then, developers can expect five more preview versions from April to August, as well as one release candidate version each in September and October. heise developer will report on each.
(vbr)