.NET 10.0 Preview 1 ist am 25. Februar 2025 erschienen. Am 18. März 2024 folgte Preview 2. Die Vorschauversionen stehen bei Microsoft bereit zum Download [1].
An der Grundstruktur (drei Runtimes, ein SDK) hat sich nichts geändert [1]. Auch die unterstützten Plattformen sind gleichgeblieben. Laut dem Screenshot der Downloadseite in Abbildung 1 sind weiterhin die Sprachversionen C# 13.0, F# 9.0 und Visual Basic .NET 17.13 enthalten; das ist aber nicht richtig, denn es gibt in .NET 10.0 bereits erste neue Sprachfeatures von C# 14.0.

Abb. 1: .NET-10.0-Downloadseite [1]
Zielframework und Sprachversion setzen
Für .NET 10.0 braucht man die Preview-Version von Visual Studio 2022 Version 17.14 (zum Redaktionsschluss für diesen Beitrag verfügbar auf dem Stand Preview 2, Abb. 2). Alternativ kann man das Target Framework auch manuell setzen:
<TargetFramework>net10.0</TargetFramework>
Allerdings reicht es zur Nutzung der neuen Sprachfeatures derzeit nicht aus, nur das Target Framework zu setzen; Man muss auch noch
<LangVersion>preview</LangVersion>
angeben, um alle neuen Sprachfeatures nutzen zu können.

Abb. 2: .NET 10.0 erscheint bereits in Visual Studio 2022 17.14 Preview 2
C# 14.0: Vereinfachung für nameof() mit generischen Typen
Bisher musste man in C# immer konkrete Typparameter angeben, um den Operator nameof() auf generische Typen anzuwenden. Jetzt, in C# 14.0, kann man als kleine Syntaxvereinfachung auch die Typparameter im Code weglassen (Listing 1).
<em>// BISHER</em> Console.WriteLine(nameof(List<int>)); <em>// --> List</em> Console.WriteLine(nameof(System.Collections.Generic.LinkedListNode<int>)); <em>// --> LinkedListNode</em> <em>Console.WriteLine(nameof(System.Collections.Generic.KeyValuePair<int, string>)); // --> KeyValuePair</em> <em>// NEU</em> Console.WriteLine(nameof(List<>)); <em>// --> List</em> Console.WriteLine(nameof(System.Collections.Generic.LinkedListNode<>)); <em>// --> LinkedListNode</em> Console.WriteLine(nameof(System.Collections.Generic.KeyValuePair<,>)); <em>// --> KeyValuePair</em>
C# 14.0: Vereinfachungen bei Lambdaausdrücken
In Lambdaausdrücken kann man in C# 14.0 jetzt Parameter-Modifizierer wie scoped, ref, in, out und _ref readonly_verwenden, ohne dabei den Datentyp benennen zu müssen. Als Beispiel für den delegate
delegate bool Extract<T>(string text, out T result);
musste man vor C# 14.0 schreiben:
Extract<int> ExtractOld = (string text, out int result) => Int32.TryParse(text, out result);
Jetzt in C# 14.0 können Entwickler:innen im Lambdaausdruck die Nennung der Datentypen _string_und int weglassen, weil diese Datentypen aus dem Kontext bereits klar sind:
Extract<int> ExtractNew = (text, out result) => Int32.TryParse(text, out result);
Das geht allerdings nicht, wenn ein variadischer Parameter mit params zum Einsatz kommt:
Add<int> AddOld = (out int result, params List<int> data) => { result = data.Sum(); return true; };
Nicht möglich ist also diese Verkürzung:
Add<int> AddNew = (out result, params data) => { result = data.Sum(); return true; };
C# 14.0: mehr Konvertierungen für Span und ReadOnlySpan
Im Rahmen der Initiative „First-Class Span Types” [2] soll C# 14.0 neue automatische Konvertierungen zwischen Arrays und Span sowie ReadOnlySpan enthalten. Dabei waren einige Konvertierungen bisher schon möglich und da das Dokument „What’s new in C# 14″ [3] nicht genau auflistet, was die Neuerungen sind, sondern nur auf die Liste aller möglichen Konvertierungen verweist, fällt es schwer, herauszufinden, was wirklich neu ist.
In einem Test konnte aber folgender Fall gefunden werden: Wenn die Klasse Developer von der Basisklasse Person erbt, dann kann ein Array
Developer[] devs = new Developer[3];
in C# 14.0 wie folgt konvertiert werden:
Span<Developer> devsSpan = devs; ReadOnlySpan<Developer> devsROSpan = devs; ReadOnlySpan<Person> personROSpan = devs; ReadOnlySpan<Person> personROSpanFromSpan = devsSpan;
In C# 13.0 war hier nur der letzte Fall erlaubt, wenn man dort preview setzte. Vermutlich wird auch dieses Sprachfeatures in C# 14.0 dann als stabil gelten.
C# 14.0: Schlüsselwort field
Im Dokument „What’s new in C# 14″ [3] beschreibt Microsoft zudem das Schlüsselwort field, mit dem man sogenannte Semi-Auto Properties erstellen kann (Listing 2). Auch dieses Sprachfeature gibt es schon in der stabilen Version von .NET 9.0 – darin aber im Status Preview, d. h., man musste auch dafür preview setzen. Die Erwähnung in „What’s new in C# 14″ legt die Vermutung nahe, dass das Sprachfeature in C# 14.0 dann als stabil gelten wird.
<em>/// <summary></em> <em>/// Semi-Auto Property</em> <em>/// </summary></em> public int ID { get; set <em>// init wäre hier auch erlaubt!</em> { if (value < 0) throw new ArgumentOutOfRangeException(); if (field > 0) throw new ApplicationException("ID schon gesetzt"); field = value; } } = -1;
C# 14.0: partielle Konstruktoren und partielle Events
C# kennt seit Version 2.0 partielle Klassen und seit Version 3.0 partielle Methoden. Im Jahr 2024 kamen in C# 13.0 partielle Properties und Indexer hinzu. Nun in C# 14.0 sollen Entwickler:innen das Schlüsselwort partial auch auf Konstruktoren und Events in C#-Klassen anwenden können (Listing 3). So es steht in den Releases-Notes zu C# in .NET 10.0 Preview 2 [4].
partial class C { partial C(int x, string y); partial event Action<int, string> MyEvent; } partial class C { partial C(int x, string y) { } partial event Action<int, string> MyEvent { add { } remove { } } }
Der C#-Compiler möchte das Beispiel aus der Dokumentation [5] aber noch nicht übersetzen und meldet Fehler sowohl in Visual Studio als auch beim Kommandozeilenbefehl dotnet build (Abb. 3 und 4), selbst wenn man preview in die Projektdatei schreibt. Laut [6] kommt das Feature in Visual Studio auch erst mit Version 17.4 Preview 3; diese Version war zum Redaktionsschluss noch nicht verfügbar.

Abb. 3: Kompilierungsfehler in Visual Studio beim Versuch der Verwendung von partiellen Konstruktoren und partiellen Events in .NET 10.0 Preview 2

Abb. 4: Kompilierungsfehler auf der Kommandozeile beim Versuch der Verwendung von partiellen Konstruktoren und partiellen Events in .NET 10.0 Preview 2
Visual Basic .NET: OverloadResolutionPriority
Auch beim Visual-Basic-.NET-Compiler gibt es nach langer Stagnation mal wieder kleine Neuerungen: Der Compiler hält sich nun an die in .NET 9.0 und C# 13.0 eingeführte Annotation [OverloadResolutionPriority] zur Priorisierung von Überladungen.
.NET SDK: vereinheitlichte Kommandozeilenparameterreihenfolge
In dem .NET-SDK-Kommandozeilenwerkzeug _dotnet_hat Microsoft in .NET 10.0 die Parameterreihenfolge vereinheitlicht. Es gab bisher viele Befehle, die aus einem Substantiv gefolgt von einem Verb oder Adjektiv bestanden (z. B. dotnet new list, dotnet workload install, dotnet nuget add).
Es gab aber auch einige Befehle, bei denen am Anfang ein Verb stand, z. B. dotnet add reference und dotnet remove package. Das hat Microsoft nun vereinheitlicht zu folgenden neuen Befehlen:
dotnet package add dotnet package list dotnet package remove dotnet reference add dotnet reference list dotnet reference remove
Die alten Kommandozeilenbefehle mit dem Verb vorne sind aber weiterhin vorhanden, sodass hier kein Breaking Change entsteht.
.NET SDK: neuer Standard bei Workload Sets
Das .NET SDK verwendet bei der Workload-Verwaltung in .NET 10.0 nun als Standard den in .NET 9.0 eingeführten Modus „Workload Sets” anstelle des bisherigen Standards, den Microsoft „Loose Manifests” nennt [7].
Im .NET SDK gibt es zudem eine Optimierung für die Beschleunigung des Package Restore [8].
Basisklassenbibliothek: LINQ-Operatoren LeftJoin() und RightJoin()
Wie in den letzten .NET-Versionen auch, liefert Microsoft in .NET 10.0 wieder neue Operatoren für Language Integrated Query (LINQ), die bestehende Konstrukte vereinfachen. Dieses Mal sind es mit LeftJoin() und RightJoin() zwei elementare Operatoren aus der Mengenlehre und den relationalen Datenbanken. Tatsächlich waren diese Operationen in LINQ bereits möglich, allerdings umständlich mit Hilfe einer Gruppierung mit GoupJoin() und SelectMany() sowie DefaultIfEmpty(). Die neuen Methoden LeftJoin() und RightJoin() vereinfachen die Verwendung, wie Listing 4 am Beispiel des Joins zwischen den Klassen Company und _Website_zeigt, die Ausgabe ist in Abbildung 5 zu sehen.
Company[] companies = [ new Company{ ID = 1, Name = "www.IT-Visions.de" }, new Company{ ID = 2, Name = "Software & Support" }, new Company{ ID = 3, Name = "Startup i.Gr." } <em>// hat noch keine Website</em> ]; Website[] websites = [ new Website{ CompanyID = 1, URL = "www.IT-Visions.de" }, new Website{ CompanyID = 1, URL = "www.dotnet10.de" }, new Website{ CompanyID = 2, URL = "www.entwickler.de" }, new Website{ URL = "www.Microsoft.com" }, <em>// Firma ist noch nicht angelegt</em> ]; CUI.H2("--- LeftJoin ALT seit .NET Framework 3.5 ---"); var AllCompaniesWithWebsitesSetOld = companies .GroupJoin(websites, c => c.ID, w => w.CompanyID, c, websites) => new { Company = c, Websites = websites }) .SelectMany( x => x.Websites.DefaultIfEmpty(), <em>// Falls keine Webseite existiert, wird `null` verwendet</em> (c, w) => new WebsiteWithCompany { Name = c.Company.Name, URL = w.URL, <em>// Falls `w` null ist, bleibt URL null</em> City = c.Company.City }); foreach (var item in AllCompaniesWithWebsitesSetOld) { Console.WriteLine((item.Name != null ? item.Name + " " + item.City : "- keine Firma - ") + " -> " + (item.URL ?? "- keine URL -")); } CUI.H2("--- LeftJoin NEU ab .NET 10.0 ---"); var AllCompaniesWithWebsitesSet = companies.LeftJoin(websites, e => e.ID, e => e.CompanyID, (c, w) => new WebsiteWithCompany { Name = c.Name, City = c.City, URL = w.URL }); foreach (var item in AllCompaniesWithWebsitesSet) { Console.WriteLine((item.Name != null ? item.Name + " " + item.City : "- keine Firma - ") + " -> " + (item.URL ?? "- keine URL -")); } CUI.H2("--- RightJoin OLD seit .NET Framework 3.5 ---"); var WebsiteWithCompanySetOLD = websites .GroupJoin(companies, w => w.CompanyID, c => c.ID, (w, companies) => new { Website = w, Companies = companies }) .SelectMany( x => x.Companies.DefaultIfEmpty(), <em>// Falls kein Unternehmen existiert, bleibt `null`</em> (w, c) => new WebsiteWithCompany { Name = c.Name, <em>// Falls `c` null ist, bleibt `Name` null</em> City = c.City, <em>// Falls `c` null ist, bleibt `City` null</em> URL = w.Website.URL }); foreach (var item in WebsiteWithCompanySetOLD) { Console.WriteLine((item.Name != null ? item.Name + " " + item.City : "- keine Firma - ") + " -> " + (item.URL ?? "- keine URL -")); } CUI.H2("--- RightJoin NEU ab .NET 10.0 ---"); var WebsiteWithCompanySet = companies.RightJoin(websites, e => e.ID, e => e.CompanyID, (c, w) => new WebsiteWithCompany { Name = c.Name, City = c.City, URL = w.URL }); foreach (var item in WebsiteWithCompanySet) { Console.WriteLine((item.Name != null ? item.Name + " " + item.City : "- keine Firma - ") + " -> " + (item.URL ?? "- keine URL -")); } <em>// Zum Vergleich: Inner Join, den es seit .NET Framework 3.5 gibt</em> CUI.H2("--- InnerJoin seit .NET Framework 3.5 ---"); var CompaniesWithWebsitesSet = companies.Join(websites, c => c.ID, w => w.CompanyID, (c, w) => new WebsiteWithCompany { Name = c.Name, URL = w.URL, City = c.City }); foreach (var item in CompaniesWithWebsitesSet) { Console.WriteLine((item.Name != null ? item.Name + " " + item.City : "- keine Firma - ") + " -> " + (item.URL ?? "- keine URL -")); }

Abb. 5: Ausgabe von Listing 4
Basisklassenbibliothek: TryAdd() und TryGetValue() für OrderedDictionary<T,T>
Die erst in .NET 9.0 neu eingeführte generische Klasse System.Collections.Generic.OrderedDictionary<T,T> bot bisher schon eine Methode TryAdd(), die versucht, ein Element hinzuzufügen. Neben der bestehenden Variante TryAdd(TKey key, TValue value) gibt es nun in .NET 10.0 auch die Methode TryAdd(TKey key, TValue value, out int index) (Listing 5). Diese neue Überladung liefert den Index zurück, falls es das Element in der Menge schon gibt. Analog dazu gibt es für die Methode TryGetValue() nun eine neue Überladung, die nicht nur den Wert eines Eintrags liefert, sondern auch die Position des gefundenen Elements per Index.
OrderedDictionary<string, string> websites = new OrderedDictionary<string, string>(); websites.Add("Software & Support", "www.Entwickler.de"); websites.Add("Microsoft", "www.Microsoft.com"); websites.Add("IT-Visions", "www.IT-Visions.de"); var propertyName = "IT-Visions"; var value = "www.IT-Visions.de"; <em>// bisher</em> if (!websites.TryAdd(propertyName, value)) { int index1 = websites.IndexOf(propertyName); <em>// Second lookup operation</em> CUI.Warning("Element " + value + " ist bereits vorhanden!"); } <em>// neu</em> if (!websites.TryAdd(propertyName, value, out int index)) { CUI.Warning("Element " + value + " ist bereits vorhanden an der Position " + index + """!"""); } <em>// neu</em> if (websites.TryGetValue(propertyName, out string? value2, out int index2)) { CUI.Success($"Element {value2} wurde gefunden an der Position {index2}."); }
Basisklassenbibliothek: IP-Adressen prüfen
Zur Prüfung von IP-Adressen gibt es in der Klasse System.Net schon seit .NET Framework 2.0 die statische Methode IPAddress.TryParse(), die eine IP-Adresse aus einer Zeichenkette extrahiert. Seit .NET Core 2.1 ist die Extraktion auch aus dem Typ ReadOnlySpan möglich. Der Rückgabewert ist ein bool-Wert und die extrahierte IP-Adresse wird in Form einer Instanz der Klasse IPAddress als out-Parameter geliefert. Wenn man nur prüfen will, ob die IP-Adresse stimmt, schreibt man _IPAddress.TryParse(eingabe, out ).
In .NET 10.0 bietet Microsoft nun in der statischen Methode _IsValid()_eine weitere Prüfungsvariante mit weniger internem Aufwand. Beispiel:
System.Net.IPAddress.IsValid("192.168.1.0")
Dabei kehrt Microsoft nun einfach eine bisher interne Methode TargetHostNameHelper.IsValidAddress() nach außen [9].
Basisklassenbibliothek: Verbesserung beim Zertifikatsexport
Seit .NET 10.0 Preview 2 soll laut Release Notes in der Klasse X509Certificate2 eine neue Methode ExportPkcs12() existieren, die Entwickler:innen mehr Kontrolle über die Verschlüsselungs- und Digest-Algorithmen geben soll. Die bisherige Methode Export() verwendet noch veraltete Algorithmen wie 3DES und SHA1. Die neue Methode ExportPkcs12() soll auch AES und SHA-2-256 bieten.
In den Release Notes zu .NET 10.0 Preview 2 [10] fehlt allerdings ein Beispiel und im Test konnte der Compiler weder auf der Klasse X509Certificate2 noch der Basisklasse X509Certificate die Methode ExportPkcs12() finden. Das zugehörige Issue auf GitHub steht auf dem Status Closed [11]. Da sich im Change-Log zu .NET 10.0 Preview 2 [12] auch kein Eintrag zu ExportPkcs12() finden lässt, sind die Release-Notes an dieser Stelle vermutlich voreilig und die neue Methode kommt erst in einer späteren Vorschauversion.
Basisklassenbibliothek: weitere Verbesserungen
Außerdem hat Microsoft diese drei Punkte in der .NET-Basisklassenbibliothek in .NET 10.0 Preview 1 und 2 verbessert:
- In der Klasse X509Certificate2Collection existiert eine neue Methode _FindByThumbprint()_um ein digitales Zertifikat anhand des SHA-Fingerabdrucks zu finden.
- Die Klasse ISOWeek bietet drei neue Methoden zum Umgang mit dem Datentyp System.DateOnly. Bisher war hier nur System.DateTime möglich:
DateOnly now = DateOnly.FromDateTime(DateTime.Now); Console.WriteLine(ISOWeek.GetWeekOfYear(now)); Console.WriteLine(ISOWeek.GetYear(now)); Console.WriteLine(ISOWeek.ToDateOnly(2025, 1, DayOfWeek.Tuesday)); <em>// 30.12.2024</em>
- Die Implementierung der Verarbeitung von ZIP-Archiven wurde optimiert, z. B. wurde das Aktualisieren von ZIP-Archiven um 99,8 Prozent beschleunigt (ein Hinzufügen einer 2-GB-Datei von 177 ms auf 0,13 ms) und verbraucht nun 99,99 Prozent weniger RAM (nun 7,01 KB statt 2 GB RAM) [13].
Verbesserungen in Windows Forms und WPF
In Windows Forms in .NET 10.0 [14] sind nun einige Überladungen der GetData()-Methode für die Zwischenablage sowie Drag-and-Drop-Operationen als [Obsolet] markiert, die noch die Klasse BinaryFormatter verwenden, die Microsoft in .NET 9.0 ausgebaut hat und für die man nun ein eigenes NuGet-Paket braucht. Dafür gibt es nun neue Operationen, die mit JSON arbeiten, z. B. SetDataAsJson() und TryGetData().
In WPF gibt es laut Release Notes zu Preview 1 [15] und Preview 2 [16] nur Leistungsverbesserungen und Bug Fixes, u. a. am in .NET 9.0 eingeführten Fluent Design für WPF.
System.Text.Json: GetPropertyCount() in der Klasse JsonElement
In der JSON-Bibliothek System.Text.Json hat Microsoft die Klasse JsonElement um die Methode GetPropertyCount() erweitert, mit der man ermitteln kann, wie viele Eigenschaften ein JSON-Objekt besitzt:
JsonElement element1 = JsonSerializer.Deserialize<JsonElement>("""{ "ID" : 1, "Name" : "Dr. Holger Schwichtenberg", "Website": "www.IT-Visions.de" }"""); Console.WriteLine(element1.GetPropertyCount()); <em>// 3</em>
Bisher war nur möglich, die Anzahl der Objekte in einem Array zu ermitteln:
JsonElement element2 = JsonSerializer.Deserialize<JsonElement>("""[ 1, 2, 3, 4 ]"""); Console.WriteLine(element2.GetArrayLength()); <em>// 4</em>
System.Text.Json: RemoveRange() und RemoveAll() in JsonArray
In der Klasse JsonArray, im Namensraum System.Text.Json.Nodes, bietet Microsoft nun zwei Methoden RemoveRange() und RemoveAll()(Listing 6):
- RemoveRange() nimmt zwei Zahlen entgegen: Index und Count. Der Index ist wie üblich nullbasiert, d. h., array.RemoveRange(5, 2) entfernt das sechste und siebte Element.
- RemoveAll() erwartet ein Predicate-Objekt und erlaubt die Angabe eines Löschkriteriums, z. B. array.RemoveAll(n => n.GetValue() > 5);
System.Text.Json.Nodes.JsonArray array = JsonSerializer.Deserialize<System.Text.Json.Nodes.JsonArray>("""[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]"""); System.Console.WriteLine(array.Count); <em>// 9</em> PrintJsonArray(array); array.RemoveRange(5, 2); PrintJsonArray(array); array.RemoveAll(n => n.GetValue<int>() > 5); PrintJsonArray(array);
System.Text.Json: Einstellungen für zirkuläre Referenzen
Die Annotation [JsonSourceGenerationOptions] für den Source Generator in der Bibliothek System.Text.Json erlaubt nun auch die Einstellung des Verhaltens bei zirkulären Referenzen auf Unspecified, _Preserve_oder IgnoreCycles, z. B.:
[JsonSourceGenerationOptions(ReferenceHandler = JsonKnownReferenceHandler.Preserve)]
ASP.NET Core: OpenAPI Specification (OAS) 3.1
In ASP.NET Core WebAPIs in .NET 10.0 werden Metadaten nun im Standard in der Version 3.1 – bisher Version 3.0 – der OpenAPI Specification (OAS) mit vollständiger Unterstützung des JSON-Schema-Standards 2020-12 [17] generiert. Die Umstellung von Version 3.0 auf Version 3.1 geht einher mit Breaking Changes [18].
Entwickler:innen können daher bei Bedarf im Startcode des ASP.NET-Core-Webservers die OAS-Version mit diesem Befehl zurückstufen:
builder.Services.AddOpenApi(options => { options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0; });
Wenn OAS-Dokumente im Rahmen des Build-Prozesses gesteuert werden, geht die Zurückstufung wie folgt in der Projektdatei (.csproj):
<OpenApiGenerateDocumentsOptions> --openapi-version OpenApi3_0 </OpenApiGenerateDocumentsOptions>
ASP.NET Core: OpenAPI-Dokumente in YAML
Die OAS-Dokumente in ASP.NET Core waren bisher immer im JSON-Format. ASP.NET Core 10.0 bietet seit Preview 1 alternativ auch das YAML-Format an, das kürzere Dokumente liefert (Abb. 6). YAML als Alternative zu JSON stellt man im Startcode ein, indem man bei der Methode MapOpenApi() einen Dokumentnamen angibt, der auf .yml oder .yaml endet:
app.MapOpenApi("/openapi/{documentName}.yml");
oder
app.MapOpenApi("/openapi/{documentName}.yaml");
Die YAML-Metadaten können Entwickler:innen aber noch nicht im Build-Prozess einstellen. Microsoft will diese Option in einer kommenden Preview-Version liefern.

Abb. 6: Ein OpenAPI-3.1-Dokument im JSON- (links) und kompakteren YAML-Format (rechts)
ASP.NET Core: OpenAPI-Dokumente mit XML-Kommentaren
Die von dem NuGet-Paket Microsoft.AspNetCore.OpenApi generierten OpenAPI-Metadaten für ein ASP.NET Core WebAPI können nun auch Informationen aus den XML-Kommentaren enthalten, die zu Klassen bzw. Methoden hinterlegt sind. Diese Integration können Entwickler:innen mit einem Eintrag in der Projektdatei aktivieren:
<PropertyGroup> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup>
Welche XML-Kommentare auf welche Weise in das OpenAPI-Dokument übernommen werden, steht leider nicht in den Release-Notes [19] und bedauerlicherweise ließ sich das auch nicht testen, da durch die Installation des .NET 10.0 Preview 2 SDK alle WebAPI-Projekte beim Start nur noch HTTP-Fehler 404 liefern, auch mit den aktuellsten Projektvorlagen neu angelegter Projekte. Das Problem trat auf zwei verschiedenen Testrechnern auf.
ASP.NET Core: Beschreibungstexte für Rückgabewerte
Bei Controller-basierten WebAPIs können Entwickler:innen nun in den Annotationen [ProducesAttribute], [ProducesResponseTypeAttribute] und [ProducesDefaultResponseType] jeweils Beschreibungstexte angeben, die auch im OAS-Dokument erscheinen:
[HttpGet(Name = "GetWeatherForecast")] [ProducesResponseType<IEnumerable<WeatherForecast>>(StatusCodes.Status200OK, Description = "The weather forecast for the next 5 days.")] public IEnumerable<WeatherForecast> Get() { ... }
Das geht in Minimal WebAPIs schon seit Version 7.0 mit dem Methodenaufruf WithDescription().
Blazor: Anpassung der Verbindungsproblemmeldungen bei Blazor Server
Blazor Server stellt einen modalen Dialog dar, falls ein Abbruch der WebSockets-Verbindung zwischen Webbrowser und Webserver erfolgt ist und eine Wiederaufnahme versucht wird. Diese Darstellung war bisher nicht anpassbar.
In der Projektvorlage Blazor Web App findet man seit .NET 10.0 Preview 2 eine neue Razor Component im Ordner _Layout mit Namen ReconnectModal.razor(Listing 7). Diese Komponente enthält die Standarddarstellung des modalen Fensters bei Verbindungsproblemen. Damit können Entwickler:innen in .NET 10.0 also Einfluss auf die Texte und die Darstellung in drei Fällen nehmen:
- Wiederherstellung wird versucht
- Wiederherstellung ist fehlgeschlagen und wird erneut versucht
- Wiederherstellung ist endgültig fehlgeschlagen
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20type%3D%22module%22%20src%3D%22%40Assets%5B%22Components%2FLayout%2FReconnectModal.razor.js%22%5D%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<script>" title="<script>" /> <dialog id="components-reconnect-modal" data-nosnippet> <div class="components-reconnect-container"> <div class="components-rejoining-animation" aria-hidden="true"> <div></div> <div></div> </div> <p class="components-reconnect-first-attempt-visible"> Rejoining the server... </p> <p class="components-reconnect-repeated-attempt-visible"> Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds. </p> <p class="components-reconnect-failed-visible"> Failed to rejoin.<br />Please retry or reload the page. </p> <button id="components-reconnect-button" class="components-reconnect-failed-visible"> Retry </button> </div> </dialog>
Zur Razor Component ReconnectModal.razor gehören auch eine CSS-Datei ReconnectModal.razor.css und eine JavaScript-Datei ReconnectModal.razor.js. Auch diese Dateien sind anpassbar. ReconnectModal.razor wird am Ende der Datei MainLayout.razor eingebunden via und kann auch komplett ersetzt werden.
Blazor: NavigateTo() behält Scrollposition
Die Methode NavigateTo() in der Blazor-Klasse _NavigationManager_führt den Nutzer bzw. die Nutzerin nicht mehr zum Beginn der Seite zurück, falls der Entwickler bzw. die Entwicklerin nur URL-Parameter an die Seite anhängt:
nav.NavigateTo(nav.GetUriWithQueryParameter("count", currentCount));
Ein Rücksprung an den Seitenanfang findet allerdings bei Angabe von true für forceLoad weiterhin statt
nav.NavigateTo(nav.GetUriWithQueryParameter("count", currentCount), forceLoad: true);
oder bei Änderungen eines Teils des relativen Pfades:
nav.NavigateTo("/counter/" + currentCount);
Blazor: Verbesserungen beim QuickGrid
Im Tabellensteuerelement können Entwickler:innen nun eine Methode angeben, die bei jeder Zeile aufgerufen wird, um die CSS-Klasse der Zeile individuell auf Basis der Daten zu setzen (Listing 8).
<QuickGrid RowClass="ApplyRowClass" ItemsProvider="@itemsProvider" TGridItem="BO.WWWings.Flight" Virtualize="true" OverscanCount="@overscanCount"> <PropertyColumn Property="@(p => p.FlightNo)" Title="FlugNr" Sortable="true" /> <PropertyColumn Property="@(p => p.Departure)" Title="Abflugort" Sortable="true" /> <PropertyColumn Property="@(p => p.Destination)" Title="Zielort" Sortable="true" /> <PropertyColumn Property="@(p => p.FlightDate)" Title="Datum" Format="dd.MM.yyyy" Sortable="true" /> </QuickGrid> @code { private string ApplyRowClass(BO.WWWings.Flight rowItem) => rowItem.FreeSeats < 10 ? "red" : null; }
Außerdem gibt es im QuickGrid-Steuerelement nun eine neue Methode CloseColumnOptionsAsync()(Listing 9), die Entwickler:innen aufrufen können, damit sich die Eingabe von Filterkriterien schließt, wenn der Filter aktiviert wird: Der Aufruf CloseColumnOptionsAsync() sorgt dafür, dass die geöffnete Filtereingabe sich automatisch wieder schließt, wenn der Nutzer bzw. die Nutzerin ein Eingabetaste drückt (Abb. 7).
<QuickGrid @ref="flightGrid" RowClass="ApplyRowClass" ItemsProvider="@itemsProvider" TGridItem="BO.WWWings.Flight" Virtualize="true" OverscanCount="@overscanCount"> <PropertyColumn Property="@(p => p.FlightNo)" Title="FlugNr" Sortable="true" /> <PropertyColumn Property="@(p => p.Departure)" Title="Abflugort" Sortable="true"> <ColumnOptions> <input type="search" @bind="departureFilter" placeholder="Filter by Departure" @bind:after="@(() => flightGrid.CloseColumnOptionsAsync())" /> </ColumnOptions> </PropertyColumn> <PropertyColumn Property="@(p => p.Destination)" Title="Zielort" Sortable="true" /> <PropertyColumn Property="@(p => p.FlightDate)" Title="Datum" Format="dd.MM.yyyy" Sortable="true" /> </QuickGrid>

Abb. 7: Geöffnete Filtereingabe soll sich durch CloseColumnOptionsAsync() automatisch schließen
Blazor: Fingerprinting für Blazors JavaScript-Datei
Die bei Blazor mitgelieferte JavaScript-Datei (je nach verwendeter Projektvorlage: blazor.web.js, blazor.server.js bzw. blazor.webassembly.js) erhält in .NET 10.0 die bereits in .NET 9.0 eingeführte Möglichkeit zur Komprimierung und einem Fingerabdruck des Inhalts im Dateinamen, der verhindert, dass alte Versionen aus dem Browsercache verwendet werden.
Entity Framework: Verbesserungen für ExecuteUpdate() und ExecuteUpdateAsync()
Bei den Methoden zum Massenaktualisieren, namentlich ExecuteUpdate() und ExecuteUpdateAsync(), bietet Entity Framework Core 10.0 nun eine bessere Übersichtlichkeit bei komplexeren Ausdrücken, indem nicht nur Expression-Lambdas, sondern auch Statement-Lambdas erlaubt sind. Anstelle von
var c0 = ctx.Flights.Where(f => f.FlightNo <= 100).ExecuteUpdate(s => s.SetProperty(f => f.FreeSeats, f => f.FreeSeats - 1) .SetProperty(f => f.Memo, f => (setMemo ? "Freie Plätze geändert am " + DateTime.Now : f.Memo)) );
können Entwickler:innen nun auch schreiben:
var c1 = ctx.Flights.Where(f => f.FlightNo <= 100).ExecuteUpdate(s => { s.SetProperty(f => f.FreeSeats, f => f.FreeSeats - 1); if (setMemo) { s.SetProperty(f => f.Memo, "Freie Plätze geändert am " + DateTime.Now); } });
Auch wenn die Release-Notes explizit nur von der asynchronen Methode _ExecuteUpdateAsync()_sprechen, funktioniert das Feature im Schnelltest auch mit der synchronen Variante ExecuteUpdate().
Entity Framework: RightJoin() und LeftJoin()
Die in .NET 10.0 neu eingeführten Operatoren LeftJoin() und RightJoin() werden auch in Entity Framework Core 10.0 bereits für Datenbankzugriffe unterstützt [20], [21]. Die folgenden Listings zeigen den Einsatz von RightJoin() und LeftJoin() bei Entity Framework Core 10.0 in Verbindung mit einer relationalen Datenbank. Auch in Entity Framework konnte man das Ergebnis von RightJoin() und LeftJoin() bisher schon via _GoupJoin()_und _SelectMany()_mit _DefaultIfEmpty()_erzielen.
Aus dem LINQ-Befehl mit LeftJoin() in Listing 10 entsteht der SQL-Befehl in Listing 11. Aus diesem LINQ-Befehl mit RightJoin() in Listing 12 entsteht der SQL-Befehl in Listing 13.
CUI.H2("Suche alle Flüge, zu denen es keinen Piloten gibt via LeftJoin()"); var ctx = new DA.WWWings.WwwingsV1EnContext(); var fluegeOhnePilot = ctx.Flights .LeftJoin( ctx.Pilots, f => f.PilotPersonId, p => p.PersonId, (f, p) => new { f.FlightNo, f.Departure, f.Destination, f.FlightDate, PilotId = f.PilotPersonId == null ? "n/a" : f.PilotPersonId.ToString(), p.Employee.Person.GivenName, p.Employee.Person.Surname, }).Where(x => x.Surname == null).Take(20).ToList(); foreach (var item in fluegeOhnePilot) { Console.WriteLine($"{item.FlightNo} {item.Departure}->{item.Destination} am {item.FlightDate}: Pilot {item.PilotId} {item.GivenName} {item.Surname}"); }
SELECT TOP(@p) [f].[FlightNo], [f].[Departure], [f].[Destination], [f].[FlightDate], CASE WHEN [f].[Pilot_PersonID] IS NULL THEN 'n/a' ELSE COALESCE(CONVERT(varchar(11), [f].[Pilot_PersonID]), '') END AS [PilotId], [p0].[GivenName], [p0].[Surname] FROM [Operation].[Flight] AS [f] LEFT JOIN [People].[Pilot] AS [p] ON [f].[Pilot_PersonID] = [p].[PersonID] LEFT JOIN [People].[Employee] AS [e] ON [p].[PersonID] = [e].[PersonID] LEFT JOIN [People].[Person] AS [p0] ON [e].[PersonID] = [p0].[PersonID] WHERE [p0].[Surname] IS NULL
var pilotenMitFlug = ctx.Flights .RightJoin( ctx.Pilots.OrderByDescending(x => x.PersonId).Take(3), f => f.PilotPersonId, p => p.PersonId, (f, p) => new { PilotId = p.PersonId, p.Employee.Person.GivenName, p.Employee.Person.Surname, Flight = f, f.Departure, f.Destination, <em>//Date = f.FlightDate == DateTime.MinValue ? "n/a" : f.FlightDate.ToShortDateString(), // TODO: LAUFZEITFEHLER. Prüfen in RTM!</em> }).OrderBy(x => x.PilotId).ToList(); foreach (var p in pilotenMitFlug) { Console.WriteLine($"Pilot #{p.PilotId} {p.GivenName} {p.Surname} fliegt " + (p.Flight != null ? $"Flug #{p.Flight?.FlightNo} {p.Flight.Departure}->{p.Flight.Destination} am {p.Flight.FlightDate}" : "bisher keinen Flug")); }
SELECT [p0].[PersonID] AS [PilotId], [p1].[GivenName], [p1].[Surname], [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] RIGHT JOIN ( SELECT TOP(@p) [p].[PersonID] FROM [People].[Pilot] AS [p] ORDER BY [p].[PersonID] DESC ) AS [p0] ON [f].[Pilot_PersonID] = [p0].[PersonID] INNER JOIN [People].[Employee] AS [e] ON [p0].[PersonID] = [e].[PersonID] INNER JOIN [People].[Person] AS [p1] ON [e].[PersonID] = [p1].[PersonID] ORDER BY [p0].[PersonID]
Es sei zudem explizit darauf hingewiesen, dass man die neuen Operatoren _RightJoin()_und LeftJoin() einschließlich der vorher schon vorhandenen Operatoren _Join()_und GroupJoin() nur für die Verbindung von Tabellen braucht, für die es im Objektmodell keine Navigationsbeziehung gibt. So kann man statt des aufwendigen RightJoin() bei einer vorhandenen Navigationsbeziehung im Objektmodell dasselbe Ausgabeergebnis mit einem Include() erreichen. In diesem Fall erhält man allerdings keine flache Liste mit Daten aus Pilot und Flug, sondern eine Objekthierarchie, daher zwei verschachtelte foreach-Schleifen (Listing 14). Entity Framework Core führt dabei nur Inner Joins aus (Listing 15).
CUI.H2("Gib zu den letzten drei angelegten Piloten alle Flüge aus via Navigation Property"); var pilotenMitFlug2 = ctx.Pilots.Include(p => p.Employee).ThenInclude(p => p.Person).Include(p => p.Flights).OrderByDescending(x => x.PersonId).Take(3).OrderBy(x => x.PersonId).ToList(); foreach (Pilot p in pilotenMitFlug2) { if (p.Flights.Count == 0) { Console.WriteLine($"Pilot #{p.PersonId} {p.Employee.Person.GivenName} {p.Employee.Person.Surname} fliegt bisher keinen Flug"); } else { foreach (var f in p.Flights) { Console.WriteLine($"Pilot #{p.PersonId} {p.Employee.Person.GivenName} {p.Employee.Person.Surname} fliegt Flug #{f.FlightNo} {f.Departure}->{f.Destination} am {f.FlightDate}"); } } }
SELECT [p0].[PersonID], [p0].[FlightHours], [p0].[FlightSchool], [p0].[LicenseDate], [p0].[LicenseType], [e].[PersonID], [e].[EmployeeNo], [e].[HireDate], [e].[Supervisor_PersonId], [p1].[PersonID], [p1].[Birthday], [p1].[City], [p1].[Country], [p1].[EMail], [p1].[GivenName], [p1].[Memo], [p1].[Photo], [p1].[Surname], [f].[FlightNo], [f].[Airline], [f].[Departure], [f].[Destination], [f].[FlightDate], [f].[FreeSeats], [f].[Memo], [f].[NonSmokingFlight], [f].[Pilot_PersonID], [f].[Seats], [f].[Timestamp] FROM ( SELECT TOP(@p) [p].[PersonID], [p].[FlightHours], [p].[FlightSchool], [p].[LicenseDate], [p].[LicenseType] FROM [People].[Pilot] AS [p] ORDER BY [p].[PersonID] DESC ) AS [p0] INNER JOIN [People].[Employee] AS [e] ON [p0].[PersonID] = [e].[PersonID] INNER JOIN [People].[Person] AS [p1] ON [e].[PersonID] = [p1].[PersonID] LEFT JOIN [Operation].[Flight] AS [f] ON [p0].[PersonID] = [f].[Pilot_PersonID] ORDER BY [p0].[PersonID], [e].[PersonID], [p1].[PersonID]
Entity Framework: weitere Verbesserungen für LINQ
Entity Framework Core bietet in Version 10.0 Preview 1 und 2 zwei Verbesserungen bei der Übersetzung von LINQ in SQL:
- Die .NET-Methode DateOnly.ToDateTime(timeOnly) wird nun in SQL übersetzt.
- Optimiert wurde die Übersetzung von Count() in ICollection.
.NET MAUI: ShadowTypeConverter
In .NET MAUI 10.0 Preview 2 können Entwickler:innen nun mit einer vereinfachten Syntax Schatten definieren:
<VerticalStackLayout BackgroundColor="#fff" Shadow="4 4 16 #000000 0.5" />
Dabei stehen die Zahlen in der Eigenschaft _Shadow_für Offset X, Offset Y, Radius, Farbe und Opazität. Zuvor musste man hier eigene Tags verwenden:
<VerticalStackLayout BackgroundColor="#fff" > <Shadow Brush="#000000" Offset="4,4" Radius="16" Opacity="0.5" /> </VerticalStackLayout>
.NET MAUI: Steuerung von Farben
Entwickler:innen können beim Steuerelement __nun neben OnColor auch _OffColor_festlegen:
<Switch OffColor="Red" OnColor="Green" />
Beim Steuerelement lässt sich die Farbe des Symbols steuern:
<SearchBar Placeholder="Search items..." SearchIconColor="Blue" />
.NET MAUI: Steuerung der Geschwindigkeit der Sprachausgabe
Bei Sprachausgabe mit der Schnittstelle Klasse TextToSpeech kann man nun die Geschwindigkeit via Eigenschaft Rate angeben (Listing 16).
IEnumerable<Locale> locales = await TextToSpeech.Default.GetLocalesAsync(); SpeechOptions options = new SpeechOptions() { Rate = 2.0f, <em>// 0.1 - 2.0</em> Pitch = 1.5f, <em>// 0.0 - 2.0</em> Volume = 0.75f, <em>// 0.0 - 1.0</em> Locale = locales.FirstOrDefault() }; await TextToSpeech.Default.SpeakAsync("Hallo aus .NET 10.0!", options);
.NET MAUI: Verbesserungen für Android
.NET MAUI unterstützt nun JDK-Version 21 und Android-Version 16 (Codename Baklava, API-Version 36). Die MAUI-Projektvorlage verwendet nun im Standard API-Version 24 (Nougat) statt API-Version 21 (Lollipop). Man kann aber weiterhin auf API-Version 21 zurückstufen, Microsoft empfiehlt aber in den Release-Notes [22], das nicht zu tun: „While API-21 is still supported in .NET 10, we recommend updating existing projects to API-24 in order to avoid unexpected runtime errors.”
Android-Projekte können Entwickler:innen nun mit dem Befehl dotnet run direkt von der Kommandozeile aus starten, z. B.:
<em>// Start auf Gerät (wenn nur ein Gerät angeschlossen ist)</em> dotnet run -p:AdbTarget=-d <em>// Start auf Emulator (wenn es nur einen gibt)</em> dotnet run -p:AdbTarget=-e <em>// Start auf einem bestimmten Emulator</em> dotnet run -p:AdbTarget="-s emulator-5554"
.NET MAUI: Verbesserungen für iOS, Mac Catalyst, macOS und tvOS
Beim Kompilieren für die Apple-Plattformen (iOS, Mac Catalyst, macOS, tvOS) erzeugt der Compiler nun automatisch Trimmer-Warnungen. Diese Warnungen kann man aber ausschalten mit:
<PropertyGroup> <SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings> </PropertyGroup>
.NET MAUI: weitere Neuerungen
Das Steuerelement gilt nun als veraltet. Entwickler:innen sollten verwenden. Ebenso ist die Mark-up-Erweiterung FontImageExtension veraltet. Man sollte jetzt das Tag __verwenden.
Breaking Changes
Wie in jeder .NET-Version der letzten Jahre üblich, wird es auch dieses Mal in .NET 10.0 wieder einige Breaking Changes gegenüber der Vorgängerversion 9.0 geben. Einige der Breaking Changes sind bereits in Preview 1 und 2 enthalten und unter [23] dokumentiert.
Dokumentation der Neuerungen
Der .NET-API-Browser mit der .NET-Klassendokumentation [24] ist bereits auf .NET 10.0 Preview 2.0 aktualisiert, sodass man dort die neuen Klassen und Methoden sieht.
Während letztes Jahr bei .NET 9.0 die Preview-Versionen nach Preview 1 nur noch auf GitHub angekündigt wurden, gibt es seit diesem Jahr wieder einen Blogeintrag zu jeder Preview-Version (Preview 1 [25], Preview 2 [26]). Die Blogbeiträge verlinken auf die Release-Notes-Dokumente auf GitHub. Zudem gibt es jeweils aber auch immer eine Ankündigung auf GitHub [27], die neben den Release-Notes auch auf „What’s New”-Dokumente in der Dokumentation verlinkt. Das ist aber nicht immer konsistent: In der Ankündigung zu Preview 1 auf GitHub [28] fehlt der Link auf die Release-Notes von Entity Framework Core [29] gänzlich.
API-Reviews und Unboxing auf YouTube
Das .NET-Entwicklungsteam überträgt seit einiger Zeit die Diskussionen bezüglich neuer APIs (API-Reviews) live im Netz auf YouTube. Nun gibt es diese API-Reviews auch öffentlich beim Entwicklungsteam von ASP.NET Core und Blazor [30]. Die Termine der API-Reviews findet man unter [31]; die Videos der Vergangenheit findet man auf dem YouTube-Kanal der .NET Foundation [32].
Zu jeder Preview gibt es jetzt auch neu einen Unboxing-Live-Stream auf YouTube. Die Aufzeichnung ist in den Blogbeiträgen verlinkt.
Weitere Pläne für .NET 10.0
Auf GitHub findet man Roadmaps für einzelne Bereiche von .NET 10.0, z. B. C# 14.0 [33] und ASP.NET Core und Blazor [34]. Demzufolge arbeitet das C#-Team an den Extension Types (der Verallgemeinerung der Extension Methods, sodass Entwickler:innen auch Eigenschaften und Konstruktoren ergänzen können), was eigentlich schon als Sprachfeature für C# 13.0 geplant war.
Die Pläne des ASP.NET-Core-Entwicklungsteams zeigen die Abbildungen 8 und 9. Ein großes Thema sind demnach die Verbesserungen der Authentifizierungsmöglichkeiten z. B. mit Passkeys und Demonstration of Proof of Possession (DPop) Authentication Tokens. Die Microsoft Identity Platform soll in die Blazor-Web-App-Projektvorlage integriert werden [35]. Minimal WebAPIs sollen auch Validierung mit Data Annotations unterstützen [36].
Der bisher schon verfügbare Persistent Component State zur Übergabe von Daten vom Prerendering bei Blazor soll nun deklarativ via Annotation [SupplyParameterFromPersistentComponentState] möglich sein [37]. Auch soll man in Blazor Server die Circuits zur späteren Wiederaufnahme persistieren können [38]. Blazor WebAssembly soll ein Performance-Profiling erhalten (Abb. 8).

Abb. 8: Geplante Verbesserungen für Blazor

Abb. 9: Geplante Verbesserungen für ASP.NET Core WebAPIs und den Kestrel-Webserver
Andere Entwicklungsteams haben ihre Roadmaps leider bisher nicht auf .NET 10.0 aktualisiert:
- .NET MAUI [39]
- Windows Forms [40]
- WPF [41]
Auch bei Entity Framework Core gibt es keine Roadmap für Version 10.0. Man kann aber natürlich auf GitHub nach dem Milestone filtern [42], findet dort aber auch viele kleinere Bugfixes in der Liste und kann auf den ersten Blick keine Strategie erkennen. Bei Entity Framework Core findet man zum Beispiel geplante Verbesserungen für komplexe Typen: Diese sollen zukünftig optional sein können [43] und ein JSON-Mapping unterstützen [44]. Damit wird es dann auch möglich, Mengen komplexer Typen zu haben [45].
Ausblick
Bis zum geplanten Erscheinen von .NET 10.0 im November 2025 werden noch fünf weitere Preview-Versionen und zwei Release-Candidate-Versionen erscheinen, über die ich selbstverständlich auch berichten werde.
Links & Literatur
[1] https://dotnet.microsoft.com/en-us/download/dotnet/10.0
[2] https://github.com/dotnet/csharplang/issues/7905
[3] https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-14
[6] https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md
[7] https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/10.0/default-workload-config
[8] https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/sdk.md
[9] https://github.com/dotnet/runtime/issues/111282
[11] https://github.com/dotnet/runtime/issues/80314
[12] https://github.com/dotnet/runtime/releases/tag/v10.0.0-preview.2.25163.2
[14] https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/winforms.md
[15] https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/wpf.md
[16] https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview2/wpf.md
[17] https://json-schema.org/specification-links#2020-12
[20] https://github.com/dotnet/efcore/issues/12793
[21] https://github.com/dotnet/efcore/issues/35367
[22] https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/dotnetmaui.md
[23] https://learn.microsoft.com/en-us/dotnet/core/compatibility/10.0
[24] https://learn.microsoft.com/en-us/dotnet/api/
[25] https://devblogs.microsoft.com/dotnet/dotnet-10-preview-1
[26] https://devblogs.microsoft.com/dotnet/dotnet-10-preview-2
[27] https://github.com/dotnet/announcements/issues
[28] https://github.com/dotnet/announcements/issues/344
[29] https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/efcore.md
[30] https://github.com/dotnet/aspnetcore/issues/60003
[32] https://www.youtube.com/@NETFoundation/streams
[33] https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md
[34] https://github.com/dotnet/aspnetcore/issues/59443
[35] https://github.com/dotnet/aspnetcore/issues/51202
[36] https://github.com/dotnet/aspnetcore/issues/46349
[37] https://github.com/dotnet/aspnetcore/issues/26794
[38] https://github.com/dotnet/aspnetcore/issues/30344
[39] https://github.com/dotnet/maui/wiki/Roadmap
[40] https://github.com/dotnet/winforms/blob/main/docs/roadmap.md
[41] https://github.com/dotnet/wpf/blob/main/roadmap.md
[42] https://github.com/dotnet/efcore/milestone/204
[43] https://github.com/dotnet/efcore/issues/31376