.NET 11.0 Release Candidate 2: Breaking Changes kurz vor Fertigstellung
Auch für den am 13. Oktober 2026 erschienenen Release Candidate 2 von .NET 11.0 verkündet Microsoft in der offiziellen Ankündigung eine „Go-Live“-Lizenz. Entwicklerinnen und Entwickler dürfen diese Version also produktiv einsetzen und erhalten Produktsupport über die offiziellen Kanäle von Microsoft.
Die Laufzeitumgebung trägt die Versionsnummer 11.0.0-rc.2.26504.105, das SDK die Versionsnummer 11.0.100-rc.2.26504.105. Microsoft unterstützt den Release Candidate 2 in der aktuellen Version 12308.306 von Visual Studio 2026 Insiders (dort ist das .NET 11.0-SDK weiterhin nicht direkt installierbar) sowie in Visual Studio Code mit der Erweiterung C# Dev Kit. Der Download ist kostenlos unter https://dotnet.microsoft.com/download/dotnet/11.0 verfügbar (siehe Abbildung 1).
Die Installation des .NET SDK 11.0 Release Candidate 2 (Abb. 1).
Neue Sprachfeatures für C# 15.0 gibt es in Release Candidate 2 nicht. Die Container-Images bleiben unverändert; bei Entity Framework Core und bei WPF beschränkt sich die neue Version auf Fehlerbehebungen.
Der Blogbeitrag zum Release Candidate 2 listet viele Neuerungen in Windows Forms, von denen die meisten allerdings schon in Preview 7 beziehungsweise im Release Candidate 1 verfügbar waren.
Dazu gehören insbesondere der Kiosk-Modus von Windows Forms, das Ereignis SystemVisualSettingsChanged und das Auslesen der Windows-Einstellungen via System.Windows.Forms.Application.SystemVisualSettings, die verzögerte Formularanzeige gegen Flackern mittels der Methode Application.SetDefaultFormRevealMode() und Eigenschaft Form.FormRevealMode sowie die Methode SuspendPainting() zum Aussetzen von Zeichnen und Layout. Nur die im Folgenden beschriebenen Neuerungen waren zuvor noch nicht enthalten.
Analyzer prüfen den Designer-Code bei Windows Forms
Bei Windows Forms basiert die Benutzeroberfläche nicht auf einer Markup-Sprache, sondern auf Programmcode in der Methode InitializeComponent() in der Designer-Datei (Formname.Designer.cs). Der WYSIWYG-Designer in Visual Studio führt diese Methode zur Entwicklungszeit aus und ändert den Programmcode bei Aktionen der Entwicklerin im Designer. Der Windows-Forms-Designer kann aber nur Code in InitializeComponent() verarbeiten, der den von Microsoft vorgegebenen Mustern folgt. Schleifen, Lambda-Ausdrücke oder Null-Coalescing-Operatoren führen dazu, dass sich ein Formular im Designer nicht mehr öffnen lässt oder der Designer Code beim nächsten Speichern verwirft.
Mit dem Release Candidate 2 liefert .NET 11.0 daher standardmäßig aktive Analyzer für C# und Visual Basic .NET, die nicht unterstützte Konstrukte bereits beim Kompilieren melden. Microsoft nennt als Motivation dafür Code-Generatoren und KI-generierten Code, der diese Regeln oft missachtet. Die Analyzer gelten allerdings aufgrund der Verankerung im .NET 11.0 SDK nur für Projekte ab .NET 11.0, nicht für das klassische .NET Framework. Die folgende Tabelle zeigt eine Auswahl der insgesamt 14 Regeln.
| Regel | Schweregrad | Gemeldetes Konstrukt |
| WFO2000 | Warnung | Property-Getter liefert bei jedem Aufruf ein neu erzeugtes Referenzobjekt |
| WFO3000 | Fehler | Schleifen, Verzweigungen, Ausnahmebehandlung, lock, using oder await in InitializeComponent() |
| WFO3001 | Fehler | Eigene Properties, Methoden oder verschachtelte Typen in der Designer-Datei |
| WFO3004 | Fehler | Collection Expressions im Designer-Code (nur C#) |
| WFO3006 | Fehler | Bedingungsoperator ?: bzw. If() mit drei Argumenten |
| WFO3007 | Fehler | Null-Coalescing (??, ??=) bzw. If() mit zwei Argumenten |
| WFO3009 | Warnung | Interpolierte Zeichenketten |
| WFO3010 | Fehler | Lambda-Ausdrücke und anonyme Methoden |
| WFO3012 | Fehler | AddHandler in InitializeComponent() statt Handles (nur Visual Basic) |
Einen automatischen Code-Fix gibt es nicht. Für bestehende Formulare, die bewusst von den Regeln abweichen, lässt sich der Schweregrad in einer .editorconfig-Datei gezielt pro Datei herabsetzen. Das macht den Code aber gegebenenfalls untauglich für den Designer.
[LegacyForm.Designer.cs]
dotnet_diagnostic.WFO3001.severity = warning
dotnet_diagnostic.WFO3006.severity = suggestion
Listing 1: Herabsetzen des Schweregrads für eine einzelne Designer-Datei in der Datei .editorconfig.
Neues Styling nur teilweise umgesetzt
Das in Preview 7 eingeführte und in Release Candidate 1 verbesserte modernisierte Styling über Control.VisualStylesMode = VisualStylesMode.Net11 hat Microsoft erneut überarbeitet. Die Rahmen der Steuerelemente TextBox, MaskedTextBox und RichTextBox zeigen nun zustandsabhängig an, ob das Steuerelement im Ruhezustand ist, ob sich die Maus darüber befindet und ob es den Fokus besitzt, deaktiviert oder schreibgeschützt ist. Die untere Rahmenkante wird dabei stärker betont und zeigt im Fokuszustand die Windows-Akzentfarbe. Zudem bleiben explizit für ein untergeordnetes Steuerelement gesetzte Werte (z.B. VisualStylesMode.Classic) nun erhalten, selbst wenn sich der Modus des übergeordneten Steuerelements ändert.
Die folgenden Screenshots (Abbildungen 2 und 3) zeigen das alte und neue Styling im Vergleich, jeweils im Light- und Dark-Mode. Man sieht, dass bei einer Schaltfläche in Standardgröße der Text abgeschnitten ist. Hier wird also nach dem Umschalten auf das neue Styling eine manuelle Änderung notwendig.
Altes und neues Styling im Light-Mode (Abb. 2).
Altes und neues Styling im Dark-Mode (Abb. 3).
Für ToolStrip, MenuStrip und DataGridView ist das neue Styling laut Microsoft aber noch nicht fertig. Diese Steuerelemente verwenden daher auch bei VisualStylesMode.Net11 standardmäßig weiterhin das klassische Rendering. Wer das moderne Rendering dennoch ausprobieren möchte, muss diese Schalter aktivieren:
AppContext.SetSwitch("System.Windows.Forms.ToolStripModernRendering", true);
AppContext.SetSwitch("System.Windows.Forms.DataGridViewModernRendering", true);
Das PropertyGrid einschließlich seiner Editoren bleibt in .NET 11.0 grundsätzlich beim klassischen Styling.
Neu in Release Candidate 2 ist zudem die Unterstützung im Visual Basic Application Framework: Die Ereignisargumentklasse ApplyApplicationDefaultsEventArgs sowie die Klasse WindowsFormsApplicationBase besitzen nun die Eigenschaften FormRevealMode und VisualStylesMode. Damit können auch Visual Basic .NET-Entwicklerinnen und -Entwickler die in Preview 7 eingeführten Darstellungsoptionen via Visual Basic Application Framework festlegen.
Begrenzte Anfangsparallelität bei HTTP/2
Bei einer neuen HTTP/2-Verbindung kennt der Client das vom Webserver erlaubte Maximum gleichzeitiger Streams erst, wenn das SETTINGS-Frame des Servers eingetroffen ist. Bis dahin geht .NET standardmäßig von 100 möglichen Streams aus. Server mit niedrigerem Limit können einen anfänglichen Ansturm von Anfragen ablehnen.
Die neue Eigenschaft InitialHttp2MaxConcurrentStreams der Klasse SocketsHttpHandler begrenzt die Anzahl der Streams bis zum Eintreffen des SETTINGS-Frames; danach gilt der vom Server mitgeteilte Wert. Der Mindestwert ist 1.
var handler = new SocketsHttpHandler
{
InitialHttp2MaxConcurrentStreams = 10 // NEU in .NET 11.0
};
using var client = new HttpClient(handler) { DefaultRequestVersion = HttpVersion.Version20 };
Listing 2: Begrenzung der Anfangsparallelität auf zehn HTTP/2-Streams.
Validierungsmeldungen mit eigenen Platzhaltern
Die Basisklasse für Validierungsannotationen System.ComponentModel.DataAnnotations.ValidationAttribute besitzt ab Release Candidate 2 die neue Methode FormatMessage(string format, string name). Damit können Entwicklerinnen und Entwickler einer Validierungsannotation eine Meldungsvorlage als eine mit Platzhaltern versehene Zeichenkette übergeben. Die Validierungsannotation selbst füllt die Platzhalter. Beim RangeAttribute sind das z.B. die Unter- und Obergrenze:
var range = new RangeAttribute(1, 10);
string message = range.FormatMessage("{0} muss zwischen {1} und {2} liegen.", "Menge");
// Menge muss zwischen 1 und 10 liegen.
Diese Methode ersetzt in ASP.NET Core die in einer früheren Vorschauversion eingeführte Schnittstelle IValidationMessageFormatter, welche Microsoft in Release Candidate 2 wieder entfernt hat. Wer in selbst erstellten Validierungsannotationen zusätzliche Argumente in eine lokalisierte Fehlermeldung einfügen will, überschreibt nun FormatMessage().
Der Unterschied zur schon lange vorhandenen Methode FormatErrorMessage(string name) liegt in der Herkunft der Meldungsvorlage: FormatErrorMessage() verwendet immer die der Validierungsannotation hinterlegte Vorlage, also den Inhalt der Eigenschaft ErrorMessage bzw. die über ErrorMessageResourceType und ErrorMessageResourceName referenzierte Ressource. Der Aufrufer kann lediglich den Feldnamen übergeben. Eine Validierungsinfrastruktur, die ihre Meldungen aus einer eigenen Quelle bezieht (z.B. über einen IStringLocalizer), konnte die attributspezifischen Platzhalter wie {1} und {2} daher bisher nicht befüllen, ohne die Interna jedes einzelnen Attributs zu kennen.
Die neue Methode FormatMessage() trennt die Vorlage von der Validierungsannotation: Der Aufrufer liefert die Textvorlage, die Validierungsannotation steuert ihre Argumente bei. Die Basisimplementierung von FormatErrorMessage() ruft nun intern FormatMessage() mit der eigenen Vorlage auf. In einer selbst erstellten Validierungsannotation genügt es daher, FormatMessage() zu überschreiben; beide Wege liefern dann dieselben Platzhalterwerte, wie das folgende Listing zeigt.
public void RunFormatMessage()
{
var attr = new VielfachesVonAttribute(6, 7);
// Vorlage aus dem Attribut über FormatErrorMessage()
Console.WriteLine(attr.FormatErrorMessage("Anzahl")); // Standardtext mit dem übergebenen Namen: Anzahl muss ein Vielfaches von 6 oder 7 sein.
// Externe (z.B. lokalisierte) Vorlage über FormatMessage()
Console.WriteLine(attr.FormatMessage("{0} must be a multiple of {1} or {2}.", "Quantity")); // Individueller Text mit gefüllten Platzhaltern: Quantity must be a multiple of 6 or 7.
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public sealed class VielfachesVonAttribute : ValidationAttribute
{
private const string DefaultErrorMessage = "{0} muss ein Vielfaches von {1} oder {2} sein.";
public int Teiler1 { get; }
public int Teiler2 { get; }
public VielfachesVonAttribute(int teiler1, int teiler2)
: base(DefaultErrorMessage)
{
Teiler1 = teiler1;
Teiler2 = teiler2;
}
public override bool IsValid(object? value)
{
return value is null || (value is int zahl && (zahl % Teiler1 == 0 || zahl % Teiler2 == 0));
}
// NEU in .NET 11.0: Vorlage kommt vom Aufrufer, das Attribut liefert {1} und {2}
public override string FormatMessage(string format, string name)
{
return string.Format(CultureInfo.CurrentCulture, format, name, Teiler1, Teiler2);
}
}
CBOR-Daten inkrementell lesen
Bereits seit dem Jahr 2020 gibt es von Microsoft das NuGet-Paket System.Formats.Cbor für das binäre Datenformat Concise Binary Object Representation (CBOR) nach RFC 8949, sowohl für das klassische .NET Framework als auch das moderne .NET. CBOR erweitert das Datenmodell von JSON, ist aber kompakter und schneller zu verarbeiten. Eingesetzt wird das Format vor allem dort, wo Bandbreite, Speicher oder Rechenleistung knapp sind oder Daten signiert werden (z.B. bei WebAuthn/FIDO2 (Passkeys), beim IoT-Protokoll CoAP nach RFC 7252, beim digitalen COVID-Zertifikat der EU, beim mobilen Führerschein nach ISO/IEC 18013-5 und bei der EUDI-Wallet für digitale Identitäten).
Die .NET-Klasse System.Formats.Cbor.CborReader kann Daten nun inkrementell verarbeiten. Dazu übergibt man im Konstruktor isFinalBlock: false. Meldet PeekState() den Zustand CborReaderState.NeedsMoreData, übergibt man der neuen Methode SlideData() die noch nicht gelesenen Restbytes zusammen mit dem nächsten Datenblock. Ein großes Dokument muss somit nicht mehr vorab vollständig in einen zusammenhängenden Speicherbereich kopiert werden. Ohne CborConformanceMode.Lax werden nicht abgeschlossene Blöcke zurückgewiesen.
// CBOR-Map: { "name": "Dr. Holger Schwichtenberg", "Jahr": 1972, "tags": ["C#", "TypeScript"] }
// Verteilt auf vier Blöcke, wie sie z.B. nacheinander aus einem Stream eintreffen.
// Block 1 endet bewusst mitten im Namen, um das inkrementelle Lesen zu zeigen.
ReadOnlyMemory<byte>[] blocks =
{
new byte[] {
0xA3, // Map (3 Einträge)
0x64, 0x6E, 0x61, 0x6D, 0x65, // "name"
0x78, 0x19, // Text mit 25 UTF-8-Bytes
0x44, 0x72, 0x2E, 0x20, 0x48, 0x6F, 0x6C, 0x67, 0x65, 0x72 // "Dr. Holger" (Teil 1)
},
new byte[] {
0x20, 0x53, 0x63, 0x68, 0x77, 0x69, 0x63, 0x68, 0x74, 0x65,
0x6E, 0x62, 0x65, 0x72, 0x67, // " Schwichtenberg" (Teil 2)
0x64, 0x4A, 0x61, 0x68, 0x72, // "Jahr"
0x19, 0x07, 0xB4 // 1972
},
new byte[] {
0x64, 0x74, 0x61, 0x67, 0x73, // "tags"
0x82, // Array (2 Elemente)
0x62, 0x43, 0x23 // "C#"
},
new byte[] {
0x6A, 0x54, 0x79, 0x70, 0x65, 0x53,
0x63, 0x72, 0x69, 0x70, 0x74 // "TypeScript"
}
};
int next = 1;
ReadOnlyMemory<byte> current = blocks[0];
var reader = new CborReader(current,
new CborReaderOptions { ConformanceMode = CborConformanceMode.Lax },
isFinalBlock: false); // NEU in .NET 11.0
// Liefert so lange Blöcke nach, bis genug Daten für das nächste Element vorliegen.
// SlideData() ersetzt den Puffer: Noch ungelesene Bytes des alten Puffers
// müssen daher vor den neuen Block gesetzt werden.
void EnsureData()
{
while (reader.PeekState() == CborReaderState.NeedsMoreData)
{
if (next >= blocks.Length)
throw new InvalidOperationException("CBOR-Daten unvollständig.");
ReadOnlyMemory<byte> rest = current[^reader.BytesRemaining..];
byte[] combined = new byte[rest.Length + blocks[next].Length];
rest.CopyTo(combined);
blocks[next].CopyTo(combined.AsMemory(rest.Length));
current = combined;
reader.SlideData(current, isFinalBlock: next == blocks.Length - 1); // NEU in .NET 11.0
next++;
}
}
string? name = null;
int jahr = 0;
var tags = new List<string>();
EnsureData();
int? count = reader.ReadStartMap(); // 3
for (int i = 0; i < count; i++)
{
EnsureData();
string key = reader.ReadTextString();
EnsureData();
switch (key)
{
case "name":
name = reader.ReadTextString();
break;
case "Jahr":
jahr = reader.ReadInt32();
break;
case "tags":
reader.ReadStartArray();
EnsureData();
while (reader.PeekState() != CborReaderState.EndArray)
{
tags.Add(reader.ReadTextString());
EnsureData();
}
reader.ReadEndArray();
break;
default:
reader.SkipValue();
break;
}
}
EnsureData();
reader.ReadEndMap();
Console.WriteLine($"{name} {jahr}: {string.Join(", ", tags)}"); // Dr. Holger Schwichtenberg 1972: C#, TypeScript
Listing 4: Inkrementelles Lesen einer CBOR-Map in vier Blöcken.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.