Einbiegen auf die Zielgerade: .NET 7.0 Release Candidate 1 ist fertig

Mit dem Release Candidate 1 reicht Microsoft nochmals eine große Anzahl neuer Funktionen für .NET 7.0 nach, insbesondere für den Datenbankzugriff.

Lesezeit: 11 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen 6 Beiträge

(Bild: Ken Wolter/Shutterstock.com)

Von
  • Dr. Holger Schwichtenberg

Für das nächste Major Release .NET 7.0 hat Microsoft seit Februar 2022 insgesamt sieben Preview-Versionen vorgelegt, über die heise Developer jeweils berichtete. Nun steht die erste von zwei Release-Candidate-Versionen bereit.

Microsofts OR-Mapper Entity Framework Core hat in dieser Version zahlreiche neue Funktionen erhalten. Abweichend von den früheren Gepflogenheiten stellt der ankündigende Blogeintrag selbst keine der neuen Funktionen dar, sondern verweist lediglich auf die mittlerweile stark angewachsene Seite "What's New in EF Core 7.0" in der Dokumentation. Die Aussage im Blog "The team focused on addressing defects, minor enhancements, and putting the finishing touches on features." verschleiert die Tatsache, dass in Entity Framework Core 7.0 Release Candidate 1 mehr wesentliche Funktionen erschienen sind als in allen anderen bisherigen Vorschauversionen.

Abhängige Objekte ("Aggregates") lassen sich in Entity Framework Core nun auf JSON-Spalten innerhalb einer relationalen Datenbanktabelle abbilden. Dafür gibt es in der Fluent-API in der Methode OnModelCreating() in Verbindung mit OwnsOne() und OwnsMany() die neue Operation ToJson(), wie das Listing 1 zeigt:

protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
   // Owned Type 1:1 mit JSON-Mapping
   modelBuilder.Entity<Company>().OwnsOne(
       p => p.Address,
       ownedNavigationBuilder =>
       {
        ownedNavigationBuilder.ToJson();
       });
 
   // Owned Type 1:n mit JSON-Mapping
   modelBuilder.Entity<Company>().OwnsMany(
    p => p.ManagementSet,
    ownedNavigationBuilder =>
    {
     ownedNavigationBuilder.ToJson();
    });
  } 

Listing 1: Einsatz von ToJson() in Verbindung mit OwnsOne() und OwnsMany()

In diesem Beispiel entsteht aus den .NET-Klassen "Company", "Address" und "Management" lediglich eine einzige Datenbanktabelle "Company", in der die Adresse als JSON-Objekt ({…}) und die Führungsregie als JSON-Array ([{…},{…},{…}]) integriert sind (siehe Abbildung 1). Der Datentyp der JSON-Spalten ist dabei nvarchar(max). Ohne den Aufruf von ownedNavigationBuilder.ToJson() entstehen bei Entity Framework Core für Aggregate zwei Tabellen: Company umfasst auch alle Spalten von "Address". Für das Management gibt es eine eigene Tabelle (siehe Abbildung 2).

Datenbanktabelle "Company" mit zwei JSON-Spalten für die abhängigen Objekte (Abb. 1).

Normales, relationales Mapping (Abb. 2).

Ein Praxistext zeigt, dass die Dokumentation zu dem JSON-Mapping leider verschweigt, dass dieses in Zusammenhang mit Vererbung nur mit dem Table-per-Hierarchy-Mapping (TPH) funktioniert. Beim Versuch, eine Vererbungshierarchie auf mehrere Datenbanktabellen aufzuteilen, kassiert man den Laufzeitfehler System.InvalidOperationException: "Entity type 'xy' references entities mapped to JSON. Only TPH inheritance is supported for those entities.". Ebenso darf der N-Gegenpart in einer 1:N-Beziehung keine Spalte besitzen, die per Konvention oder Konfiguration ein Primärschlüssel ist. Eine Property "ID" in der Klasse "Management" führt zum Laufzeitfehler System.InvalidOperationException: "Entity type 'Management' is part of a collection mapped to JSON and has its ordinal key defined explicitly. Only implicitly defined ordinal keys are supported.". Bei der 1:1-Beziehung darf hingegen eine Primärschlüsselspalte existieren. Abweichend vom Standard bei Entity Framework Core werden beim JSON-Mapping aber keine automatischen Werte für den Schlüssel vergeben, wie "AddressID" in Abbildung 1 zeigt, die immer 0 ist.

Entwicklerinnen und Entwickler können Instanzen dieser JSON-gemappten Typen wie andere Entitätstypen über die Entity-Framework-Core-API hinzufügen, ändern und löschen. LINQ-Abfragen, die sich auf die abhängigen Objekte beziehen, wie etwa

var allCompaniesInEssen = ctx.Company.Where(c=>c.Address.City.StartsWith("Essen")).ToList();

werden in entsprechende JSON-Operationen in SQL verwandelt:

SELECT [c].[CompanyID], [c].[Name], [c].[CommercialRegister], [c].[CommercialRegisterID], JSON_QUERY([c].[Address],'$'), JSON_QUERY([c].[ManagementSet],'$')
FROM [Company] AS [c]
WHERE CAST(JSON_VALUE([c].[Address],'$.City') AS nvarchar(50)) IS NOT NULL AND (CAST(JSON_VALUE([c].[Address],'$.City') AS nvarchar(50)) LIKE N'Essen%')

Diese JSON-Unterstützung liefert Microsoft vorerst nur für den eigenen Microsoft SQL Server. JSON-Spalten in SQLite sollen erst später folgen. Andere Datenbankprovideranbieter müssen ihre Treiber ebenfalls anpassen.

Bereits in Preview 3 von Entity Framework Core 7.0 im April hatte Microsoft verkündet, dass der generierte Programmcode beim Reverse Engineering mit dem Kommandozeilenbefehl dotnet ef dbcontext scaffold beziehungsweise dem PowerShell-Commandlet Scaffold-DbContext nun (wie im klassischen Entity Framework) wieder mit dem Text Template Transformation Toolkit (T4) anpassbar ist. In den Preview-Versionen 3 bis 7 mussten Entwickler sich aber die entsprechenden Vorlagen mühsam von GitHub beschaffen. Mit dem Release Candidate 1 bietet Microsoft nun auf NuGet.org ein Paket als Projektvorlage an, das die T4-Vorlagen für Kontextklasse (DbContext.t4) und Entitätsklassen (EntityType.t4) liefert.

Dazu müssen sich Entwicklerinnen und Entwickler zunächst per Kommandozeile die Projektvorlage "Microsoft.EntityFrameworkCore.Templates" beschaffen (siehe auch Abbildung 3):

dotnet new install Microsoft.EntityFrameworkCore.Templates::7.0.0-*

Diese Vorlage ist anschließend auszuführen mit:

dotnet new ef-templates

Installation der T4-Vorlagen via Developer PowerShell innerhalb von Visual Studio (Abb. 3).

Dieser Befehl sollte auf dem Projekt angewendet werden, das beim Reverse Engineering via Parameter -Project festgelegt wird. In der Regel nutzt man für die Ausführung der Reverse-Engineering-Werkzeuge ein getrenntes Konsolenprojekt, zum Beispiel mit Namen "Tools", das später nicht auf dem Zielsystem ausgeliefert wird. Als Zielordner der Codegenerierung gibt man via OutputDir und -ContextDir andere Projekte in der Projektmappe an:

Scaffold-DbContext 
-Connection "Server=IhrServer;Database=IhreDB;Trusted_Connection=True;MultipleActiveResultSets=True;Encrypt=true" 
-Provider Microsoft.EntityFrameworkCore.SqlServer 
-OutputDir ..\BO -Namespace BO 
-ContextDir ..\DA -ContextNamespace DA 
-NoPluralize 
-UseDatabaseNames 
-Context WWWingsContext 
-StartupProject Tools 
-Project Tools 
-force

Abbildung 4 zeigt die zum Projekt "Tools" hinzugefügten Vorlagen DbContext.t4 und EntityType.t4. Nach einer Anpassung der Vorlagen werden diese bei der nächsten Ausführung von Scaffold-DbContext automatisch berücksichtigt.

Die angepassten Versionen der T4-Vorlagen von Microsoft generierten die Kontextklasse in Projekt "DA" und die Entitätsklassen in Projekt "BO". In Visual Studio ist die Erweiterung "T4 Language" installiert (Abb. 4).

Details zur T4-Integration finden sich in der Dokumentation und auf GitHub.

Entity Framework Core basiert genau wie der Vorgänger ADO.NET Entity Framework auf einer Vielzahl von Konventionen, die man bei Bedarf durch eigene Konfiguration außer Kraft setzen kann. Außerdem kann man seit Entity Framework Core 3.0 eigene Konventionsklassen schreiben, die man aber bisher etwas mühsam über eine eigene ConventionSetBuilder-Klasse auf Basis der Standard-ConventionSetBuilder-Klasse des jeweiligen Datenbankproviders (zum Beispiel SqlServerConventionSetBuilder) einbinden musste.

In Entity Framework Core 7.0 geht dies einfacher, denn die Basisklasse DbContext bietet nun eine Methode in ConfigureConventions() zum Überschreiben, in der man Konventionen entfernen und hinzufügen kann. Die Methode ConfigureConventions() wird genau wie OnModelCreating() nur einmalig bei der ersten Instanziierung der Kontextklasse in einem Prozess aufgerufen.

Der Code in Listing 2 setzt via configurationBuilder.Conventions.Remove() drei Standardkonventionen außer Kraft und aktiviert zwei eigene selbstimplementierte (aber hier nicht dargestellte) Konventionen mit configurationBuilder.Conventions.Add().

class Context : DbContext
{
...
 protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
 {
  // setze Konvention ausser Kraft, die [Index] auf Entitaetsklasse berücksichtigt
  configurationBuilder.Conventions.Remove(typeof(IndexAttributeConvention));
  // setze Konvention ausser Kraft, dass alle Fremdschluessel einen Index erhalten
  configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
  // setze Konvention ausser Kraft, dass alle Spalten ID oder ENTITYNAMEID zum PK werden
  configurationBuilder.Conventions.Remove(typeof(KeyDiscoveryConvention));
 
  // Eigene Konvention für GUID-Spalten als PK
  configurationBuilder.Conventions.Add(_ => new GuidPrimaryKeyConvention());
  // Eigene Konvention für Index für alle [ITVIndex] annotierte Spalten
  configurationBuilder.Conventions.Add(_ => new ITVIndexAttributeConvention()); 
 }
...
}

Listing 2: ConfigureConventions() in der Kontextklasse

Entity Framework Core war wie der Vorgänger Entity Framework bisher sehr auf einzelne Objekte konzentriert. Um eine Vielzahl von Objekten zu löschen oder zu ändern, mussten Entwicklerinnen und Entwickler diese erst ins RAM laden und dann für jedes Objekt einen einzelnen DELETE- bzw. UPDATE-Befehl zum Datenbankmanagementsystem senden. Das ist sehr ineffizient, wenn man die Aktion auch als Massenoperation mit einem einzigen Befehl für alle Datensätze hätte formulieren können. Entwickler und Entwicklerinnen mussten bisher an der Stelle entweder auf das Absenden klassischer SQL-Befehle oder Entity Framework Core-Erweiterungen wie EFPlus ausweichen.

Entity Framework Core 7.0 bietet nun von Haus aus Unterstützung für Massenoperationen (Bulk Operations) mit den Befehlen ExecuteUpdate() und ExecuteDelete() beziehungsweise den asynchronen Pendants ExecuteUpdateAsync() und ExecuteDeleteAsync(). Aus dieser in LINQ-Syntax implementierten Befehlszeile

var count = ctx.FlightSet
.TagWith(nameof(BulkDeleteEFC7))
.Where(x => x.FlightNo >= min)
.ExecuteDelete();

sendet der OR-Mapper nur noch einen einzigen DELETE-Befehl zum Datenbankmanagementsystem:

-- BulkDeleteEFC7
--//////////////////////////////////////////////////
DELETE TOP(1000 /* @__p_1 */) 
FROM   [WWWings].[Flight] AS [f]
WHERE  [f].[FlightNo] >= 20000 /* @__min_0 */

Ebenso entsteht aus

var count = ctx.FlightSet
.TagWith(nameof(BulkUpdateEFC7))
.Where(x => x.Departure == "Berlin" && x.Date >= DateTime.Now)
.ExecuteUpdate(x => x.SetProperty(x => x.FreeSeats,x => x.FreeSeats - 1));

nur ein einziger UPDATE-Befehl:

-- BulkUpdateEFC7
--//////////////////////////////////////////////////
UPDATE [f]
SET    [f].[FreeSeats] = [f].[FreeSeats] - CAST(1 AS smallint) 
FROM   [WWWings].[Flight] AS [f]
WHERE  [f].[Departure] = N'Berlin' AND [f].[FlightDate] >= GETDATE()

Zu beachten ist, dass ein Befehl ohne Where()-Einschränkung wie ctx.FlightSet.ExecuteDelete() alle Datensätze aus der korrespondierenden Tabelle entfernt.

Bei diesen Massenoperationen kann Entity Framework Core allerdings bisher nur auf eine einzige Tabelle zugreifen. ExecuteUpdate() und ExecuteDelete() funktionieren also bei Vererbung nur mit Table-per-Hierarchy-Mapping (TPH), nicht mit Table-per-Type (TPT) oder Table-per-Concrete-Type (TPC) sowie Entity Splitting (Aufteilung einer Entität auf mehrere Tabellen). Dazu gibt es ein offenes Issue auf GitHub.

Ebenso ist in Entity Framework Core 7.0 nun bei der Persistierungsmethode SaveChanges() wieder (wie im klassischen Entity Framework) ein automatischer Aufruf von Stored Procedures anstelle der Generierung von UPDATE-, INSERT- und DELETE-Ad-hoc-Befehlen möglich (siehe efcore-Issue 245 auf GitHub). Dafür gibt es in der Fluent-API die neuen Operationen mit sprechenden Namen: InsertUsingStoredProcedure(), UpdateUsingStoredProcedure() und DeleteUsingStoredProcedure(), wie Listing 3 zeigt:

modelBuilder.Entity<Person>()
             .InsertUsingStoredProcedure(
                 "Person_Insert",
                 spb => spb
                     .HasParameter(w => w.Name)
                     .HasParameter(w => w.Birthday)
                     .HasParameter(w => w.PersonID, pb => pb.IsOutput()))
             .UpdateUsingStoredProcedure(
                 "Person_Update",
                 spb => spb
                     .HasParameter(w => w.PersonID)
                     .HasParameter(w => w.Name)
                     .HasParameter(w => w.Birthday))
             .DeleteUsingStoredProcedure(
                 "Person_Delete",
                 spb => spb.HasParameter(w => w.PersonID));

Listing 3: Einsatz von Stored Procedures für CUD-Operationen

Die Liste der Breaking Changes in Entity Framework Core 7.0 gegenüber der Vorgängerversion (siehe Abbildung 5) führt bisher nur einen Inkompatibilitätspunkt auf, der jedoch gravierenden Einfluss haben kann.

Wenn Entity Framework Core zum Mapping einer Datenbanktabelle im Microsoft SQL Server, die Datenbank-Trigger besitzt, verwendet wird, müssen Entwicklerinnen und Entwickler das dem Entity Framework Core nun in der Fluent-API via Aufruf von HasTrigger() anzeigen:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Entitaetsklasse>().ToTable(tb => tb.HasTrigger("TriggerName"));
}

HasTrigger() dient dabei nicht dazu, den Trigger anzulegen, sondern die Art und Weise zu ändern, wie Entity Framework Core INSERT- und UPDATE-Befehle zum Microsoft SQL Server sendet, wenn das Datenbankmanagementsystem Werte vergibt, die in das Objekt übernommen werden müssen. In der vorherigen Version des OR-Mappers wurde ein entsprechender INSERT-Befehl so gesendet:

DECLARE @inserted0 TABLE ([Id] int);
INSERT INTO [TabellenName] ([Name]) OUTPUT INSERTED.[Id] INTO @inserted0 VALUES (@p0);
SELECT [i].[Id] FROM @inserted0 i;

In Entity Framework Core 7.0 hat Microsoft etwas performantere Syntax zum Standard erhoben:

INSERT INTO [TabellenName] ([Name]) OUTPUT INSERTED.[Id] VALUES (@p0);

Allerdings ist diese neue Syntax nicht möglich, falls die Tabelle Trigger besitzt. Daher müssen Entwickler und Entwicklerinnen mit HasTrigger() dafür sorgen, dass Entity Framework Core in diesem Fall die alte, aber langsamere SQL-Syntax verwendet. Da HasTrigger() bislang keine weitere Bedeutung hat, kann man dort jeden beliebigen Namen angeben.

Breaking Changes in Entity Framework Core 7.0 (Abb. 5).

(Bild: Microsoft )

Ein weiterer Blogeintrag listet Neuerungen in ASP.NET Core und Blazor in Version 7.0 Release Candidate 1 auf. Dazu gehören:

  • das Abfangen von Navigationsereignissen in Blazor
  • flexiblere Open-ID-Connect-Authentifizierung in Blazor WebAssembly
  • Verbesserungen für das Debugging in Blazor WebAssembly, das bisher im Vergleich zu anderen .NET-basierten UI-Frameworks einigen Einschränkungen unterlag
  • Neue API via Annotationen [JSImport] und [JSExport] für Aufrufe zwischen JavaScript und C# in WebAssembly als Ersatz für die nun obsolete Schnittstelle IJSUnmarshalledRuntime
  • Beschleunigung von Uploads mit HTTP/2
  • Experimentelle Implementierung des IETF-Entwurfs "WebTransport" für mehrere bidirektionale Streams über eine HTTP/3-Verbindung im Webserver Kestrel
  • OpenAPI-Unterstützung für gRPC-Dienste mit JSON-Transcoding
  • Aktivierung der in Preview 4 eingeführten Rate Limiting Middleware für einzelne Endpunkte

Heise-Konferenz zu .NET 7

Am 22. November 2022 richten heise Developer und dpunkt.verlag in Kooperation mit www.IT-Visions.de mit der betterCode() .NET 7 eine Online-Konferenz zum kommenden Release von Microsofts Entwicklungsplattform .NET aus. Dieses soll im November erscheinen. Es ist zwar kein Long-Term Release wie das letztes Jahr erschienene und gerade aktuelle .NET 6., in das neue Release fließen aber viele Neuerungen und Arbeiten zur Verbesserung der Codequalität ein, die dann die Migration auf .NET 7 rechtfertigen können.

Microsoft hat in der Basisklassenbibliothek die schon in Preview 1 eingeführte Annotation [RegexGenerator] in [GeneratedRegex] allgemein umbenannt. Die in Preview 5 angekündigten Basistypen System.Int128 und System.UInt128 fehlen weiterhin. Auch der in Preview 7 angekündigte Unix-Rechte-Support via UnixFileMode fehlt immer noch in Release Candidate 1. Ebenso hat DateTime weiterhin weder die Eigenschaften Microseconds noch Nanoseconds, obwohl dies im Blogeintrag zu Preview 4 verkündet wurde.

.NET 7.0 soll am 8. November 2022 als fertiges Produkt erscheinen. Im Oktober ist eine neunte und letzte Vorabversion unter dem Titel "Release Candidate 2" zu erwarten.

(map)