Politur an allen Ecken und Enden

.NET 9.0 Preview 3 und 4

.NET 9.0 Preview 3 und 4

Politur an allen Ecken und Enden

.NET 9.0 Preview 3 und 4


Auch in Preview 3 und 4 von .NET 9.0 zeigen sich keine großen, alles andere überragenden neuen Features wie es sie in .NET 8.0 für Blazor gab. Vielmehr ergänzt Microsoft viele nützliche Kleinigkeiten in C#, der Basisklassenbibliothek, ASP.NET Core, Blazor, Entity Framework Core und sogar für WPF gibt es mal wieder etwas Neues.

Nach Preview 1 am 13. Februar 2024 und Preview 2 am 12. März 2024 folgten für .NET 9.0 die Preview 3 am 11. April 2024 und Preview 4 am 21. Mai 2024. Preview 4 erschien im Rahmen der Build-Konferenz 2024 (Kasten: „Wenig .NET 9.0 auf der Build 2024“). Die Vorschauversionen stehen zum kostenfreien Download bereit unter [1].

Als Visual-Studio-Version braucht man 17.11 Preview. Microsoft hat im Rahmen der Build Version 17.10 zur stabilen Version gemacht und mit der Vorschauversion auf 17.11 begonnen. .NET 9.0 soll im November 2024 erscheinen. .NET 9.0 wird dabei als ungerade Versionsnummer nur den kurzen Standard-Term-Support (STS) für 18 Monate erhalten, also nur bis Mai 2026, während der Support für das Ende 2023 erschienene .NET 8.0 mit Long-Term-Support (LTS) noch bis November 2026 läuft.

Wenig .NET 9.0 auf der Build 2024

.NET 9.0 Preview 4 ist im Rahmen der diesjährigen Microsoft-Build-Entwicklerkonferenz (vom 21. bis 23. Mai 2024) erschienen. Wie 2023 fand die Konferenz in Seattle und online statt, wobei nicht alle Vorträge live aus Seattle gestreamt wurden.

Ein großes Thema auf der Konferenz war .NET 9.0 leider nicht. In den Keynotes wurde .NET 9.0 gar nicht erwähnt, dort ging es ausschließlich um künstliche Intelligenz. Es gab aber mehrere Vorträge, in denen .NET 9.0 zumindest am Ende behandelt wurde, die Mehrheit davon wurden aber vorher aufgezeichnet:

  • Im Beitrag „Modern Full-Stack Web Development with ASP.NET Core & Blazor“ redet Daniel Roth leider die ersten 21 von 41 Minuten über Blazor 8.0 aus dem letzten November. Lediglich die restlichen rund 20 Minuten widmet er neuen Features in Blazor 9.0 [2].

  • Auch im Beitrag „EF Core 9: Evolving Data Access in .NET“ beginnen die Informationen zu Version 9.0 erst in Minute 26 und dann geht es bei Shay Rojansky ausschließlich um Native AOT in Verbindung mit Entity Framework Core, was in Version 8.0 ja noch nicht möglich war [3]. Die ersten 26 Minuten redet Arthur Vickers allgemein über Entity Framework Core und dessen Verhalten in Verbindung mit verschiedenen Datenbankmanagementsystemen. Dabei hat er auch James Kovacs (Director of Engineering in Database Experience MongoDB) als Gast zugeschaltet, denn Anfang Mai 2024 ist die erste stabile Version des MongoDB-Treibers für Entity Framework Core erschienen.

  • Klaus Loeffelmann und Merrie McGaw redeten in „What’s New with WinForms in .NET 9?“ auch erst ab Minute 38 von 60 über .NET 9.0 [4]. Hier gab es am Ende eine beeindruckende KI-Demonstration in Windows-Forms-Steuerelementen.

  • Auch im Beitrag „Enhancing .NET MAUI: Quality, Performance, and Interoperability in .NET 9“ [5] handelten die ersten 30 Minuten von Allgemeinem zu MAUI sowie Version 8.0 statt 9.0

  • Um die Wahl sowie die Integration zwischen WPF und WinUI 3 ging es live. In dem Vortrag gab es einige Ankündigungen für das Windows App SDK 1.6, das im September erscheinen soll [6].

  • Live mit Aufzeichnung redeten Dustin Campbell und Mads Torgersen über die nächste Version von C# im Vortrag „What’s new in C# 13“. Dabei wurde aber nur ein Teil der geplanten Features von C# 13.0 behandelt [7].

  • Zwei Live-Beiträge mit Aufzeichnung gab es zu .NET Aspire, das im Rahmen der Build erstmals als stabile Version erschienen ist: „Demystify cloud-native development with .NET Aspire“ [8] und „.NET Aspire development on any OS with the Visual Studio family“ [9].

Ein Beitrag zu den geplanten Verbesserungen der Runtime und Basisklassenbibliothek in .NET 9.0 war im Session-Katalog der Build 2024 nicht auffindbar [10].

.NET SDK: Terminal Logger ist Standard und verbessert

In .NET 8.0 hatte Microsoft einen neuen Terminal Logger für bessere Ausgaben bei den Kommandozeilenwerkzeugen dotnet und msbuild eingeführt. Zu den Verbesserungen gehören prägnantere Ausgaben, farbige Ausgaben für Fehler und Warnungen, anklickbare Hyperlinks und sich aktualisierende Zeitangaben. In .NET 8.0 musste Entwicklerinnen und Entwickler den Terminal Logger stets noch manuell mit dem Kommandozeilenparameter /tl aktivieren, z. B. dotnet build /tl und msbuild /tl.

In .NET 9.0 ist der Terminal Logger nun Standard bei den Befehlen dotnet build, dotnet msbuild, dotnet clean, dotnet pack, dotnet publish, dotnet restore und dotnet test – sofern das moderne Windows Terminal (wt.exe) verwendet wird und nicht die alte Windows-Konsole (conhost.exe). Entwicklerinnen und Entwickler können aber weiterhin die alte Ausgabe mit dem Parameter --tl:off erzwingen (Abb. 1). Alternativ dazu kann man die Umgebungsvariable MSBUILDTERMINALLOGGER auf den Wert off setzen. Beim direkten Aufruf von MSBuild.exe ist aber ohne den Parameter /tl auch im .NET 9.0 SDK weiterhin der klassische Logger aktiv.

schwichtenberg_preview_1

Abb. 1: Terminal Logger in .NET 9.0

Zusätzlich zeigt der Terminal Logger in .NET 9.0 (seit Preview 3) gegenüber der Version in .NET 8.0 nun am Ende seiner Ausgabe eine Zusammenfassung der Anzahl der Fehler und Warnungen an (Abb. 1). Außerdem stellt er nun Meldungen mit Zeilenumbrüchen besser dar: ohne Wiederholung der Informationen über Projekt und Ort, siehe Abbildung 2 und 3 im Vergleich.

schwichtenberg_preview_2

Abb. 2: Warnung mit Umbrüchen im Terminal Logger in .NET 8.0 (Bildquelle: Microsoft)

schwichtenberg_preview_3

Abb. 3: Warnung mit Umbrüchen im Terminal Logger in .NET 9.0 (Bildquelle: Microsoft)

.NET Runtime: Schnellere Laufzeitfehler

Es ist seit vielen Jahren bekannt, dass die .NET-Laufzeitumgebung beim Behandeln von Laufzeitfehlern relativ langsam ist. Aus diesem Grund gehört die Vermeidung von Laufzeitfehlern zu den Best Practices für .NET-Entwicklerinnen und -Entwickler. Insbesondere sollte man Laufzeitfehler nicht als Ersatz für Kontrollflussanweisungen verwenden, um z. B. bei ungültigen Werten in einer Importdatei eine Schleife oder Unterroutine zu verlassen.

Microsoft hat aber laut eigener Aussage in den Release Notes zu Preview 3 der .NET 9.0 Runtime die Behandlung von Laufzeitfehlern um den Faktor 2 bis 4 gesteigert [11]. Die Aussage gelte für die Plattformen Windows x64, Windows Arm64, Linux x64 und Linux Arm64, aber nicht für ein 32-Bit-Windows. Entwicklerinnen und Entwickler können via Umgebungsvariable DOTNET_LegacyExceptionHandling = 1 die Laufzeitumgebung oder in der Projektdatei via <RuntimeHostConfigurationOption Include="System.Runtime.LegacyExceptionHandling" Value="true" /> die .NET-Laufzeitumgebung dazu zwingen, das alte, langsamere Fehlerbehandlungsverfahren einzusetzen. Solche Fallbacks hat Microsoft auch bei anderen fundamentalen Änderungen an der Laufzeitumgebung für den Fall bereitgestellt, dass es zu Verhaltensabweichungen kommt. Wir erinnern uns an den erst nach der RTM-Version festgestellten RyuJIT-Bug in .NET-Framework 4.6 [12].

C# 13.0: Mengentypen bei params

Seit der ersten Version von C# gibt es Parameterarrays für sogenannte variadische Parameter, mit denen eine Methode eine beliebig lange Liste von Parametern eines Typs empfangen kann, wenn das mit dem Schlüsselwort params eingeleitet wird (Listing 1).

Listing 1

public void MethodeMitbeliebigVielenParametern_Alt(string text, params int[] args)
{
  CUI.H2(nameof(MethodeMitbeliebigVielenParametern_Alt));
  CUI.Print(text + ": " + args.Length);
  foreach (var item in args)
  {
    CUI.LI(item);
  }
}

Diese Methode kann man beispielsweise so aufrufen:

MethodeMitbeliebigVielenParametern_Alt("Anzahl Zahlen", 1, 2, 3);
MethodeMitbeliebigVielenParametern_Alt("Number of numbers", 1, 2, 3, 4);

Neu in C# 13.0 ist, dass statt eines Arrays bei den Parametern auch generische Mengentypen verwendet werden dürfen, z. B. List<T> (Listing 2).

Listing 2

public void MethodeMitbeliebigVielenParametern_Neu(string text, params List<int> args)
{
  CUI.H2(nameof(MethodeMitbeliebigVielenParametern_Neu));
  CUI.Print(text + ": " + args.Count);  // statt args.Length
  foreach (var item in args)
  {
    CUI.LI(item);
  }
}

Analog ist der Aufruf dann genauso flexibel möglich wie beim Parameterarray:

MethodeMitbeliebigVielenParametern_Neu("Anzahl Zahlen", 1, 2, 3);
MethodeMitbeliebigVielenParametern_Neu("Number of numbers", 1, 2, 3, 4);

Voraussetzung (für dieses und alle anderen C#-13.0-Sprachfeatures) ist derzeit, dass man in der Projektdatei den Tag <LangVersion>preview</LangVersion> setzt. Dann sind diese generischen Mengentypen bei params in C# 13.0 erlaubt:

  • System.Collections.Generic.IEnumerable<T>

  • System.Collections.Generic.IReadOnlyCollection<T>

  • System.Collections.Generic.IReadOnlyList<T>

  • System.Collections.Generic.ICollection<T>

  • System.Collections.Generic.IList<T>

  • alle Klassen, die System.Collections.Generic.IEnumerable<T> implementieren

  • System.Span<T>

  • System.ReadOnlySpan<T>

In kommenden Preview-Versionen will Microsoft zahlreiche Klassen aus der .NET-Klassenbibliothek, die Parameterarrays verwenden (z. B. String.Format(), Console.WriteLine(), APIs im Namensraum System.Drawing), mit zusätzlichen Methodenüberladungen für System.ReadOnlySpan<T> anbieten. Das vermeidet die bei Arrays üblichen, langsameren Heap-Allokationen, da System.ReadOnlySpan<T> auf dem Stack lebt.

C# 13.0: neue Abkürzung für das Escape-Zeichen

Mit den uralten VT100/ANSI-Escape-Codes kann man auch heute noch in Konsolenanwendungen zahlreiche Formatierungen auslösen, z. B. 24-Bit-Farben, Fettschrift, Unterstreichen, Durchstreichen, Blinken usw. Die Escape-Codes werden durch das Escape-Zeichen (ACSII-Zeichen 27, hexadezimal: 0x1b) eingeleitet. Bisher konnte man das Escape-Zeichen in .NET-Konsolenanwendungen bei Console.WriteLine() nur umständlich über \u001b, \U0000001b oder \x1b ausdrücken, wobei Letzteres nicht empfohlen ist: „Wenn Sie die Escapesequenz \x verwenden, weniger als vier Hexadezimalziffern angeben und es sich bei den Zeichen, die der Escapesequenz unmittelbar folgen, um gültige Hexadezimalziffern handelt (z. B. 0-9, A-F und a-f), werden diese als Teil der Escapesequenz interpretiert. \xA1 erzeugt beispielsweise ‚¡‘ (entspricht dem Codepunkt U+00A1). Wenn das nächste Zeichen jedoch ‚A‘ oder ‚a‘ ist, wird die Escapesequenz stattdessen als \xA1A interpretiert und ‚ਚ‘ erzeugt (entspricht dem Codepunkt U+0A1A). In solchen Fällen können Fehlinterpretationen vermieden werden, indem Sie alle vier Hexadezimalziffern (z. B. \x00A1) angeben“ [13].

Typischerweise sahen Ausgaben mit VT100/ANSI-Escape-Codes dann aus wie in Listing 3. In C# 13.0 führt Microsoft nun \e als Kurzform für das Escape-Zeichen ein, sodass die Zeichenfolgen deutlich kompakter und übersichtlicher werden (Listing 4). Abbildung 4 zeigt das Ergebnis, das sowohl Listing 3 als auch Listing 4 produziert.

Listing 3: Bisherige VT100/ANSI-Escape-Codes

Console.WriteLine("This is a regular text");
Console.WriteLine("\u001b[1mThis is a bold text\u001b[0m");
Console.WriteLine("\u001b[2mThis is a dimmed text\u001b[0m");
Console.WriteLine("\u001b[3mThis is an italic text\u001b[0m");
Console.WriteLine("\u001b[4mThis is an underlined text\u001b[0m");
Console.WriteLine("\u001b[5mThis is a blinking text\u001b[0m");
Console.WriteLine("\u001b[6mThis is a fast blinking text\u001b[0m");
Console.WriteLine("\u001b[7mThis is an inverted text\u001b[0m");
Console.WriteLine("\u001b[8mThis is a hidden text\u001b[0m");
Console.WriteLine("\u001b[9mThis is a crossed-out text\u001b[0m");
Console.WriteLine("\u001b[21mThis is a double-underlined text\u001b[0m");
Console.WriteLine("\u001b[38;2;255;0;0mThis is a red text\u001b[0m");
Console.WriteLine("\u001b[48;2;255;0;0mThis is a red background\u001b[0m");
Console.WriteLine("\u001b[38;2;0;0;255;48;2;255;255;0mThis is a blue text with a yellow background\u001b[0m");

Listing 4: Etwas übersichtlichere VT100/ANSI-Escape-Codes mit der neuen Abkürzung \e in C# 13.0

Console.WriteLine("This is a regular text");
Console.WriteLine("\e[1mThis is a bold text\e[0m");
Console.WriteLine("\e[2mThis is a dimmed text\e[0m");
Console.WriteLine("\e[3mThis is an italic text\e[0m");
Console.WriteLine("\e[4mThis is an underlined text\e[0m");
Console.WriteLine("\e[5mThis is a blinking text\e[0m");
Console.WriteLine("\e[6mThis is a fast blinking text\e[0m");
Console.WriteLine("\e[7mThis is an inverted text\e[0m");
Console.WriteLine("\e[8mThis is a hidden text\e[0m");
Console.WriteLine("\e[9mThis is a crossed-out text\e[0m");
Console.WriteLine("\e[21mThis is a double-underlined text\e[0m");
Console.WriteLine("\e[38;2;255;0;0mThis is a red text\e[0m");
Console.WriteLine("\e[48;2;255;0;0mThis is a red background\e[0m");
Console.WriteLine("\e[38;2;0;0;255;48;2;255;255;0mThis is a blue text with a yellow background\e[0m");
schwichtenberg_preview_4

Abb. 4: Die Ausgabe von Listing 3 und 4 sieht gleich aus

C# 13.0: neue Lock-Klasse für lock-Statements

Ab .NET 9.0 gibt es für das Sperren von Codeblöcken vor dem Zugriff durch weitere Threads eine neue Klasse System.Threading.Lock, die man nun im Standard in Verbindung mit dem lock-Statement in C# verwenden sollte, „for best performance“, wie Microsoft in der Dokumentation schreibt [14].

Listing 5 zeigt ein Beispiel mit lock und System.Threading.Lock. In den derzeitigen Preview-Versionen muss man die Klasse System.Threading.Lock noch in der Projektdatei explizit aktivieren mit: <EnablePreviewFeatures>True</EnablePreviewFeatures>.

Der C#-13.0-Compiler macht dann aus

lock (_balanceLock)
{
  _balance += amount;
}

einen Aufruf der EnterScope()-Methode in der Klasse System.Threading.Lock:

using (_balanceLock.EnterScope())
{
  _balance += amount;
}

Listing 5: Ein lock in C# 13.0 mit der neuen Klasse System.Threading.Lock [15]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace NET9_Console.CS13;
using System;
using System.Threading.Tasks;
 
public class Account
{
  // Vor C# 13.0 wurde hier object verwendet statt System.Threading.Lock 
  private readonly System.Threading.Lock _balanceLock = new();
  private decimal _balance;
  
  public Account(decimal initialBalance) => _balance = initialBalance;
  
  public decimal Debit(decimal amount)
  {
    if (amount < 0)
    {
      throw new ArgumentOutOfRangeException(nameof(amount), "The debit amount cannot be negative.");
    }

    decimal appliedAmount = 0;
    lock (_balanceLock)
    {
      if (_balance >= amount)
      {
        _balance -= amount;
        appliedAmount = amount;
      }
    }
    return appliedAmount;
  }
  
  public void Credit(decimal amount)
  {
    if (amount < 0)
    {
      throw new ArgumentOutOfRangeException(nameof(amount), "The credit amount cannot be negative.");
    }

    lock (_balanceLock)
    {
      _balance += amount;
    }
  }

  public decimal GetBalance()
  {
    lock (_balanceLock)
    {
      return _balance;
    }
  }
}

class AccountTest
{
  static async Task Main()
  {
    var account = new Account(1000);
    var tasks = new Task[100];
    for (int i = 0; i < tasks.Length; i++)
    {
      tasks[i] = Task.Run(() => Update(account));
    }
    await Task.WhenAll(tasks);
    Console.WriteLine($"Account's balance is {account.GetBalance()}");
    // Output:
    // Account's balance is 2000
  }
  
  static void Update(Account account)
  {
    decimal[] amounts = [0, 2, -3, 6, -2, -1, 8, -5, 11, -6];
    foreach (var amount in amounts)
    {
      if (amount >= 0)
      {
        account.Credit(amount);
      }
      else
      {
        account.Debit(Math.Abs(amount));
      }
    }
  }
}

Basisklassenbibliothek: mehr Genauigkeit für TimeSpan

Die Datenstruktur System.TimeSpan gibt es schon seit der ersten Version des .NET Framework aus dem Jahr 2002. Nun in .NET 9.0 adressiert Microsoft eine kleine Herausforderung, die es in all den Jahren gab: Die Konvertierungsmethoden FromMicroseconds(), FromSeconds(), FromMinutes(), FromHours() und FromDays() erwarten als Parameter einen Double-Wert, der als Fließkommazahl aber ungenau ist. Microsoft führt daher in .NET 9.0 nun zusätzlich neue Überladungen dieser Methoden ein, die Ganzzahlen als Parameter erwarten:

  • public static TimeSpan FromDays(int days);

  • public static TimeSpan FromDays(int days, int hours = 0, long minutes = 0, long seconds = 0, long milliseconds = 0, long microseconds = 0);

  • public static TimeSpan FromHours(int hours);

  • public static TimeSpan FromHours(int hours, long minutes = 0, long seconds = 0, long milliseconds = 0, long microseconds = 0);

  • public static TimeSpan FromMinutes(long minutes);

  • public static TimeSpan FromMinutes(long minutes, long seconds = 0, long milliseconds = 0, long microseconds = 0);

  • public static TimeSpan FromSeconds(long seconds);

  • public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0);

  • public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0);

  • public static TimeSpan FromMicroseconds(long microseconds);

Das folgende Beispiel beweist die größere Genauigkeit der neuen Überladungen:

// bisher
TimeSpan timeSpan1 = TimeSpan.FromSeconds(value: 101.832);
Console.WriteLine($"timeSpan1 = {timeSpan1}"); // timeSpan1 = 00:01:41.8319999
 
// neu
TimeSpan timeSpan2 = TimeSpan.FromSeconds(seconds: 101, milliseconds: 832);
Console.WriteLine($"timeSpan2 = {timeSpan2}"); // timeSpan2 = 00:01:41.8320000

Basisklassenbibliothek: PersistedAssemblyBuilder anstelle von AssemblyBuilder

In .NET 9.0 Preview 1 (siehe Bericht im Windows Developer 6.2024) hatte Microsoft die aus dem klassischen .NET Framework bekannte Möglichkeit wieder eingeführt, dynamisch zur Laufzeit erstellte Assemblies im Dateisystem oder einem beliebigen Stream zu persistieren. In Preview 3 hat Microsoft das API aber nun geändert: Anstelle der zuvor verwendeten Klasse AssemblyBuilder

AssemblyBuilder ab = AssemblyBuilder.DefinePersistedAssembly(new AssemblyName("Math"), typeof(object).Assembly);

nutzt man nun die neue Klasse PersistedAssemblyBuilder:

PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(new AssemblyName("Math"), typeof(object).Assembly);

Listing 6 zeigt dazu ein Beispiel. Weitere Beispiele zur Anpassung der Metadaten, wie z. B. dem Einstiegspunkt, findet man in den Release Notes [16].

Listing 6: Beispiel zum Einsatz von PersistedAssemblyBuilder

string assemblyPath = Path.Combine(System.AppContext.BaseDirectory, "Math.dll");
 
PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(new AssemblyName("Math"), typeof(object).Assembly);
TypeBuilder tb = ab.DefineDynamicModule("MathModule").DefineType("MathUtil", TypeAttributes.Public | TypeAttributes.Class);

MethodBuilder mb = tb.DefineMethod("Sum", MethodAttributes.Public | MethodAttributes.Static, typeof(int), [typeof(int), typeof(int)]);
ILGenerator il = mb.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ret);

tb.CreateType();
Console.WriteLine("Speichere Assembly unter: " + assemblyPath);
ab.Save(assemblyPath); // Speichern ins Dateisystem oder einen Stream

Entity Framework Core: typischere Erstellung von Hierarchie-IDs

In Entity Framework Core 8.0, erschienen zusammen mit .NET 8.0 im November 2023, hatte Microsoft die Unterstützung für hierarchische Datentabellen in Microsoft SQL Server mit dem Spaltentyp hierarchyid eingeführt. Seitdem gibt es eine korrespondierende .NET-Klasse Microsoft.EntityFrameworkCore.HierarchyId.

In .NET 9.0 (seit Preview 3) gibt es in der Klasse HierarchyId eine kleine Verbesserung in Form einer neue Überladung der Methode Parse(): Entwicklerinnen und Entwickler können HierarchyId-Instanzen nicht nur wie bisher aus einer Zeichenkette (z. B. "/4/1/3/1/2/"), sondern auch typsicherer auf Basis einer anderen HierarchyId-Instanz und Ganzzahlen erstellen (Listing 7).

Listing 7: Einsatz der neuen Überladungen der Methode Parse() in der Klasse HierarchyId

for (int j = 1; j < 5; j++)
{
  // alt
  var level2 = $"{startLevel}{i}/{j}/"; 
  var hid2Alt = HierarchyId.Parse(level2);
  // neu
  var hid2Neu = HierarchyId.Parse(abteilungsleiter.Level, j);

  var n = new Employee(hid2Neu, f.Name.FullName(), f.Random.Number(1970, 2023));
  n.Company = company;
  ctx.Add(n);
  Console.WriteLine("   " + n.ToString());
  var c3 = ctx.SaveChanges();
}

Entity Framework Core: automatisch kompilierte Modelle

Entity Framework Core erzeugt immer schon erst zur Laufzeit einen Programmcode für das Mapping eines Objektmodells auf ein Datenbankschema. Diese Laufzeitcodegenerierung kann bei großen Objektmodellen zeitaufwendig sein und zudem ist Laufzeitcodegenerierung nicht kompatibel zum Ahead-of-Timer-Compiler „Native AOT“, den es seit .NET 7.0 gibt und den Microsoft in .NET 9.0 auch für Datenbankzugriffe mit Entity Framework Core (zumindest in einigen Szenarien) möglich machen will. In .NET 8.0 funktioniert Entity Framework Core leider gar nicht bei aktivierter Native-AOT-Kompilierung.

Seit Version 6.0 von Entity Framework Core gibt es als Alternative zur kompletten Laufzeitkompilierung bereits kompilierte Modelle, bei denen ein Teil der Laufzeitcodegenerierung zur Entwicklungszeit stattfindet.

Der bisherige Weg zum kompilierten Modell sieht so aus: Entwicklerinnen und Entwickler müssen zuerst einen Kommandozeilenbefehl ausführen – dotnet ef dbcontext optimize, bzw. in der PowerShell-basierten NuGet-Package-Manager-Konsole in Visual Studio: Optimize-DbContext.

Danach galt es, im Programmcode noch einen Methodenaufruf in OnConfiguring() zu ergänzen: .UseModel(Kontextname.Instance). Dabei ist zu beachten: Man muss die kompilierten Modelle auf der Kommandozeile immer wieder neu erzeugen, sobald es Änderungen am Objektmodell oder der Kontextklasse gibt.

Alle diese Schritte können in Entity Framework Core 9.0 entfallen. Das Modellkompilieren zur Entwicklungszeit lässt sich mit einem neuen MSBuild-Task <EFOptimizeContext> automatisieren. Dazu muss man das neue NuGet-Paket Microsoft.EntityFrameworkCore.Tasks [17] (verfügbar seit Preview 3) einbinden und dann in der Projektdatei die Einstellung <EFOptimizeContext> auf den Wert true setzen:

<PropertyGroup>
  <EFOptimizeContext>true</EFOptimizeContext>
</PropertyGroup>

Bei jedem Übersetzungsvorgang sieht man dann auf der Konsole bzw. im Outputfenster: Optimizing DbContext...

Anpassungen der Modellkompilierung können Entwicklerinnen und Entwickler über weitere Projekteinstellungen wie <EFStartupProject>, <DbContextName> und <EFTargetNamespace> vornehmen.

Der Methodenaufruf UseModel() ist beim neuen Vorgehen nicht mehr notwendig, sofern das kompilierte Modell in der gleichen Assembly liegt wie die Kontextklasse. Dies gilt unabhängig davon, ob man den MSBuild-Task <EFOptimizeContext> einsetzt oder die Modellkompilierung weiterhin von Hand anstößt.

Entity Framework Core: schreibgeschützte Objektmengen als Property-Typ

Arrays und typisierte Listen wie List<int> für einzelne Properties mit primitiven Typen funktionierten vor Entity-Framework-Core-Version 8.0 nur in Verbindung mit dem Datenbankmanagementsystem PostgreSQL, weil es dort einen eigenen Mengentyp für elementare Datentypen gibt. In Entity Framework Core 8.0 hat Microsoft die Möglichkeit eingeführt, Arrays und typisierte Listen wie List<int>, List<string> und List<Guid> in anderen Datenbankmanagementsystemen auf JSON-Arrays in einer Textspalte (z. B. nvarchar(MAX) in Microsoft SQL Server und TEXT in SQLite) abzubilden.

In Entity Framework Core 9.0 erweitert Microsoft diese Mengenabbildungen für einzelne Properties auf die schreibgeschützten .NET-Typen IReadOnlyList<T>, IReadOnlyCollection<T> und ReadOnlyCollection<T> (Listing 8). Bei SQLite und SQL Server entstehen auch wieder JSON-Spalten. Mit PostgreSQL ließ sich das Feature zu der Zeit noch nicht testen, weil es den Treiber für PostgreSQL erst in Version 9.0 Preview 3 gab, die schreibgeschützten .NET-Typen aber erst in Preview 4 von Entity Framework Core kamen. Der Versuch, Entity Framework Core 9.0 Preview 4 mit dem PostgreSQL-Treiber in Preview 3 zu verwenden, endet im Laufzeitfehler „The type initializer for 'Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping.NpgsqlBigIntegerTypeMapping' threw an exception“.

Listing 8: IReadOnlyList<T>, IReadOnlyCollection<T> und ReadOnlyCollection<T> als Spaltentyp beim OR-Mapping

public class DataTypeTest
{
  public Int32 ID { get; set; }
...
  #region Array-Typ-Mapping --> Neu in EFCore 8.0 (zuvor nur für PostgreSQL!)
  [Comment("C#: byte[]")]
  public byte[] ByteArray { get; set; }

  [Comment("C#: short[]")]
  public short[] ShortArray { get; set; }

  [Comment("C#: int[]")]
  public int[] IntArray { get; set; }

  [Comment("C#: long[]")]
  public long[] LongArray { get; set; }

  //[Comment("C#: byte[]")] --> Immer noch nicht möglich in EFC 9.0 :-(
  //public Int128[] Int128Array { get; set; }

  [Comment("C#: string[]")]
  public string[] StringArray { get; set; }

  [Comment("C#: DateTime[]")]
  public DateTime[] DateTimeArray { get; set; }

  [Comment("C#: bool[]")]
  public bool[] BoolArray { get; set; }

  [Comment("C#: Guid[]")]
  public Guid[] GuidArray { get; set; }

  [Comment("C#: ABC[]")]
  public ABC[] EnumArray { get; set; }
  #endregion

  #region Listen --> Neu in EFCore 8.0 (zuvor nur für PostgreSQL!)
  [Comment("C#: List<int>")]
  public List<int> IntList { get; set; }

  [Comment("C#: List<Guid>")]
  public List<Guid> GuidList { get; set; }
  #endregion

  #region ReadOnly-Listen (Neu in EFCore 9.0)
  [Comment("C#: ReadOnlyCollection<int>")]
  public ReadOnlyCollection<int> ReadOnlyCollection { get; set; }

  [Comment("C#: IReadOnlyCollection<int>")]
  public IReadOnlyCollection<int> IReadOnlyCollection { get; set; } = new List<int>();

  [Comment("C#: IReadOnlyList<int>")]
  public IReadOnlyList<int> IReadOnlyList { get; set; } = new List<int>();

  #endregion
...
}

Die Verwendung von Properties mit den schreibgeschützten Mengentypen IReadOnlyList<T>, IReadOnlyCollection<T> und ReadOnlyCollection<T> ist auch in LINQ-Abfragen möglich. Entity Framework Core 9.0 übersetzt beispielsweise die folgende LINQ-Befehlsfolge

var resultSet = ctx.DataTypeTest
  .Where(x => x.ReadOnlyCollection.Any(x => x >= 42))
  .Select(x => new DataTypeTest() { ID = x.ID, ReadOnlyCollection = x.ReadOnlyCollection })
  .ToList();

in nachstehende SQL-Abfrage:

SELECT "d"."ID", "d"."ReadOnlyCollection"
FROM "DataTypeTest" AS "d"
WHERE EXISTS (
  SELECT 1
  FROM json_each("d"."ReadOnlyCollection") AS "r"
  WHERE "r"."value" >= 42)

Entity Framework Core: Gruppierungen über komplexe Typen

Auch eine weitere neue LINQ-zu-SQL-Übersetzung hat Microsoft seit Version 9.0 Preview 4 eingebaut: LINQ-Abfragen mit Gruppierungen über komplexe Typen sind nun möglich. Komplexe Typen gab es im klassischen Entity Framework und gibt es wieder seit Entity Framework Core 8.0. Sie werden im Standard nicht zu eigenständigen Tabellen, sondern zu Spalten in der Tabelle zum übergeordneten Typ.

Nehmen wir an, es gibt als Teil der Klasse DataTypeTest einen komplexen Typ KomplexerTyp mit zwei Datenmitgliedern mit Namen Feld1 und Feld2. Dann entsteht aus dieser Abfrage

var q = ctx.DataTypeTest
  .GroupBy(b => b.KomplexerTyp)
  .Select(g => new { g.Key, Count = g.Count() });

diese SQL-Abfrage:

SELECT "d"."KomplexerTyp_Feld1", "d"."KomplexerTyp_Feld2", COUNT(*) AS "Count"
FROM "DataTypeTest" AS "d"
GROUP BY "d"."KomplexerTyp_Feld1", "d"."KomplexerTyp_Feld2"

Entity Framework Core: Verbesserungen für Azure-Cosmos-DB-Datenbanktreiber

Weitere Verbesserungen finden sich im Entity-Framework-Core-Datenbanktreiber für Azure Cosmos DB. Der Cosmos-DB-Provider hat bereits seit Entity Framework Core 6.0 primitive Sammlungen in begrenzter Form unterstützt. In Version 9.0 wird diese Unterstützung verbessert, indem die Metadaten und API-Oberflächen für primitive Sammlungen in Dokumentdatenbanken mit denen in relationalen Datenbanken konsolidiert werden. Dadurch können primitive Sammlungen explizit mit der Modellierungs-API abgebildet werden, dass die Konfiguration von Facetten des Elementtyps ermöglicht. Zum Beispiel kann eine Liste von erforderlichen (nicht null) Zeichenfolgen nun wie folgt abgebildet werden:

modelBuilder.Entity<Book>()
  .PrimitiveCollection(e => e.Quotes)
  .ElementType(b => b.IsRequired());

Zudem wirft der Entity-Framework-Core-Datenbanktreiber für Azure Cosmos DB ab Entity Framework Core 9.0 nun einen Laufzeitfehler aus, wenn Entwicklerinnen und Entwickler synchrone Zugriffe (z. B. mit ToList()) versuchen. Der Treiber unterstützt intern nämlich gar keine synchronen Zugriffe. Bisher waren synchrone Zugriffe dennoch möglich, wurden intern asynchron ausgeführt und dann wurde bis zum Ausführungsende blockiert. Das konnte zu Deadlocks führen. In Version 9.0 gibt es in diesem Falle nun im Standard einen Laufzeitfehler. Derzeit kann man das alte Verhalten noch wiederherstellen mit:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  => optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));

Microsoft hat in der Dokumentation [18] verkündet, dass man in zwei Jahren (in Version 11.0 von Entity Framework Core) synchrone Aufrufe im Treiber für Azure Cosmos DB komplett verbieten will. Spätestens dann müssen Entwicklerinnen und Entwickler alle synchronen Materialisierungsoperationen wie ToList() in die asynchronen Pendants ToListAsync() umgewandelt haben. Außerdem unterstützt der Cosmos-DB-Provider für Entity Framework Core ab Version 9.0 auch die rollenbasierte Zugriffskontrolle (RBAC) [19].

Ausblick

Neben den bisher in den Preview-Versionen 1 bis 4 enthaltenen Features stehen weitere Punkte bei Microsoft auf der Agenda. Einige sollen hier erwähnt sein:

  • Microsoft zeigte auf der Build-2024-Konferenz im C#-13.0-Vortrag [20] einen noch nicht in Preview 4 enthaltenen Prototyp der Extension Types als Verallgemeinerung der in C# 3.0 eingeführten Extension Methods. Ein Extension Type (z. B. public implicit extension StringExtension for String { … }) kann nicht nur Methoden, sondern auch berechnete Properties und Indexer enthalten, auf Instanz- oder Klassenebene. Auch generische Parameter können ergänzt werden. Nur eigene Zustände (also Fields) darf ein Extension Type nicht beinhalten.

  • Im Build-Vortrag zu Entity Framework Core 9.0 [21] zeigte Shay Rojanski Fortschritte bei der Ahead-of-Time-Kompilierung für Entity Framework Core auf Basis von statischer Codeanalyse und C# Interceptors, mit denen LINQ-Befehle schon beim Veröffentlichen der Anwendung in SQL übersetzt werden. Allerdings wurde auch klargestellt, dass es in Version 9.0 noch keine stabile Implementierung geben wird und noch nicht alle LINQ-Abfragen möglich sein werden. Insbesondere werden dynamisch zusammengebaute LINQ-Abfragen nicht möglich sein, weil die statische Codeanalyse solche Abfragen nicht erfassen kann.

  • Darüberhinausgehende Ideen des Entity-Framework-Entwicklungsteams muss man sich mühsam auf GitHub zusammensuchen [22], denn die offizielle Roadmap [23] enthält auch Stand Ende Mai 2024 nur „Coming soon“.

Das waren die Neuerungen in C# 13.0 und der Basisklassenbibliothek in .NET 9.0 sowie Entity Framework 9.0. In der nächsten Ausgabe geht es um ASP.NET Core 9.0, Blazor 9.0 sowie WPF und MAUI in .NET 9.0.

Holger Schwichtenberg

Dr. Holger Schwichtenberg - alias der "DOTNET-DOKTOR" - ist technischer Leiter des Expertennetzwerks www.IT-Visions.de, das mit 53 renommierten Experten zahlreiche mittlere und große Unternehmen durch Beratungen und Schulungen sowie bei der Softwareentwicklung unterstützt. Seine persönlichen Tätigkeitsschwerpunkte sind Webanwendungen, verteilte Systeme, systemnahe Programmierung und Datenbankzugriffe. Er programmiert leidenschaftlich in C# und JavaScript/TypeScript sowie PowerShell. Durch seine Auftritte auf zahlreichen nationalen und internationalen Fachkonferenzen sowie mehr als 90 Fachbücher für O’Reilly, Addison-Wesley, Microsoft Press und dem Hanser-Verlag gehört er zu den bekanntesten Softwareentwicklungsexperten in Deutschland. Darüber hinaus ist er ständiger Mitarbeiter bei Windows Developer und anderen Fachzeitschriften. Er hat in seiner Karriere bereits über 1500 Fachartikel veröffentlicht. Von Microsoft wird er für sein .NET-Fachwissen seit nunmehr 20 Jahren als Microsoft Most Valuable Professional (MVP) ausgezeichnet. Zudem ist er seit 1999 durchgehend Sprecher auf jeder BASTA!-Konferenz.


Weitere Artikel zu diesem Thema