Neuerungen in EF Core 6.0

EF Core 6.0 wurde für NuGet bereitgestellt. Diese Seite enthält eine Übersicht über interessante Änderungen in den einzelnen Vorschauversionen.

Tipp

Sie können alle im Folgenden gezeigten Beispiele für Preview 1 ausführen und debuggen, indem Sie den Beispielcode von GitHub herunterladen.

Temporäre SQL Server-Tabellen

GitHub-Issue: 4693.

Temporäre Tabellen für SQL Server verfolgen automatisch alle Daten, die jemals in einer Tabelle gespeichert wurden, selbst wenn diese Daten danach aktualisiert oder gelöscht wurden. Dazu wird eine parallele „Verlaufstabelle“ erstellt, in der Verlaufsdaten mit Zeitstempel gespeichert werden, wenn eine Änderung an der Haupttabelle vorgenommen wird. Dadurch können Verlaufsdaten z. B. zur Überwachung abgefragt oder wiederhergestellt werden, z. B. zur Wiederherstellung nach versehentlicher Mutation oder Löschung.

EF Core unterstützt jetzt:

  • Die Erstellung temporärer Tabellen mithilfe von Migrationen
  • Transformation vorhandener Tabellen in temporäre Tabellen mithilfe von Migrationen
  • Abfragen von Verlaufsdaten
  • Wiederherstellen von Daten von einem bestimmten Zeitpunkt in der Vergangenheit

Konfigurieren einer temporären Tabelle

Der Modell-Generator kann verwendet werden, um eine Tabelle als temporär zu konfigurieren. Beispiel:

modelBuilder
    .Entity<Employee>()
    .ToTable("Employees", b => b.IsTemporal());

Wenn Sie EF Core zum Erstellen der Datenbank verwenden, wird die neue Tabelle als temporäre Tabelle mit den SQL Server-Standardwerten für die Zeitstempel und die Verlaufstabelle konfiguriert. Ein Beispiel dafür ist folgender Employee-Entitätstyp:

public class Employee
{
    public Guid EmployeeId { get; set; }
    public string Name { get; set; }
    public string Position { get; set; }
    public string Department { get; set; }
    public string Address { get; set; }
    public decimal AnnualSalary { get; set; }
}

Die erstellte temporäre Tabelle sieht wie folgt aus:

DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
    [EmployeeId] uniqueidentifier NOT NULL,
    [Name] nvarchar(100) NULL,
    [Position] nvarchar(100) NULL,
    [Department] nvarchar(100) NULL,
    [Address] nvarchar(1024) NULL,
    [AnnualSalary] decimal(10,2) NOT NULL,
    [PeriodEnd] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    [PeriodStart] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
    PERIOD FOR SYSTEM_TIME([PeriodStart], [PeriodEnd])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistory]))');

Beachten Sie, dass SQL Server zwei ausgeblendete datetime2-Spalten PeriodEnd und PeriodStart erstellt. Diese „Zeitraumspalten“ stellen den Zeitraum dar, in dem die Daten in der Zeile vorhanden waren. Diese Spalten werden Schatteneigenschaften im EF Core-Modell zugeordnet, sodass sie wie später gezeigt in Abfragen verwendet werden können.

Wichtig

Die Zeiten in diesen Spalten sind immer die UTC-Zeit, die von SQL Server generiert wird. UTC-Zeiten werden für alle Vorgänge verwendet, die temporäre Tabellen betreffen, z. B. in den unten gezeigten Abfragen.

Beachten Sie auch, dass eine zugeordnete Verlaufstabelle EmployeeHistory automatisch erstellt wird. Die Namen der Zeitraumspalten und der Verlaufstabelle können durch zusätzliche Konfiguration für den Modell-Generator geändert werden. Beispiel:

modelBuilder
    .Entity<Employee>()
    .ToTable(
        "Employees",
        b => b.IsTemporal(
            b =>
            {
                b.HasPeriodStart("ValidFrom");
                b.HasPeriodEnd("ValidTo");
                b.UseHistoryTable("EmployeeHistoricalData");
            }));

Dies wird in der Tabelle berücksichtigt, die von SQL Server erstellt wurde:

DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
    [EmployeeId] uniqueidentifier NOT NULL,
    [Name] nvarchar(100) NULL,
    [Position] nvarchar(100) NULL,
    [Department] nvarchar(100) NULL,
    [Address] nvarchar(1024) NULL,
    [AnnualSalary] decimal(10,2) NOT NULL,
    [ValidFrom] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    [ValidTo] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
    PERIOD FOR SYSTEM_TIME([ValidFrom], [ValidTo])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistoricalData]))');

Verwenden temporärer Tabellen

Meistens werden temporäre Tabellen wie jede andere Tabelle verwendet. Das heißt, Zeitraumspalten und Verlaufsdaten werden vom SQL Server transparent behandelt und können von der Anwendung ignoriert werden. Beispielsweise können neue Entitäten normal in der Datenbank gespeichert werden:

context.AddRange(
    new Employee
    {
        Name = "Pinky Pie",
        Address = "Sugarcube Corner, Ponyville, Equestria",
        Department = "DevDiv",
        Position = "Party Organizer",
        AnnualSalary = 100.0m
    },
    new Employee
    {
        Name = "Rainbow Dash",
        Address = "Cloudominium, Ponyville, Equestria",
        Department = "DevDiv",
        Position = "Ponyville weather patrol",
        AnnualSalary = 900.0m
    },
    new Employee
    {
        Name = "Fluttershy",
        Address = "Everfree Forest, Equestria",
        Department = "DevDiv",
        Position = "Animal caretaker",
        AnnualSalary = 30.0m
    });

context.SaveChanges();

Diese Daten können dann normal abgefragt, aktualisiert und gelöscht werden. Beispiel:

var employee = context.Employees.Single(e => e.Name == "Rainbow Dash");
context.Remove(employee);
context.SaveChanges();

Zudem kann nach einer normalen Nachverfolgungsabfrage auf die Werte aus den Zeitraumspalten der aktuellen Daten von den nachverfolgten Entitäten aus zugegriffen werden. Beispiel:

var employees = context.Employees.ToList();
foreach (var employee in employees)
{
    var employeeEntry = context.Entry(employee);
    var validFrom = employeeEntry.Property<DateTime>("ValidFrom").CurrentValue;
    var validTo = employeeEntry.Property<DateTime>("ValidTo").CurrentValue;

    Console.WriteLine($"  Employee {employee.Name} valid from {validFrom} to {validTo}");
}

Dies ergibt folgende Ausgabe:

Starting data:
  Employee Pinky Pie valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
  Employee Rainbow Dash valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
  Employee Fluttershy valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM

Beachten Sie, dass die ValidTo-Spalte (standardmäßig als PeriodEnd bezeichnet) den maximalen datetime2-Wert enthält. Dies ist für die aktuellen Zeilen in der Tabelle immer der Fall. Die ValidFrom-Spalten (standardmäßig als PeriodStart bezeichnet) enthält die UTC-Zeit, zu der die Tabellenzeile eingefügt wurde.

Abfragen von Verlaufsdaten

EF Core unterstützt das Abfragen von Verlaufsdaten aus der Tabelle über mehrere neue Abfrageoperatoren:

  • TemporalAsOf: Gibt Zeilen zurück, die zur angegebenen UTC-Zeit aktiv (aktuell) waren. Dies ist eine einzelne Tabellenzeile aus der Verlaufstabelle für einen bestimmten Primärschlüssel.
  • TemporalAll: Gibt alle Zeilen in den Verlaufsdaten zurück. Dies sind in der Regel viele Tabellenzeilen aus der Verlaufstabelle für einen bestimmten Primärschlüssel.
  • TemporalFromTo: Gibt alle Zeilen zurück, die zwischen zwei angegebenen UTC-Zeiten aktiv waren. Dies können viele Tabellenzeilen aus der Verlaufstabelle für einen bestimmten Primärschlüssel sein.
  • TemporalBetween: Entspricht TemporalFromTo, allerdings werden Tabellenzeilen eingeschlossen, die an der oberen Grenze aktiv wurden.
  • TemporalContainedIn: Gibt alle Tabellenzeilen zurück, die zwischen zwei angegebenen UTC-Zeiten aktiv waren und beendet wurden. Dies können viele Tabellenzeilen aus der Verlaufstabelle für einen bestimmten Primärschlüssel sein.

Hinweis

Weitere Informationen dazu, welche Zeilen für jeden dieser Operatoren enthalten sind, finden Sie in der Dokumentation zu temporären Tabellen für SQL Server.

Nach Aktualisierungen und Löschungen unserer Daten können wir beispielsweise mit TemporalAll eine Abfrage ausführen, um die Verlaufsdaten anzuzeigen:

var history = context
    .Employees
    .TemporalAll()
    .Where(e => e.Name == "Rainbow Dash")
    .OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
    .Select(
        e => new
        {
            Employee = e,
            ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
            ValidTo = EF.Property<DateTime>(e, "ValidTo")
        })
    .ToList();

foreach (var pointInTime in history)
{
    Console.WriteLine(
        $"  Employee {pointInTime.Employee.Name} was '{pointInTime.Employee.Position}' from {pointInTime.ValidFrom} to {pointInTime.ValidTo}");
}

Beachten Sie, wie die EF.Property-Methode für den Zugriff auf Werte aus den Zeitraumspalten verwendet werden kann. Diese wird in der OrderBy-Klausel verwendet, um die Daten zu sortieren und diese Werte dann in einer Projektion in die zurückgegebenen Daten einzuschließen.

Diese Abfrage gibt die folgenden Daten zurück:

Historical data for Rainbow Dash:
  Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
  Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM

Beachten Sie, dass die letzte zurückgegebene Tabellenzeile am 26.08.2021 um 16:44:59 Uhr nicht mehr aktiv war. Dies liegt daran, dass die Tabellenzeile für Rainbow Dash zu diesem Zeitpunkt aus der Haupttabelle gelöscht wurde. Wir werden später sehen, wie diese Daten wiederhergestellt werden können.

Ähnliche Abfragen lassen sich mit TemporalFromTo, TemporalBetween oder TemporalContainedIn schreiben. Beispiel:

var history = context
    .Employees
    .TemporalBetween(timeStamp2, timeStamp3)
    .Where(e => e.Name == "Rainbow Dash")
    .OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
    .Select(
        e => new
        {
            Employee = e,
            ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
            ValidTo = EF.Property<DateTime>(e, "ValidTo")
        })
    .ToList();

In diesem Beispiel gibt die Abfrage die folgenden Tabellenzeilen zurück:

Historical data for Rainbow Dash between 8/26/2021 4:41:14 PM and 8/26/2021 4:42:44 PM:
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM

Wiederherstellen von Verlaufsdaten

Wie bereits erwähnt, wurde Rainbow Dash aus der Employees Tabelle gelöscht. Dies war eindeutig ein Fehler, also kehren wir zu einem früheren Zeitpunkt zurück und stellen die fehlende Zeile aus diesem Zeitpunkt wieder her.

var employee = context
    .Employees
    .TemporalAsOf(timeStamp2)
    .Single(e => e.Name == "Rainbow Dash");

context.Add(employee);
context.SaveChanges();

Diese Abfrage gibt eine einzelne Tabellenzeile für Rainbow Dash zu der angegebenen UTC-Zeit zurück. Alle Abfragen mit temporären Operatoren werden standardmäßig nicht nachverfolgt, sodass auch die zurückgegebene Entität hier nicht nachverfolgt wird. Dies ist sinnvoll, da sie derzeit nicht in der Haupttabelle vorhanden ist. Um die Entität erneut in die Haupttabelle einzufügen, markieren wir sie einfach als Added und rufen dann SaveChanges auf.

Nach dem erneuten Einfügen der Tabellenzeile Rainbow Dash zeigt die Abfrage der Verlaufsdaten, dass die Zeile so wiederhergestellt wurde, wie sie zur angegebenen UTC-Zeit existierte:

Historical data for Rainbow Dash:
  Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
  Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:44:59 PM to 12/31/9999 11:59:59 PM

Migrationsbundles

GitHub-Issue: 19693.

Mit EF Core Migrationen werden Datenbankschemaupdates entsprechend den Änderungen am EF-Modell generiert. Diese Schema-Updates sollten zum Zeitpunkt der Anwendungsbereitstellung angewendet werden, häufig im Rahmen eines Continuous Integration/Continuous Deployment-Systems (CI/CD). system.

EF Core bietet jetzt eine neue Möglichkeit, diese Datenbankschemaupdates zu übernehmen: Migrationsbundles. Ein Migrationsbundle ist eine kleine ausführbare Datei mit Migrationen und dem Code, der zur Anwendung dieser Migrationen auf die Datenbank erforderlich ist.

Hinweis

Eine ausführlichere Erläuterung zu Migrationen, Bundles und Bereitstellungen finden Sie im .NET-Blog unter Einführung in DevOps-freundliche EF Core-Migrationspakete.

Migrationsbundles werden mit dem Befehlszeilentool dotnet ef erstellt. Beachten Sie, dass Sie die aktuelle Version des Tools installiert haben müssen, bevor Sie fortfahren.

Ein Bundle muss Migrationen enthalten. Diese werden mit dotnet ef migrations add wie in der dotnet ef migrations add beschrieben erstellt. Nachdem Sie die Migrationen für die Bereitstellung vorbereitet haben, erstellen Sie mithilfe von dotnet ef migrations bundle ein Bundle. Beispiel:

PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>

Die Ausgabe ist eine ausführbare Datei, die für Ihr Zielbetriebssystem geeignet ist. In meinem Fall ist dies Windows x64, sodass eine efbundle.exe in meinem lokalen Ordner abgelegt wird. Beim Ausführen dieser ausführbaren Datei werden die darin enthaltenen Migrationen angewendet:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
Applying migration '20210903083845_MyMigration'.
Done.
PS C:\local\AllTogetherNow\SixOh>

Migrationen werden nur dann auf die Datenbank angewendet, wenn sie noch nicht übernommen wurden. Wenn Sie z. B. dasselbe Bundle erneut ausführen, wird es nicht ausgeführt, da es keine neuen Migrationen gibt, die übernommen werden müssen:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
No migrations were applied. The database is already up to date.
Done.
PS C:\local\AllTogetherNow\SixOh>

Wenn jedoch Änderungen am Modell vorgenommen werden und mit dotnet ef migrations add weitere Migrationen generiert werden, können diese in einer neuen ausführbaren Datei gebündelt werden, die übernommen werden kann. Beispiel:

PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add SecondMigration
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add Number3
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle --force
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>

Beachten Sie, dass mit der --force-Option das vorhandene Bundle mit einem neuen überschrieben werden kann.

Beim Ausführen dieses neuen Bundle werden die folgenden beiden neuen Migrationen auf die Datenbank angewendet:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
Applying migration '20210903084526_SecondMigration'.
Applying migration '20210903084538_Number3'.
Done.
PS C:\local\AllTogetherNow\SixOh>

Standardmäßig verwendet das Bundle die Datenbankverbindungszeichenfolge aus der Konfiguration Ihrer Anwendung. Es kann jedoch eine andere Datenbank migriert werden, wenn deren Verbindungszeichenfolge in der Befehlszeile übergeben wird. Beispiel:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe --connection "Data Source=(LocalDb)\MSSQLLocalDB;Database=SixOhProduction"
Applying migration '20210903083845_MyMigration'.
Applying migration '20210903084526_SecondMigration'.
Applying migration '20210903084538_Number3'.
Done.
PS C:\local\AllTogetherNow\SixOh>

Beachten Sie, dass dieses Mal alle drei Migrationen angewendet wurden, da noch keine Migration für die Produktionsdatenbank übernommen wurde.

Andere Optionen können an die Befehlszeile übergeben werden. Einige häufig genutzte Optionen sind:

  • --output, um den Pfad der zu erstellenden ausführbaren Datei anzugeben.
  • --context, um den DbContext-Typ anzugeben, der für ein Projekt mit mehreren Kontexttypen verwendet werden soll.
  • --project, um das zu verwendende Projekt anzugeben. Der Standardwert ist das aktuelle Arbeitsverzeichnis.
  • --startup-project, um das zu verwendende Startprojekt anzugeben. Der Standardwert ist das aktuelle Arbeitsverzeichnis.
  • --no-build, um zu verhindern, dass das Projekt vor dem Ausführen des Befehls erstellt wird. Diese Option sollte nur verwendet werden, wenn bekannt ist, dass das Projekt auf dem neuesten Stand ist.
  • --verbose, um ausführliche Informationen zur Ausführung des Befehls anzuzeigen. Verwenden Sie diese Option, wenn Sie Informationen in Fehlerberichte einschließen.

Zeigen Sie mit dotnet ef migrations bundle --help alle verfügbaren Optionen an.

Beachten Sie, dass standardmäßig jede Migration in einer eigenen Transaktion angewendet wird. Im GitHub-Issue #22616 finden Sie eine Erläuterung möglicher zukünftiger Verbesserungen in diesem Bereich.

Konfiguration des Präkonventionenmodells

GitHub-Issue: 12229.

Bei früheren Versionen von EF Core musste die Zuordnung für jede Eigenschaft eines bestimmten Typs explizit konfiguriert werden, wenn sich diese Zuordnung vom Standard unterschied. Dies schließt „Facets“ wie die maximale Länge von Zeichenfolgen und die Dezimalgenauigkeit sowie die Wertkonvertierung für den Eigenschaften-Typ ein.

Dies erforderte entweder:

  • Die Konfiguration des Modellgenerators für jede Eigenschaft, oder
  • Ein Zuordnungsattribut für jede Eigenschaft oder
  • Die explizite Iteration aller Eigenschaften aller Entitätstypen und die Verwendung der Metadaten-APIs auf niedriger Ebene beim Erstellen des Modells.

Beachten Sie, dass die explizite Iteration fehleranfällig und wenig robust ist, da die Liste der Entitätstypen und zugeordneten Eigenschaften zum Zeitpunkt dieser Iteration möglicherweise nicht endgültig ist.

Bei EF Core 6.0 kann diese Zuordnungskonfiguration einmal für einen bestimmten Typ angegeben werden. Sie wird dann auf alle Eigenschaften dieses Typs im Modell angewendet. Dies wird als „Konfiguration des Präkonventionsmodells“ bezeichnet, da Aspekte des Modells konfiguriert werden, die dann von den Modellerstellungskonventionen verwendet werden. Eine solche Konfiguration wird durch Überschreiben von ConfigureConventions auf DbContext übernommen:

public class SomeDbContext : DbContext
{
    protected override void ConfigureConventions(
        ModelConfigurationBuilder configurationBuilder)
    {
        // Pre-convention model configuration goes here
    }
}

Berücksichtigen Sie beispielsweise folgende Entitätstypen:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsActive { get; set; }
    public Money AccountValue { get; set; }

    public Session CurrentSession { get; set; }

    public ICollection<Order> Orders { get; } = new List<Order>();
}

public class Order
{
    public int Id { get; set; }
    public string SpecialInstructions { get; set; }
    public DateTime OrderDate { get; set; }
    public bool IsComplete { get; set; }
    public Money Price { get; set; }
    public Money? Discount { get; set; }

    public Customer Customer { get; set; }
}

Alle Zeichenfolgeneigenschaften können als ANSI (statt Unicode) konfiguriert werden und haben eine maximale Länge von 1024 Zeichen:

configurationBuilder
    .Properties<string>()
    .AreUnicode(false)
    .HaveMaxLength(1024);

Alle DateTime-Eigenschaften können mithilfe der Standardkonvertierung von DateTime in longs in der Datenbank in 64-Bit-Ganzzahlen konvertiert werden:

configurationBuilder
    .Properties<DateTime>()
    .HaveConversion<long>();

Alle booleschen Eigenschaften können mit einem der integrierten Wertkonverter in die Ganzzahlen 0 oder 1 umgewandelt werden:

configurationBuilder
    .Properties<bool>()
    .HaveConversion<BoolToZeroOneConverter<int>>();

Wenn Session eine transiente Eigenschaft der Entität ist und nicht beibehalten werden soll, kann sie überall im Modell ignoriert werden:

configurationBuilder
    .IgnoreAny<Session>();

Die Konfiguration des Präkonventionenmodells ist bei der Arbeit mit Wertobjekten sehr nützlich. Beispielsweise wird der Typ Money im obigen Modell durch eine schreibgeschützte Struktur dargestellt:

public readonly struct Money
{
    [JsonConstructor]
    public Money(decimal amount, Currency currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public override string ToString()
        => (Currency == Currency.UsDollars ? "$" : "£") + Amount;

    public decimal Amount { get; }
    public Currency Currency { get; }
}

public enum Currency
{
    UsDollars,
    PoundsSterling
}

Diese wird dann mithilfe eines benutzerdefinierten Wertkonverters in und aus JSON serialisiert:

public class MoneyConverter : ValueConverter<Money, string>
{
    public MoneyConverter()
        : base(
            v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
            v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null))
    {
    }
}

Dieser Wertkonverter kann einmal für alle Verwendungen von Money konfiguriert werden:

configurationBuilder
    .Properties<Money>()
    .HaveConversion<MoneyConverter>()
    .HaveMaxLength(64);

Beachten Sie auch, dass zusätzliche Facets für die Zeichenfolgenspalte angegeben werden können, in der der serialisierte JSON-Code gespeichert wird. In diesem Fall ist die Spalte auf eine maximale Länge von 64 beschränkt.

Die für SQL Server mithilfe von Migrationen erstellten Tabellen zeigen, wie die Konfiguration auf alle zugeordneten Spalten angewendet wurde:

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] varchar(1024) NULL,
    [IsActive] int NOT NULL,
    [AccountValue] nvarchar(64) NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
CREATE TABLE [Order] (
    [Id] int NOT NULL IDENTITY,
    [SpecialInstructions] varchar(1024) NULL,
    [OrderDate] bigint NOT NULL,
    [IsComplete] int NOT NULL,
    [Price] nvarchar(64) NOT NULL,
    [Discount] nvarchar(64) NULL,
    [CustomerId] int NULL,
    CONSTRAINT [PK_Order] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Order_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id])
);

Es ist auch möglich, eine Standardtypzuordnung für einen bestimmten Typ anzugeben. Beispiel:

configurationBuilder
    .DefaultTypeMapping<string>()
    .IsUnicode(false);

Dies ist selten erforderlich, kann aber nützlich sein, wenn ein Typ in einer Abfrage auf eine Weise verwendet wird, die nicht mit einer zugeordneten Eigenschaft des Modells übereinstimmt.

Hinweis

Weitere Informationen und Beispiele für die Konfiguration von Präkonventionenmodellen finden Sie unter Ankündigung von Entity Framework Core 6.0 Preview 6: Konfigurieren von Konventionen im .NET-Blog.

Kompilierte Modelle

GitHub-Issue: 1906.

Kompilierte Modelle können die EF Core-Startzeit für Anwendungen mit großen Modellen verkürzen. Ein großes Modell besitzt in der Regel hunderte bis tausende Entitätstypen und Beziehungen.

Die Startzeit ist die Zeit bis zum Ausführen des ersten Vorgangs für einen DbContext, wenn dieser DbContext-Typ zum ersten Mal in der Anwendung verwendet wird. Beachten Sie, dass das Erstellen einer DbContext-Instanz allein noch nicht dazu führt, dass das EF-Modell initialisiert wird. Typische erste Operationen, die das Modell initialisieren, sind dagegen das Aufrufen von DbContext.Add oder das Ausführen der ersten Abfrage.

Kompilierte Modelle werden mit dem dotnet ef-Befehlszeilenwerkzeug erstellt. Beachten Sie, dass Sie die aktuelle Version des Tools installiert haben müssen, bevor Sie fortfahren.

Um das kompilierte Modell zu erstellen, wird ein neuer dbcontext optimize-Befehl verwendet. Beispiel:

dotnet ef dbcontext optimize

Mit den Optionen --output-dir und --namespace kann das Verzeichnis und der Namespace angegeben werden, in denen das kompilierte Modell generiert wird. Beispiel:

PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels> dotnet ef dbcontext optimize --output-dir MyCompiledModels --namespace MyCompiledModels
Build started...
Build succeeded.
Successfully generated a compiled model, to use it call 'options.UseModel(MyCompiledModels.BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels>

Die Ausgabe der Ausführung dieses Befehls enthält einen Codeabschnitt zum Kopieren und Einfügen in die DbContext-Konfiguration, damit EF Core das kompilierte Modell verwendet. Beispiel:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseModel(MyCompiledModels.BlogsContextModel.Instance)
        .UseSqlite(@"Data Source=test.db");

Bootstrapping des kompilierten Modells

Es ist in der Regel nicht erforderlich, den generierten Bootstrappingcode zu beachten. Manchmal kann es jedoch hilfreich sein, das Modell oder dessen Ladevorgang anzupassen. Der Bootstrappingcode sieht in etwa wie folgt aus:

[DbContext(typeof(BlogsContext))]
partial class BlogsContextModel : RuntimeModel
{
    private static BlogsContextModel _instance;
    public static IModel Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new BlogsContextModel();
                _instance.Initialize();
                _instance.Customize();
            }

            return _instance;
        }
    }

    partial void Initialize();

    partial void Customize();
}

Dies ist eine partielle Klasse mit partiellen Methoden, die implementiert werden können, um das Modell nach Bedarf anzupassen.

Darüber hinaus können mehrere kompilierte Modelle für DbContext-Typen generiert werden, die je nach Laufzeitkonfiguration unterschiedliche Modelle verwenden können. Diese sollten wie oben gezeigt in verschiedenen Ordnern und Namespaces platziert werden. Laufzeitinformationen, z. B. die Verbindungszeichenfolge, können dann untersucht und das richtige Modell bei Bedarf zurückgegeben werden. Beispiel:

public static class RuntimeModelCache
{
    private static readonly ConcurrentDictionary<string, IModel> _runtimeModels
        = new();

    public static IModel GetOrCreateModel(string connectionString)
        => _runtimeModels.GetOrAdd(
            connectionString, cs =>
                {
                    if (cs.Contains("X"))
                    {
                        return BlogsContextModel1.Instance;
                    }

                    if (cs.Contains("Y"))
                    {
                        return BlogsContextModel2.Instance;
                    }

                    throw new InvalidOperationException("No appropriate compiled model found.");
                });
}

Einschränkungen

Für kompilierte Modelle gelten einige Einschränkungen:

Aufgrund dieser Einschränkungen sollten Sie kompilierte Modelle nur verwenden, wenn Ihre EF Core-Startzeit zu langsam ist. Das Kompilieren kleiner Modelle lohnt sich in der Regel nicht.

Wenn die Unterstützung eines dieser Features für Ihren Erfolg entscheidend ist, entscheiden Sie sich für die oben verlinkten Punkte.

Vergleichstests

Tipp

Sie können versuchen, ein großes Modell zu kompilieren und dafür einen Benchmark auszuführen, indem Sie den Beispielcode aus GitHub herunterladen.

Das Modell im oben erwähnten GitHub-Repository enthält 449 Entitätstypen, 6390 Eigenschaften und 720 Beziehungen. Dies ist ein nicht allzu großes Modell. Bei der Messung mit BenchmarkDotNet beträgt die durchschnittliche Zeit bis zur ersten Abfrage auf einem einigermaßen leistungsfähigen Laptop 1,02 Sekunden. Durch die Verwendung kompilierter Modelle wird diese auf derselben Hardware auf 117 Millisekunden verkürzt. Eine 8- bis 10-fache Verbesserung dieser Art bleibt fast immer erhalten, auch wenn die Modellgröße zunimmt.

Leistungssteigerung für kompilierte Modelle

Hinweis

Eine ausführlichere Erläuterung der EF Core-Startleistung und kompilierter Modelle finden Sie unter Ankündigung von Entity Framework Core 6.0 Preview 5: Kompilierte Modelle im .NET-Blog.

Höhere Leistung bei TechEmpower-Fortunes

GitHub-Issue: 23611.

Die Abfrageleistung für EF Core 6.0 wurde erheblich verbessert. Dies gilt insbesondere in folgenden Fällen:

  • Die Abfrageleistung für die branchenübliche TechEmpower-Benchmark ist bei EF Core 6.0 jetzt um 70 % höher als bei EF Core 5.0.
    • Diese Leistungssteigerung betrifft den gesamten Stack, einschließlich Verbesserungen im Benchmarkcode, der .NET-Runtime usw.
  • EF Core 6.0 selbst führt nicht nachverfolgte Abfragen 31 % schneller aus.
  • Heapzuordnungen wurden beim Ausführen von Abfragen um 43 % reduziert.

Nach diesen Verbesserungen reduzierte sich die Lücke zwischen dem beliebten „Micro-ORM“-Dapper und EF Core im TechEmpower Fortunes-Benchmark von 55 % auf etwa unter 5 %.

Hinweis

Eine ausführliche Erläuterung der Verbesserungen der Abfrageleistung in EF Core 6.0 finden Sie unter Ankündigung der Entity Framework Core 6.0 Preview 4: Performance Edition im .NET-Blog.

Cosmos-Erweiterungen

EF Core 6.0 enthält viele Verbesserungen für den Azure Cosmos DB-Datenbankanbieter.

Tipp

Sie können alle Cosmos-spezifischen Beispiele ausführen und debuggen, indem Sie den Beispielcode von GitHub herunterladen.

Standardmäßig impliziter Besitz

GitHub-Issue: 24803.

Beim Erstellen eines Modells für den Cosmos-Anbieter markiert EF Core 6.0 untergeordnete Entitätstypen standardmäßig als sich im Besitz ihrer übergeordneten Entität befindend. Somit ist der größte Teil der OwnsMany- und OwnsOne-Aufrufe im Cosmos-Modell nicht mehr erforderlich. Dies erleichtert das Einbetten untergeordneter Typen in das Dokument für den übergeordneten Typ. Dies ist in der Regel die geeignete Methode zum Modellieren von übergeordneten und untergeordneten Typen in einer Dokumentdatenbank.

Betrachten Sie beispielsweise die folgenden Entitätstypen:

public class Family
{
    [JsonPropertyName("id")]
    public string Id { get; set; }
    
    public string LastName { get; set; }
    public bool IsRegistered { get; set; }
    
    public Address Address { get; set; }

    public IList<Parent> Parents { get; } = new List<Parent>();
    public IList<Child> Children { get; } = new List<Child>();
}

public class Parent
{
    public string FamilyName { get; set; }
    public string FirstName { get; set; }
}

public class Child
{
    public string FamilyName { get; set; }
    public string FirstName { get; set; }
    public int Grade { get; set; }

    public string Gender { get; set; }

    public IList<Pet> Pets { get; } = new List<Pet>();
}

In EF Core 5.0 wurden diese Typen für Cosmos mit der folgenden Konfiguration modelliert:

modelBuilder.Entity<Family>()
    .HasPartitionKey(e => e.LastName)
    .OwnsMany(f => f.Parents);

modelBuilder.Entity<Family>()
    .OwnsMany(f => f.Children)
    .OwnsMany(c => c.Pets);

modelBuilder.Entity<Family>()
    .OwnsOne(f => f.Address);        

In EF Core 6.0 ist der Besitz implizit, wodurch die Modellkonfiguration auf Folgendes reduziert wird:

modelBuilder.Entity<Family>().HasPartitionKey(e => e.LastName);

Die resultierenden Cosmos-Dokumente enthalten die Eltern, Kinder, Haustiere und Adresse der Familie, die in das Familiendokument eingebettet sind. Beispiel:

{
  "Id": "Wakefield.7",
  "LastName": "Wakefield",
  "Discriminator": "Family",
  "IsRegistered": true,
  "id": "Family|Wakefield.7",
  "Address": {
    "City": "NY",
    "County": "Manhattan",
    "State": "NY"
  },
  "Children": [
    {
      "FamilyName": "Merriam",
      "FirstName": "Jesse",
      "Gender": "female",
      "Grade": 8,
      "Pets": [
        {
          "GivenName": "Goofy"
        },
        {
          "GivenName": "Shadow"
        }
      ]
    },
    {
      "FamilyName": "Miller",
      "FirstName": "Lisa",
      "Gender": "female",
      "Grade": 1,
      "Pets": []
    }
  ],
  "Parents": [
    {
      "FamilyName": "Wakefield",
      "FirstName": "Robin"
    },
    {
      "FamilyName": "Miller",
      "FirstName": "Ben"
    }
  ],
  "_rid": "x918AKh6p20CAAAAAAAAAA==",
  "_self": "dbs/x918AA==/colls/x918AKh6p20=/docs/x918AKh6p20CAAAAAAAAAA==/",
  "_etag": "\"00000000-0000-0000-adee-87f30c8c01d7\"",
  "_attachments": "attachments/",
  "_ts": 1632121802
}

Hinweis

Beachten Sie, dass die Konfiguration OwnsOne/OwnsMany verwendet werden muss, wenn Sie diese sich im Besitz befindenden Typen weiter konfigurieren müssen.

Eine Sammlung primitiver Typen

GitHub-Issue: 14762.

EF Core 6.0 ordnet Sammlungen primitiver Typen nativ zu, wenn der Cosmos-Datenbankanbieter verwendet wird. Betrachten Sie beispielsweise die folgenden Entitätstypen:

public class Book
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public IList<string> Quotes { get; set; }
    public IDictionary<string, string> Notes { get; set; }
}

Sowohl die Liste als auch das Wörterbuch können auf normale Weise aufgefüllt und in die Datenbank eingefügt werden:

using var context = new BooksContext();

var book = new Book
{
    Title = "How It Works: Incredible History",
    Quotes = new List<string>
    {
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    },
    Notes = new Dictionary<string, string>
    {
        { "121", "Fridges" },
        { "144", "Peter Higgs" },
        { "48", "Saint Mark's Basilica" },
        { "36", "The Terracotta Army" }
    }
};

context.Add(book);
context.SaveChanges();

Dies ergibt folgendes JSON-Dokument:

{
    "Id": "0b32283e-22a8-4103-bb4f-6052604868bd",
    "Discriminator": "Book",
    "Notes": {
        "36": "The Terracotta Army",
        "48": "Saint Mark's Basilica",
        "121": "Fridges",
        "144": "Peter Higgs"
    },
    "Quotes": [
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    ],
    "Title": "How It Works: Incredible History",
    "id": "Book|0b32283e-22a8-4103-bb4f-6052604868bd",
    "_rid": "t-E3AIxaencBAAAAAAAAAA==",
    "_self": "dbs/t-E3AA==/colls/t-E3AIxaenc=/docs/t-E3AIxaencBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9b50-fc769dc901d7\"",
    "_attachments": "attachments/",
    "_ts": 1630075016
}

Diese Sammlungen können dann wieder auf die übliche Weise aktualisiert werden:

book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";

context.SaveChanges();

Einschränkungen:

  • Nur Wörterbücher mit Zeichenfolgenschlüsseln werden unterstützt.
  • Das Abfragen des Inhalts primitiver Sammlungen wird derzeit nicht unterstützt. Entscheiden Sie sich für #16926, #25700 sowie #25701, wenn diese Features für Sie wichtig sind.

Übersetzungen für integrierte Funktionen

GitHub-Issue: 16143.

Der Cosmos-Anbieter übersetzt jetzt weitere BCL-Methoden (der Base Class Library) in integrierte Cosmos-Funktionen. In den folgenden Tabellen sind die Übersetzungen aufgeführt, die in EF Core 6.0 neu sind.

Zeichenfolgenübersetzungen

BCL-Methode Integrierte Funktion Notizen
String.Length LENGTH
String.ToLower LOWER
String.TrimStart LTRIM
String.TrimEnd RTRIM
String.Trim TRIM
String.ToUpper UPPER
String.Substring SUBSTRING
+-Operator CONCAT
String.IndexOf INDEX_OF
String.Replace REPLACE
String.Equals STRINGEQUAL Nur Aufrufe ohne Unterscheidung nach Groß-/Kleinschreibung

Die Übersetzungen für LOWER, LTRIM, RTRIM, TRIM, UPPER und SUBSTRING wurden von LOWER beigesteuert. Danke vielmals!

Beispiel:

var stringResults
    = context.Triangles.Where(
            e => e.Name.Length > 4
                 && e.Name.Trim().ToLower() != "obtuse"
                 && e.Name.TrimStart().Substring(2, 2).Equals("uT", StringComparison.OrdinalIgnoreCase))
        .ToList();

Übersetzt:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((LENGTH(c["Name"]) > 4) AND (LOWER(TRIM(c["Name"])) != "obtuse")) AND STRINGEQUALS(SUBSTRING(LTRIM(c["Name"]), 2, 2), "uT", true)))

Mathematische Übersetzungen

BCL-Methode Integrierte Funktion
Math.Abs oder MathF.Abs ABS
Math.Acos oder MathF.Acos ACOS
Math.Asin oder MathF.Asin ASIN
Math.Atan oder MathF.Atan ATAN
Math.Atan2 oder MathF.Atan2 ATN2
Math.Ceiling oder MathF.Ceiling CEILING
Math.Cos oder MathF.Cos COS
Math.Exp oder MathF.Exp EXP
Math.Floor oder MathF.Floor FLOOR
Math.Log oder MathF.Log LOG
Math.Log10 oder MathF.Log10 LOG10
Math.Pow oder MathF.Pow POWER
Math.Round oder MathF.Round ROUND
Math.Sign oder MathF.Sign SIGN
Math.Sin oder MathF.Sin SIN
Math.Sqrt oder MathF.Sqrt SQRT
Math.Tan oder MathF.Tan TAN
Math.Truncate oder MathF.Truncate TRUNC
DbFunctions.Random RAND

Diese Übersetzungen wurden von @Marusyk beigetragen. Danke vielmals!

Beispiel:

var hypotenuse = 42.42;
var mathResults
    = context.Triangles.Where(
            e => (Math.Round(e.Angle1) == 90.0
                  || Math.Round(e.Angle2) == 90.0)
                 && (hypotenuse * Math.Sin(e.Angle1) > 30.0
                     || hypotenuse * Math.Cos(e.Angle2) > 30.0))
        .ToList();

Übersetzt:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((ROUND(c["Angle1"]) = 90.0) OR (ROUND(c["Angle2"]) = 90.0)) AND (((@__hypotenuse_0 * SIN(c["Angle1"])) > 30.0) OR ((@__hypotenuse_0 * COS(c["Angle2"])) > 30.0))))

DateTime-Übersetzungen

BCL-Methode Integrierte Funktion
DateTime.UtcNow GetCurrentDateTime

Diese Übersetzungen wurden von @Marusyk beigetragen. Danke vielmals!

Beispiel:

var timeResults
    = context.Triangles.Where(
            e => e.InsertedOn <= DateTime.UtcNow)
        .ToList();

Übersetzt:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (c["InsertedOn"] <= GetCurrentDateTime()))

Unformatierte SQL-Abfragen mit FromSql

GitHub-Issue: 17311.

Manchmal ist es notwendig, eine unformatierte SQL-Abfrage auszuführen, statt LINQ zu verwenden. Dies wird jetzt von Cosmos über die FromSql-Methode unterstützt. Dies funktioniert genauso wie bei relationalen Anbietern. Beispiel:

var maxAngle = 60;
var rawResults
    = context.Triangles.FromSqlRaw(
            @"SELECT * FROM root c WHERE c[""Angle1""] <= {0} OR c[""Angle2""] <= {0}",
            maxAngle)
        .ToList();

Wird wie folgt ausgeführt:

SELECT c
FROM (
         SELECT * FROM root c WHERE c["Angle1"] <= @p0 OR c["Angle2"] <= @p0
     ) c
WHERE (c["Discriminator"] = "Triangle")

Unterschiedliche Abfragen

GitHub-Issue: 16144.

Abfragen, die Distinct verwenden, werden jetzt übersetzt. Beispiel:

var distinctResults
    = context.Triangles
        .Select(e => e.Angle1).OrderBy(e => e).Distinct()
        .ToList();

Übersetzt:

SELECT DISTINCT c["Angle1"]
FROM root c
WHERE (c["Discriminator"] = "Triangle")
ORDER BY c["Angle1"]

Diagnose

GitHub-Issue: 17298.

Cosmos protokolliert jetzt weitere Diagnoseinformationen, beispielsweise Ereignisse zum Einfügen, Abfragen, Aktualisieren und Löschen von Daten aus der Datenbank. Die Anforderungseinheiten (Request Units, RU) sind in diesen Ereignissen enthalten, wenn dies sinnvoll ist.

Hinweis

Die hier gezeigten Protokolle verwenden EnableSensitiveDataLogging(), so dass ID-Werte angezeigt werden.

Beim Einfügen eines Elements in die Cosmos-Datenbank wird das Ereignis CosmosEventId.ExecutedCreateItem generiert. Ein Beispiel ist der folgende Code:

var triangle = new Triangle
{
    Name = "Impossible",
    PartitionKey = "TrianglesPartition",
    Angle1 = 90,
    Angle2 = 90,
    InsertedOn = DateTime.UtcNow
};
context.Add(triangle);
context.SaveChanges();

Protokolliert folgendes Diagnoseereignis:

info: 8/30/2021 14:41:13.356 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed CreateItem (5 ms, 7.43 RU) ActivityId='417db46f-fcdd-49d9-a7f0-77210cd06f84', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

Beim Abrufen von Elementen aus Cosmos-Datenbank mithilfe einer Abfrage wird das CosmosEventId.ExecutingSqlQuery-Ereignis generiert, anschließend werden ein oder mehrere CosmosEventId.ExecutedReadNext-Ereignisse für die gelesenen Elemente generiert. Ein Beispiel ist der folgende Code:

var equilateral = context.Triangles.Single(e => e.Name == "Equilateral");

Protokolliert die folgenden Diagnoseereignisse:

info: 8/30/2021 14:41:13.475 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing SQL query for container 'Shapes' in partition '(null)' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
      OFFSET 0 LIMIT 2
info: 8/30/2021 14:41:13.651 CosmosEventId.ExecutedReadNext[30102] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReadNext (169.6126 ms, 2.93 RU) ActivityId='4e465fae-3d49-4c1f-bd04-142bc5d0b0a1', Container='Shapes', Partition='(null)', Parameters=[]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
      OFFSET 0 LIMIT 2

Das Abrufen eines einzelnen Elements aus der Cosmos-Datenbank mithilfe von Find mit einem Partitionsschlüssel generiert die Ereignisse CosmosEventId.ExecutingReadItem und CosmosEventId.ExecutedReadItem. Ein Beispiel ist der folgende Code:

var isosceles = context.Triangles.Find("Isosceles", "TrianglesPartition");

Protokolliert die folgenden Diagnoseereignisse:

info: 8/30/2021 14:53:39.326 CosmosEventId.ExecutingReadItem[30101] (Microsoft.EntityFrameworkCore.Database.Command)
      Reading resource 'Isosceles' item from container 'Shapes' in partition 'TrianglesPartition'.
info: 8/30/2021 14:53:39.330 CosmosEventId.ExecutedReadItem[30103] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReadItem (1 ms, 1 RU) ActivityId='3c278643-4e7f-4bb2-9953-6055b5f1288f', Container='Shapes', Id='Isosceles', Partition='TrianglesPartition'

Das Speichern eines aktualisierten Elements in der Cosmos-Datenbank generiert das CosmosEventId.ExecutedReplaceItem-Ereignis. Ein Beispiel ist der folgende Code:

triangle.Angle2 = 89;
context.SaveChanges();

Protokolliert folgendes Diagnoseereignis:

info: 8/30/2021 14:53:39.343 CosmosEventId.ExecutedReplaceItem[30105] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReplaceItem (6 ms, 10.67 RU) ActivityId='1525b958-fea1-49e8-89f9-d429d0351fdb', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

Wenn Sie ein Element aus der Cosmos löschen, wird das Ereignis CosmosEventId.ExecutedDeleteItem generiert. Ein Beispiel ist der folgende Code:

context.Remove(triangle);
context.SaveChanges();

Protokolliert folgendes Diagnoseereignis:

info: 8/30/2021 14:53:39.359 CosmosEventId.ExecutedDeleteItem[30106] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DeleteItem (6 ms, 7.43 RU) ActivityId='cbc54463-405b-48e7-8c32-2c6502a4138f', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

Konfigurieren des Durchsatzes

GitHub-Issue: 17301.

Das Cosmos-Modell kann jetzt mit manuell oder automatisch skaliertem Durchsatz konfiguriert werden. Diese Werte bestimmen den Durchsatz für die Datenbank. Beispiel:

modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(6000);

Darüber hinaus können einzelne Entitätstypen so konfiguriert werden, dass der Durchsatz für den entsprechenden Container bereitgestellt wird. Beispiel:

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasAutoscaleThroughput(3000);
        entityTypeBuilder.HasAutoscaleThroughput(12000);
    });

Konfigurieren der Gültigkeitsdauer

GitHub-Issue: 17307.

Entitätstypen im Cosmos-Modell können jetzt mit der Standardgültigkeitsdauer für den Analysespeicher konfiguriert werden. Beispiel:

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasDefaultTimeToLive(100);
        entityTypeBuilder.HasAnalyticalStoreTimeToLive(200);
    });

Auflösen der HTTP-Client Factory

GitHub-Issue: 21274. Dieses Feature wurde von @dnperfors beigetragen. Danke vielmals!

Die vom Cosmos-Anbieter verwendete HttpClientFactory kann jetzt explizit festgelegt werden. Dies kann besonders beim Testen nützlich sein, z. B. um die Zertifikatüberprüfung zu umgehen, wenn der Cosmos-Emulator unter Linux verwendet wird:

Hinweis

Ein ausführliches Beispiel für die Anwendung der Verbesserungen des Cosmos-Anbieters auf eine vorhandene Anwendung finden Sie im .NET-Blog unter Verwenden des EF Core Azure Cosmos-Datenbankanbieters für eine Testlauf.

Verbesserungen am Gerüstbau einer vorhandenen Datenbank

In EF Core 6.0 sind mehrere Verbesserungen für das Reverse Engineering eines EF-Modells aus einer vorhandenen Datenbank enthalten.

Gerüstbau für n:n-Beziehungen

GitHub-Issue: 22475.

EF Core 6.0 erkennt einfache Jointabellen und generiert automatisch eine m:n-Zuordnung für sie. Betrachten Sie beispielsweise Tabellen für Posts und Tags und eine Jointabelle, PostTag, die sie verbindet:

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]));

CREATE TABLE [PostTag] (
    [PostsId] int NOT NULL,
    [TagsId] int NOT NULL,
    CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
    CONSTRAINT [FK_PostTag_Posts_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE);

Diese Tabellen können über die Befehlszeile erstellt werden. Beispiel:

dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=BloggingWithNRTs" Microsoft.EntityFrameworkCore.SqlServer

Dies führt zu einer Klasse für Post:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

Dazu kommt eine Klasse für Tag:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

Es gibt jedoch keine Klasse für die PostTag-Tabelle. Stattdessen wird ein Gerüst für die Konfiguration für eine n:n-Beziehung erstellt:

entity.HasMany(d => d.Tags)
    .WithMany(p => p.Posts)
    .UsingEntity<Dictionary<string, object>>(
        "PostTag",
        l => l.HasOne<Tag>().WithMany().HasForeignKey("PostsId"),
        r => r.HasOne<Post>().WithMany().HasForeignKey("TagsId"),
        j =>
            {
                j.HasKey("PostsId", "TagsId");
                j.ToTable("PostTag");
                j.HasIndex(new[] { "TagsId" }, "IX_PostTag_TagsId");
            });

Gerüstbau für C#-Nullverweistypen

GitHub-Issue: 15520.

EF Core 6.0 richtet über Gerüstet jetzt ein EF-Modell und Entitätstypen ein, die C#-Nullverweistypen (NRTs, Nullable Reference Types) verwenden. Die NRT-Nutzung wird automatisch erstellt, wenn die NRT-Unterstützung in dem C#-Projekt aktiviert ist, in dem der Code erstellt wird.

Die folgende Tags-Tabelle enthält z. B. beide Nullwerte zulassende Non-Nullable-Zeichenfolgenspalten:

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

Dies führt in der generierten Klasse zu entsprechenden Nullwerte zulassenden und Non-Nullable-Zeichenfolgeneigenschaften:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

Auf ähnliche Weise enthalten die folgenden Posts-Tabellen eine erforderliche Beziehung zur Blogs-Tabelle:

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]));

Dies führt zum Gerüstbau einer Non-Nullable--Beziehung (erforderlich) zwischen Blogs:

public partial class Blog
{
    public Blog()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;

    public virtual ICollection<Post> Posts { get; set; }
}

Sowie zwischen Beiträgen:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

Schließlich werden DbSet-Eigenschaften im generierten DbContext auf NRT-freundliche Weise erstellt. Beispiel:

public virtual DbSet<Blog> Blogs { get; set; } = null!;
public virtual DbSet<Post> Posts { get; set; } = null!;
public virtual DbSet<Tag> Tags { get; set; } = null!;

Integration von Datenbankkommentaren in Codekommentare

GitHub-Issue: 19113. Dieses Feature wurde von @ErikEJ beigetragen. Danke vielmals!

Kommentare in SQL-Tabellen und -Spalten werden jetzt in die erstellten Entitätstypen integriert, wenn für ein EF Core-Modell ein Reverse Engineering aus einer vorhandenen SQL Server-Datenbank erfolgt.

/// <summary>
/// The Blog table.
/// </summary>
public partial class Blog
{
    /// <summary>
    /// The primary key.
    /// </summary>
    [Key]
    public int Id { get; set; }
}

LINQ-Abfrageerweiterungen

EF Core 6.0 enthält mehrere Verbesserungen bei der Übersetzung und Ausführung von LINQ-Abfragen.

Verbesserte GroupBy-Unterstützung

GitHub-Issues: #12088, #13805, und #22609.

EF Core 6.0 enthält eine bessere Unterstützung für GroupBy-Abfragen. EF Core unterstützt jetzt insbesondere die folgenden Aktionen:

  • Übersetzen von GroupBy gefolgt von FirstOrDefault (oder ähnlich) über eine Gruppe
  • Unterstützt das Auswählen der obersten N Ergebnisse aus einer Gruppe
  • Erweitert Navigationen, nachdem der Operator GroupBy angewendet wurde

Im Folgenden finden Sie Beispielabfragen aus Kundenberichten und deren Übersetzung auf SQL Server.

Beispiel 1:

var people = context.People
    .Include(e => e.Shoes)
    .GroupBy(e => e.FirstName)
    .Select(
        g => g.OrderBy(e => e.FirstName)
            .ThenBy(e => e.LastName)
            .FirstOrDefault())
    .ToList();
SELECT [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t].[FirstName], [s].[Id], [s].[Age], [s].[PersonId], [s].[Style]
FROM (
    SELECT [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
    FROM (
        SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName], [p0].[LastName]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
LEFT JOIN [Shoes] AS [s] ON [t0].[Id] = [s].[PersonId]
ORDER BY [t].[FirstName], [t0].[FirstName]

Beispiel 2:

var group = context.People
    .Select(
        p => new
        {
            p.FirstName,
            FullName = p.FirstName + " " + p.MiddleInitial + " " + p.LastName
        })
    .GroupBy(p => p.FirstName)
    .Select(g => g.First())
    .First();
SELECT [t0].[FirstName], [t0].[FullName], [t0].[c]
FROM (
    SELECT TOP(1) [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[FirstName], [t1].[FullName], [t1].[c]
    FROM (
        SELECT [p0].[FirstName], (((COALESCE([p0].[FirstName], N'') + N' ') + COALESCE([p0].[MiddleInitial], N'')) + N' ') + COALESCE([p0].[LastName], N'') AS [FullName], 1 AS [c], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]

Beispiel 3:

var people = context.People
    .Where(e => e.MiddleInitial == "Q" && e.Age == 20)
    .GroupBy(e => e.LastName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e.Length)
    .ToList();
SELECT (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))
FROM [People] AS [p]
WHERE ([p].[MiddleInitial] = N'Q') AND ([p].[Age] = 20)
GROUP BY [p].[LastName]
ORDER BY CAST(LEN((
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))) AS int)

Beispiel 4:

var results = (from person in context.People
               join shoes in context.Shoes on person.Age equals shoes.Age
               group shoes by shoes.Style
               into people
               select new
               {
                   people.Key,
                   Style = people.Select(p => p.Style).FirstOrDefault(),
                   Count = people.Count()
               })
    .ToList();
SELECT [s].[Style] AS [Key], (
    SELECT TOP(1) [s0].[Style]
    FROM [People] AS [p0]
    INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
    WHERE ([s].[Style] = [s0].[Style]) OR ([s].[Style] IS NULL AND [s0].[Style] IS NULL)) AS [Style], COUNT(*) AS [Count]
FROM [People] AS [p]
INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
GROUP BY [s].[Style]

Beispiel 5:

var results = context.People
    .GroupBy(e => e.FirstName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e)
    .ToList();
SELECT (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))
FROM [People] AS [p]
GROUP BY [p].[FirstName]
ORDER BY (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))

Beispiel 6:

var results = context.People.Where(e => e.Age == 20)
    .GroupBy(e => e.Id)
    .Select(g => g.First().MiddleInitial)
    .OrderBy(e => e)
    .ToList();
SELECT (
    SELECT TOP(1) [p1].[MiddleInitial]
    FROM [People] AS [p1]
    WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))
FROM [People] AS [p]
WHERE [p].[Age] = 20
GROUP BY [p].[Id]
ORDER BY (
    SELECT TOP(1) [p1].[MiddleInitial]
    FROM [People] AS [p1]
    WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))

Beispiel 7:

var size = 11;
var results
    = context.People
        .Where(
            p => p.Feet.Size == size
                 && p.MiddleInitial != null
                 && p.Feet.Id != 1)
        .GroupBy(
            p => new
            {
                p.Feet.Size,
                p.Feet.Person.LastName
            })
        .Select(
            g => new
            {
                g.Key.LastName,
                g.Key.Size,
                Min = g.Min(p => p.Feet.Size),
            })
        .ToList();
Executed DbCommand (12ms) [Parameters=[@__size_0='11'], CommandType='Text', CommandTimeout='30']
SELECT [p0].[LastName], [f].[Size], MIN([f0].[Size]) AS [Min]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
LEFT JOIN [People] AS [p0] ON [f].[Id] = [p0].[Id]
LEFT JOIN [Feet] AS [f0] ON [p].[Id] = [f0].[Id]
WHERE (([f].[Size] = @__size_0) AND [p].[MiddleInitial] IS NOT NULL) AND (([f].[Id] <> 1) OR [f].[Id] IS NULL)
GROUP BY [f].[Size], [p0].[LastName]

Beispiel 8:

var result = context.People
    .Include(x => x.Shoes)
    .Include(x => x.Feet)
    .GroupBy(
        x => new
        {
            x.Feet.Id,
            x.Feet.Size
        })
    .Select(
        x => new
        {
            Key = x.Key.Id + x.Key.Size,
            Count = x.Count(),
            Sum = x.Sum(el => el.Id),
            SumOver60 = x.Sum(el => el.Id) / (decimal)60,
            TotalCallOutCharges = x.Sum(el => el.Feet.Size == 11 ? 1 : 0)
        })
    .Count();
SELECT COUNT(*)
FROM (
    SELECT [f].[Id], [f].[Size]
    FROM [People] AS [p]
    LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
    GROUP BY [f].[Id], [f].[Size]
) AS [t]

Beispiel 9:

var results = context.People
    .GroupBy(n => n.FirstName)
    .Select(g => new
    {
        Feet = g.Key,
        Total = g.Sum(n => n.Feet.Size)
    })
    .ToList();
SELECT [p].[FirstName] AS [Feet], COALESCE(SUM([f].[Size]), 0) AS [Total]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
GROUP BY [p].[FirstName]

Beispiel 10:

var results = from Person person1
                  in from Person person2
                         in context.People
                     select person2
              join Shoes shoes
                  in context.Shoes
                  on person1.Age equals shoes.Age
              group shoes by
                  new
                  {
                      person1.Id,
                      shoes.Style,
                      shoes.Age
                  }
              into temp
              select
                  new
                  {
                      temp.Key.Id,
                      temp.Key.Age,
                      temp.Key.Style,
                      Values = from t
                                   in temp
                               select
                                   new
                                   {
                                       t.Id,
                                       t.Style,
                                       t.Age
                                   }
                  };
SELECT [t].[Id], [t].[Age], [t].[Style], [t0].[Id], [t0].[Style], [t0].[Age], [t0].[Id0]
FROM (
    SELECT [p].[Id], [s].[Age], [s].[Style]
    FROM [People] AS [p]
    INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
    GROUP BY [p].[Id], [s].[Style], [s].[Age]
) AS [t]
LEFT JOIN (
    SELECT [s0].[Id], [s0].[Style], [s0].[Age], [p0].[Id] AS [Id0]
    FROM [People] AS [p0]
    INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
) AS [t0] ON (([t].[Id] = [t0].[Id0]) AND (([t].[Style] = [t0].[Style]) OR ([t].[Style] IS NULL AND [t0].[Style] IS NULL))) AND ([t].[Age] = [t0].[Age])
ORDER BY [t].[Id], [t].[Style], [t].[Age], [t0].[Id0]

Beispiel 11:

var grouping = context.People
    .GroupBy(i => i.LastName)
    .Select(g => new { LastName = g.Key, Count = g.Count() , First = g.FirstOrDefault(), Take = g.Take(2)})
    .OrderByDescending(e => e.LastName)
    .ToList();
SELECT [t].[LastName], [t].[c], [t0].[Id], [t2].[Id], [t2].[Age], [t2].[FirstName], [t2].[LastName], [t2].[MiddleInitial], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial]
FROM (
    SELECT [p].[LastName], COUNT(*) AS [c]
    FROM [People] AS [p]
    GROUP BY [p].[LastName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
    FROM (
        SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[LastName] ORDER BY [p0].[Id]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[LastName] = [t0].[LastName]
LEFT JOIN (
    SELECT [t3].[Id], [t3].[Age], [t3].[FirstName], [t3].[LastName], [t3].[MiddleInitial]
    FROM (
        SELECT [p1].[Id], [p1].[Age], [p1].[FirstName], [p1].[LastName], [p1].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p1].[LastName] ORDER BY [p1].[Id]) AS [row]
        FROM [People] AS [p1]
    ) AS [t3]
    WHERE [t3].[row] <= 2
) AS [t2] ON [t].[LastName] = [t2].[LastName]
ORDER BY [t].[LastName] DESC, [t0].[Id], [t2].[LastName], [t2].[Id]

Beispiel 12:

var grouping = context.People
    .Include(e => e.Shoes)
    .OrderBy(e => e.FirstName)
    .ThenBy(e => e.LastName)
    .GroupBy(e => e.FirstName)
    .Select(g => new { Name = g.Key, People = g.ToList()})
    .ToList();
SELECT [t].[FirstName], [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t0].[Id0], [t0].[Age0], [t0].[PersonId], [t0].[Style]
FROM (
    SELECT [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], [s].[Id] AS [Id0], [s].[Age] AS [Age0], [s].[PersonId], [s].[Style]
    FROM [People] AS [p0]
    LEFT JOIN [Shoes] AS [s] ON [p0].[Id] = [s].[PersonId]
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
ORDER BY [t].[FirstName], [t0].[Id]

Beispiel 13:

var grouping = context.People
    .GroupBy(m => new {m.FirstName, m.MiddleInitial })
    .Select(am => new
    {
        Key = am.Key,
        Items = am.ToList()
    })
    .ToList();
SELECT [t].[FirstName], [t].[MiddleInitial], [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial]
FROM (
    SELECT [p].[FirstName], [p].[MiddleInitial]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName], [p].[MiddleInitial]
) AS [t]
LEFT JOIN [People] AS [p0] ON (([t].[FirstName] = [p0].[FirstName]) OR ([t].[FirstName] IS NULL AND [p0].[FirstName] IS NULL)) AND (([t].[MiddleInitial] = [p0].[MiddleInitial]) OR ([t].[MiddleInitial] IS NULL AND [p0].[MiddleInitial] IS NULL))
ORDER BY [t].[FirstName], [t].[MiddleInitial]

Modell

Für diese Beispiele werden die folgenden Entitätstypen verwendet:

public class Person
{
    public int Id { get; set; }
    public int Age { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string MiddleInitial { get; set; }
    public Feet Feet { get; set; }
    public ICollection<Shoes> Shoes { get; } = new List<Shoes>();
}

public class Shoes
{
    public int Id { get; set; }
    public int Age { get; set; }
    public string Style { get; set; }
    public Person Person { get; set; }
}

public class Feet
{
    public int Id { get; set; }
    public int Size { get; set; }
    public Person Person { get; set; }
}

Übersetzen von String.Concat mit mehreren Argumenten

GitHub-Issue: 23859. Dieses Feature wurde von @wmeints beigetragen. Danke vielmals!

Ab EF Core 6.0 werden Aufrufe von String.Concat mit mehreren Argumenten jetzt in SQL übersetzt. Beispielsweise wird die folgende Abfrage:

var shards = context.Shards
    .Where(e => string.Concat(e.Token1, e.Token2, e.Token3) != e.TokensProcessed).ToList();

Sie wird jetzt in folgenden SQL-Code übersetzt, wenn Sie SQL Server verwenden:

SELECT [s].[Id], [s].[Token1], [s].[Token2], [s].[Token3], [s].[TokensProcessed]
FROM [Shards] AS [s]
WHERE (([s].[Token1] + ([s].[Token2] + [s].[Token3])) <> [s].[TokensProcessed]) OR [s].[TokensProcessed] IS NULL

Reibungslosere Integration in System.Linq.Async

GitHub-Issue: 24041.

Mit dem System.Linq.Async-Paket wird clientseitige asynchrone LINQ-Verarbeitung hinzugefügt. Die Verwendung dieses Pakets mit früheren Versionen von EF Core war aufgrund eines Namespacekonflikts für die asynchronen LINQ-Methoden mühsam. In EF Core 6.0 haben wir den C#-Musterabgleich für IAsyncEnumerableT> so genutzt, dass das verfügbar gemachte EF Core-Element > die Schnittstelle nicht direkt implementieren muss.

Beachten Sie, dass die meisten Anwendungen System.Linq.Async nicht verwenden müssen, da EF Core-Abfragen in der Regel vollständig auf dem Server übersetzt werden.

GitHub-Issue: 23921.

In EF Core 6.0 haben wir die Parameteranforderungen für FreeText(DbFunctions, String, String) und Contains gelockert. Dadurch können diese Funktionen mit binären Spalten oder mit Spalten verwendet werden, die mit einem Wertkonverter zugeordnet werden. Stellen Sie sich beispielsweise einen Entitätstyp mit einer Eigenschaft Name vor, die als Wertobjekt definiert ist:

public class Customer
{
    public int Id { get; set; }

    public Name Name{ get; set; }
}

public class Name
{
    public string First { get; set; }
    public string MiddleInitial { get; set; }
    public string Last { get; set; }
}

Diese wird JSON in der Datenbank zugeordnet:

modelBuilder.Entity<Customer>()
    .Property(e => e.Name)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<Name>(v, (JsonSerializerOptions)null));

Eine Abfrage kann nun mithilfe von Contains oder FreeText ausgeführt werden, obwohl der Typ der-Eigenschaft Name und nicht string ist. Beispiel:

var result = context.Customers.Where(e => EF.Functions.Contains(e.Name, "Martin")).ToList();

Der folgende SQL-Code wird generiert, wenn SQL Server verwendet wird:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE CONTAINS([c].[Name], N'Martin')

Übersetzen von ToString in SQLite

GitHub-Issue: 17223. Dieses Feature wurde von @ralmsdeveloper beigetragen. Danke vielmals!

Aufrufe von ToString() werden bei Verwendung des SQLite-Datenbankanbieters jetzt in SQL übersetzt. Das kann für Textsuchen nützlich sein, die Spalten mit anderen Werten als Zeichenfolgen betreffen. Nehmen Sie beispielsweise an, dass ein User-Entitätstyp vorliegt, der Telefonnummern als numerische Werte speichert:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public long PhoneNumber { get; set; }
}

ToString kann verwendet werden, um die Nummer in der Datenbank in eine Zeichenfolge zu konvertieren. Diese Zeichenfolge können Sie dann mit einer Funktion wie LIKE suchen, um Nummern zu ermitteln, die einem Muster entsprechen. Sie könnten beispielsweise alle Nummern mit 555 suchen:

var users = context.Users.Where(u => EF.Functions.Like(u.PhoneNumber.ToString(), "%555%")).ToList();

Wenn Sie eine SQLite-Datenbank verwenden, wird dieser Code in den folgenden SQL-Code übersetzt:

SELECT "u"."Id", "u"."PhoneNumber", "u"."Username"
FROM "Users" AS "u"
WHERE CAST("u"."PhoneNumber" AS TEXT) LIKE '%555%'

Hinweis: Die Übersetzung von ToString() für SQL Server wird bereits in EF Core 5.0 und möglicherweise auch von anderen Datenbankanbieter unterstützt.

EF.Functions.Random

GitHub-Issue: 16141. Dieses Feature wurde von @RaymondHuy beigetragen. Danke vielmals!

EF.Functions.Random ist einer Datenbankfunktion zugeordnet, die eine Pseudozufallszahl zwischen 0 und ausschließlich 1 zurückgibt. Übersetzungen wurden im EF Core-Repository für SQL Server, SQLite und Cosmos implementiert. Nehmen Sie beispielsweise an, dass ein User-Entitätstyp mit der Eigenschaft Popularity vorliegt:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public int Popularity { get; set; }
}

Popularity kann Werte von 1 bis einschließlich 5 aufweisen. Mithilfe EF.Functions.Random von können Sie eine Abfrage schreiben, um alle Benutzer mit einer zufällig ausgewählten Beliebtheit zurückzugeben:

var users = context.Users.Where(u => u.Popularity == (int)(EF.Functions.Random() * 5.0) + 1).ToList();

Wenn Sie eine SQL Server-Datenbank verwenden, wird dieser Code in den folgenden SQL-Code übersetzt:

SELECT [u].[Id], [u].[Popularity], [u].[Username]
FROM [Users] AS [u]
WHERE [u].[Popularity] = (CAST((RAND() * 5.0E0) AS int) + 1)

Verbesserte SQL Server-Übersetzung für IsNullOrWhitespace

GitHub-Issue: 22916. Dieses Feature wurde von @Marusyk beigetragen. Danke vielmals!

Betrachten Sie die folgende Abfrage:

var users = context.Users.Where(
    e => string.IsNullOrWhiteSpace(e.FirstName)
         || string.IsNullOrWhiteSpace(e.LastName)).ToList();

Vor EF Core 6.0 wurde dieser Code in SQL Server folgendermaßen übersetzt:

SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR (LTRIM(RTRIM([u].[FirstName])) = N'')) OR ([u].[LastName] IS NULL OR (LTRIM(RTRIM([u].[LastName])) = N''))

In EF Core 6.0 wurde diese Übersetzung verbessert:

SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR ([u].[FirstName] = N'')) OR ([u].[LastName] IS NULL OR ([u].[LastName] = N''))

Definieren der Abfrage für den In-Memory-Anbieter

GitHub-Issue: 24600.

Eine neue ToInMemoryQuery-Methode kann verwendet werden, um eine definierende Abfrage für die In-Memory-Datenbank für einen bestimmten Entitätstyp zu schreiben. Dies ist besonders nützlich, um das Äquivalent von Sichten für die In-Memory-Datenbank zu erstellen, insbesondere wenn diese Sichten schlüssellose Entitätstypen zurückgeben. Stellen Sie sich beispielsweise eine Kundendatenbank für Kunden im Vereinigten Königreich vor. Jeder Kunde hat eine Adresse:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public int Id { get; set; }
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

Stellen Sie sich nun vor, wir möchten einen Überblick über diese Daten, die darstellt, wie viele Kunden es in jedem Postleitzahlbereich gibt. Sie können einen schlüssellosen Entitätstyp erstellen, um diese Zahlen zu repräsentieren:

public class CustomerDensity
{
    public string Postcode { get; set; }
    public int CustomerCount { get; set; }
}

Definieren Sie außerdem eine DbSet-Eigenschaft für den DbContext sowie Sätze für andere Entitätstypen der obersten Ebene:

public DbSet<Customer> Customers { get; set; }
public DbSet<CustomerDensity> CustomerDensities { get; set; }

Anschließend können Sie in OnModelCreating eine LINQ-Abfrage schreiben, die die Daten definiert, die für CustomerDensities zurückgegeben werden sollen:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<CustomerDensity>()
        .HasNoKey()
        .ToInMemoryQuery(
            () => Customers
                .GroupBy(c => c.Address.Postcode.Substring(0, 3))
                .Select(
                    g =>
                        new CustomerDensity
                        {
                            Postcode = g.Key,
                            CustomerCount = g.Count()
                        }));
}

Dies kann dann genau wie jede andere DbSet-Eigenschaft abgefragt werden:

var results = context.CustomerDensities.ToList();

Übersetzen von Teilzeichenfolgen mit einem einzelnen Parameter

GitHub-Issue: 20173. Dieses Feature wurde von @stevendarby beigetragen. Danke vielmals!

EF Core 6.0 übersetzt jetzt die Verwendungen von string.Substring mit einem einzelnen Argument. Beispiel:

var result = context.Customers
    .Select(a => new { Name = a.Name.Substring(3) })
    .FirstOrDefault(a => a.Name == "hur");

Wenn Sie SQL Server verwenden, wird dieser Code in den folgenden SQL-Code übersetzt:

SELECT TOP(1) SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) AS [Name]
FROM [Customers] AS [c]
WHERE SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) = N'hur'

Aufgeteilte Abfragen für andere Sammlungen als Navigationssammlungen

GitHub-Issue: 21234.

EF Core unterstützt das Aufteilen einer einzelnen LINQ-Abfrage in mehrere SQL Abfragen. In EF Core 6.0 wurde diese Unterstützung um Fälle erweitert, in denen andere Sammlungen als Navigationssammlungen in der Abfrageprojektion enthalten sind.

Im Folgenden finden Sie Beispielabfragen, die die Übersetzung in SQL Server in eine einzelne Abfrage oder mehrere Abfragen zeigen.

Beispiel 1:

LINQ-Abfrage:

context.Customers
    .Select(
        c => new
        {
            c,
            Orders = c.Orders
                .Where(o => o.Id > 1)
        })
    .ToList();

Einzelne SQL-Abfrage:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Mehrere SQL-Abfragen:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Beispiel 2:

LINQ-Abfrage:

context.Customers
    .Select(
        c => new
        {
            c,
            OrderDates = c.Orders
                .Where(o => o.Id > 1)
                .Select(o => o.OrderDate)
        })
    .ToList();

Einzelne SQL-Abfrage:

SELECT [c].[Id], [t].[OrderDate], [t].[Id]
FROM [Customers] AS [c]
  LEFT JOIN (
  SELECT [o].[OrderDate], [o].[Id], [o].[CustomerId]
  FROM [Order] AS [o]
  WHERE [o].[Id] > 1
  ) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Mehrere SQL-Abfragen:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Beispiel 3:

LINQ-Abfrage:

context.Customers
    .Select(
        c => new
        {
            c,
            OrderDates = c.Orders
                .Where(o => o.Id > 1)
                .Select(o => o.OrderDate)
                .Distinct()
        })
    .ToList();

Einzelne SQL-Abfrage:

SELECT [c].[Id], [t].[OrderDate]
FROM [Customers] AS [c]
  OUTER APPLY (
  SELECT DISTINCT [o].[OrderDate]
  FROM [Order] AS [o]
  WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
  ) AS [t]
ORDER BY [c].[Id]

Mehrere SQL-Abfragen:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
  CROSS APPLY (
  SELECT DISTINCT [o].[OrderDate]
  FROM [Order] AS [o]
  WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
  ) AS [t]
ORDER BY [c].[Id]

Entfernen der letzten ORDER BY-Klausel beim Beitritt zur Sammlung

GitHub-Issue: 19828.

Beim Laden verwandter 1:n-Entitäten fügt EF Core ORDER BY-Klauseln hinzu, um sicherzustellen, dass alle verknüpften Entitäten für eine bestimmte Entität zusammengefügt werden. Die letzte ORDER BY-Klausel ist jedoch nicht erforderlich, damit EF die erforderlichen Gruppierungen generiert und eine Auswirkung auf die Leistung erzielen kann. Daher wird für EF Core 6.0 diese Klausel entfernt.

Angenommen, Sie haben die folgende Abfrage vorliegen:

context.Customers
    .Select(
        e => new
        {
            e.Id,
            FirstOrder = e.Orders.Where(i => i.Id == 1).ToList()
        })
    .ToList();

Wenn EF Core 5.0 in SQL Server ausgeführt wird, wird diese Abfrage wie folgt übersetzt:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id], [t].[Id]

Mit EF Core 6.0 wird sie stattdessen so übersetzt:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Tag-Abfragen mit Dateiname und Zeilennummer

GitHub-Issue: 14176. Dieses Feature wurde von @michalczerwinski beigetragen. Danke vielmals!

Abfragetags ermöglichen das Hinzufügen eines strukturellen Tags zu einer LINQ-Abfrage, damit es dann in das generierte SQL eingefügt werden kann. In EF Core 6.0 kann dieser Vorgang verwendet werden, um Abfragen mit dem Dateinamen und der Zeilennummer des LINQ-Codes zu markieren. Beispiel:

var results1 = context
    .Customers
    .TagWithCallSite()
    .Where(c => c.Name.StartsWith("A"))
    .ToList();

Der folgende SQL-Code wird generiert, wenn SQL Server verwendet wird:

-- file: C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\TagWithFileAndLineSample.cs:21

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE [c].[Name] IS NOT NULL AND ([c].[Name] LIKE N'A%')

Änderungen an der eigenen optionalen abhängigen Behandlung

GitHub-Issue: 24558.

Es wird schwierig zu wissen, ob eine optionale abhängige Entität vorhanden ist oder nicht, wenn sie eine Tabelle mit ihrer Prinzipalentität teilt. Dies liegt daran, dass eine Zeile in der Tabelle für die abhängige Entität vorhanden ist, da die Prinzipalentität sie benötigt, unabhängig davon, ob die abhängige Entität vorhanden ist oder nicht. Sie müssen sicherstellen, dass die abhängige Entität mindestens über eine erforderliche Eigenschaft verfügt, um diese Situation unmissverständlich behandeln zu können. Da eine erforderliche Eigenschaft nicht NULL sein darf, bedeutet dies, dass die abhängige Entität nicht vorhanden ist, wenn der Wert in der Spalte für diese Eigenschaft NULL ist.

Stellen Sie sich beispielsweise eine Customer-Klasse vor, in der jede/r Kunde/-in über eine eigene Address verfügt:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }

    [Required]
    public string Postcode { get; set; }
}

Die Adresse ist optional, was bedeutet, dass es gültig ist, einen Kunden ohne Adresse zu speichern:

context.Customers1.Add(
    new()
    {
        Name = "Foul Ole Ron"
    });

Wenn ein Kunde oder eine Kundin jedoch über eine Adresse verfügt, muss diese Adresse mindestens eine Postleitzahl ohne NULL haben:

context.Customers1.Add(
    new()
    {
        Name = "Havelock Vetinari",
        Address = new()
        {
            Postcode = "AN1 1PL",
        }
    });

Dies wird sichergestellt, indem die Postcode-Eigenschaft als Required markiert wird.

Wenn Kunden bzw. Kundinnen abgefragt werden und die Postleitzahlspalte NULL ist, bedeutet dies, dass Kunden und Kundinnen keine Adresse besitzen, und so wird die Navigationseigenschaft Customer.Address auf NULL belassen. Beispiel: Iterieren der Kunden/-innen und Überprüfen, ob die Adresse NULL ist:

foreach (var customer in context.Customers1)
{
    Console.Write(customer.Name);

    if (customer.Address == null)
    {
        Console.WriteLine(" has no address.");
    }
    else
    {
        Console.WriteLine($" has postcode {customer.Address.Postcode}.");
    }
}

Dabei werden die folgenden Ergebnisse generiert:

Foul Ole Ron has no address.
Havelock Vetinari has postcode AN1 1PL.

Nehmen Sie stattdessen an, dass keine Eigenschaft der Adresse erforderlich ist:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

Jetzt ist es möglich, sowohl eine/n Kunden/-in ohne Adresse als auch jene mit einer Adresse zu speichern, bei der alle Adresseigenschaften NULL sind:

context.Customers2.Add(
    new()
    {
        Name = "Foul Ole Ron"
    });

context.Customers2.Add(
    new()
    {
        Name = "Havelock Vetinari",
        Address = new()
    });

In der Datenbank sind diese beiden Fälle jedoch nicht voneinander zu unterscheiden, wie Sie durch direktes Abfragen der Datenbankspalten sehen können:

Id  Name               House   Street  City    Postcode
1   Foul Ole Ron       NULL    NULL    NULL    NULL
2   Havelock Vetinari  NULL    NULL    NULL    NULL

Aus diesem Grund warnt EF Core 6.0 Sie beim Speichern eines optionalen abhängigen Werts, bei dem alle Eigenschaften NULL sind. Beispiel:

warn: 9/27/2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) The entity of type 'Address' with primary key values {CustomerId: -2147482646} is an optional dependent using table sharing. The entity does not have any property with a non-default value to identify whether the entity exists. This means that when it is queried no object instance will be created instead of an instance with all properties set to default values. Any nested dependents will also be lost. Either don't save any instance with only default values or mark the incoming navigation as required in the model. (warn: 9/27/2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) Die Entität des Typs 'Address' mit den primären Schlüsselwerten {CustomerId: -2147482646} ist eine optionale Abhängigkeit, die Tabellenfreigabe nutzt. Die Entität besitzt keine Eigenschaft mit einem nicht-standardmäßigen Wert, um zu identifizieren, ob die Entität vorhanden ist. Das bedeutet, dass bei Abfrage keine Objektinstanz anstelle einer Instanz mit allen Eigenschaften, die auf Standardwerten festgelegt sind, erstellt werden. Alle geschachtelten abhängigen Objekte gegen ebenfalls verloren.)

Dieser Schritt wird noch komplizierter, wenn die optionale abhängige Entität selbst als Prinzipal für eine optionale abhängige Entität fungiert, also derselben Tabelle zugeordnet ist. Anstatt nur eine Warnung auszugeben, verhindert EF Core 6.0 gerechtfertigte Fälle von geschachtelten optionalen abhängigen Objekten. Betrachten Sie beispielsweise das folgende Modell, bei dem ContactInfo sich im Besitz von Customer befindet und Address sich wiederum im Besitz von ContactInfo befindet:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

public class ContactInfo
{
    public string Phone { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

Wenn ContactInfo.Phone jetzt NULL ist, erstellt EF Core keine Instanz von Address, wenn die Beziehung optional ist, obwohl die Adresse selbst möglicherweise Daten enthält. Für diese Art von Modell löst EF Core 6.0 die folgende Ausnahme aus:

System.InvalidOperationException: Der Entitätstyp „ContactInfo“ ist ein optionales abhängiges Objekt, das die Tabellenfreigabe verwendet und andere abhängige Objekte ohne erforderliche nicht freigegebene Eigenschaften enthält, um zu ermitteln, ob die Entität vorhanden ist. Wenn alle Nullwerte zulassenden Eigenschaften einen NULL-Wert in der Datenbank enthalten, wird in der Abfrage keine Objektinstanz erstellt, wodurch die Werte geschachtelter abhängiger Objekte verloren gehen. Fügen Sie eine erforderliche Eigenschaft hinzu, um Instanzen mit NULL-Werten für andere Eigenschaften zu erstellen, oder markieren Sie die eingehende Navigation als erforderlich, damit immer eine Instanz erstellt wird.

Unter dem Strich soll vermieden werden, dass eine optionale abhängige Entität alle Nullable-Eigenschaftswerte enthalten kann und eine Tabelle mit ihrer Prinzipalentität teilt. Es gibt drei Möglichkeiten, diese Situation zu vermeiden:

  1. Legen Sie fest, dass die abhängige Entität erforderlich ist. Dies bedeutet, dass die abhängige Entität immer einen Wert hat, nachdem sie abgefragt wurde, auch wenn alle ihre Eigenschaften NULL sind.
  2. Stellen Sie sicher, dass die abhängige Enthält wie oben beschrieben mindestens eine erforderliche Eigenschaft enthält.
  3. Speichern Sie optional abhängige Entitäten in ihrer eigenen Tabelle, anstatt die Tabelle mit der Prinzipalentität zu teilen.

Eine abhängig Entität kann mithilfe des Attributs Required in der Navigation als erforderlich eingestuft werden:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }

    [Required]
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

Alternativ ist sie bei der Angabe in OnModelCreating erforderlich:

modelBuilder.Entity<WithRequiredNavigation.Customer>(
    b =>
        {
            b.OwnsOne(e => e.Address);
            b.Navigation(e => e.Address).IsRequired();
        });

Abhängige Entitäten können in einer anderen Tabelle gespeichert werden, indem die zu verwendenden Tabellen in OnModelCreating angegeben werden:

modelBuilder
    .Entity<WithDifferentTable.Customer>(
        b =>
            {
                b.ToTable("Customers");
                b.OwnsOne(
                    e => e.Address,
                    b => b.ToTable("CustomerAddresses"));
            });

Weitere Beispiele für optionale abhängige Entitäten, einschließlich Fällen mit geschachtelten optionalen abhängigen Entitäten finden Sie im OptionalDependentsSample auf GitHub.

Neue Zuordnungsattribute

EF Core 6.0 enthält mehrere neue Attribute, die auf Code angewendet werden können, um die Zuordnung zur Datenbank zu ändern.

UnicodeAttribute

GitHub-Issue: 19794. Dieses Feature wurde von @RaymondHuy beigetragen. Danke vielmals!

Ab EF Core 6.0 können Zeichenfolgeneigenschaften einer Spalte zugewiesen werden, die kein Unicode-Format verwendet, indem das Zuordnungsattribut ohne direkte Angabe des Datenbanktyps verwendet wird. Nehmen Sie beispielsweise ein, dass ein Book-Entitätstyp mit einer Eigenschaft für die Book im Format „ISBN 978-3-16-148410-0“ vorliegt:

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }

    [Unicode(false)]
    [MaxLength(22)]
    public string Isbn { get; set; }
}

Da ISBNs keine Unicode-Zeichen enthalten dürfen, bewirkt das Attribut Unicode, dass kein Unicode-Zeichenfolgentyp verwendet wird. Außerdem wird MaxLength verwendet, um die Größe der Datenbankspalte einzuschränken. In SQL Server wäre das Ergebnis beispielsweise eine Datenbankspalte des Typs varchar(22):

CREATE TABLE [Book] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NULL,
    [Isbn] varchar(22) NULL,
    CONSTRAINT [PK_Book] PRIMARY KEY ([Id]));

Hinweis

EF Core ordnet Zeichenfolgeneigenschaften standardmäßig Unicode-Spalten zu. UnicodeAttribute wird ignoriert, wenn das Datenbanksystem andere Unicode-Typen unterstützt.

PrecisionAttribute

GitHub-Issue: 17914. Dieses Feature wurde von @RaymondHuy beigetragen. Danke vielmals!

Die Genauigkeit und die Dezimalstellen einer Datenbankspalte können jetzt mithilfe von Zuordnungsattributen konfiguriert werden, ohne den Datenbanktyp direkt anzugeben. Nehmen Sie beispielsweise an, dass ein Product-Entitätstyp mit der Dezimaleigenschaft Price vorliegt:

public class Product
{
    public int Id { get; set; }

    [Precision(precision: 10, scale: 2)]
    public decimal Price { get; set; }
}

EF Core ordnet diese Eigenschaft einer Datenbankspalte mit einer Genauigkeit von 10 und einem Dezimalstellenwert von 2 zu. Beispielsweise unter SQL Server:

CREATE TABLE [Product] (
    [Id] int NOT NULL IDENTITY,
    [Price] decimal(10,2) NOT NULL,
    CONSTRAINT [PK_Product] PRIMARY KEY ([Id]));

EntityTypeConfigurationAttribute

GitHub-Issue: 23163. Dieses Feature wurde von @KaloyanIT beigetragen. Danke vielmals!

IEntityTypeConfigurationTEntity>-Instanzen lassen jetzt zu, dass die >-Konfiguration für jeden Entitätstyp in einer eigenen Konfigurationsklasse enthalten ist. Beispiel:

public class BookConfiguration : IEntityTypeConfiguration<Book>
{
    public void Configure(EntityTypeBuilder<Book> builder)
    {
        builder
            .Property(e => e.Isbn)
            .IsUnicode(false)
            .HasMaxLength(22);
    }
}

Normalerweise muss diese Konfigurationsklasse instanziiert und über DbContext.OnModelCreating aufgerufen werden. Beispiele:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    new BookConfiguration().Configure(modelBuilder.Entity<Book>());
}

Ab EF Core 6.0 kann EntityTypeConfigurationAttribute dem Entitätstyp hinzugefügt werden, sodass EF Core die entsprechende Konfiguration suchen und verwenden kann. Beispiele:

[EntityTypeConfiguration(typeof(BookConfiguration))]
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Isbn { get; set; }
}

Dieses Attribut bedeutet, dass EF Core die angegebene IEntityTypeConfiguration-Implementierung verwendet, wenn der Entitätstyp Book in einem Modell enthalten ist. Der Entitätstyp wird dem Modell mit einem der gängigen Verfahren hinzugefügt, beispielsweise durch das Erstellen einer DbSetTEntity>-Eigenschaft für den Entitätstyp:

public class BooksContext : DbContext
{
    public DbSet<Book> Books { get; set; }

    //...

Alternativ dazu können Sie ihn in OnModelCreating registrieren:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Book>();
}

Hinweis

EntityTypeConfigurationAttribute-Typen werden in einer Assembly nicht automatisch erkannt. Entitätstypen müssen dem Modell hinzugefügt werden, bevor das Attribut für diesen Entitätstyp erkannt wird.

Verbesserungen beim Erstellen von Modellen

Zusätzlich zu neuen Zuordnungsattributen enthält EF Core 6.0 mehrere weitere Verbesserungen am Modellprozess.

Unterstützung für SQL Server-Sparsespalten

GitHub-Issue: 8023.

SQL Server-Sparsespalten sind normale Spalten, die für das Speichern von NULL-Werten optimiert wurden. Das kann bei Verwendung der TPH-Vererbungszuordnung nützlich sein, bei denen Eigenschaften von selten verwendeten Untertypen in den meisten Zeilen der Tabelle zu NULL-Spaltenwerten führen. Nehmen Sie beispielsweise an, dass die Klasse ForumModerator vorliegt, die eine Erweiterung von ForumUser darstellt:

public class ForumUser
{
    public int Id { get; set; }
    public string Username { get; set; }
}

public class ForumModerator : ForumUser
{
    public string ForumName { get; set; }
}

Selbst bei Millionen Benutzern gibt es nur wenige Moderatoren. Deshalb ist es in diesem Fall sinnvoll, ForumName als Sparsespalte zuzuordnen. Diese kann jetzt über IsSparse in IsSparse konfiguriert werden. Beispiele:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<ForumModerator>()
        .Property(e => e.ForumName)
        .IsSparse();
}

EF Core-Migrationen kennzeichnen diese Spalte dann als Sparsespalte. Beispiele:

CREATE TABLE [ForumUser] (
    [Id] int NOT NULL IDENTITY,
    [Username] nvarchar(max) NULL,
    [Discriminator] nvarchar(max) NOT NULL,
    [ForumName] nvarchar(max) SPARSE NULL,
    CONSTRAINT [PK_ForumUser] PRIMARY KEY ([Id]));

Hinweis

Für Sparsespalten gibt es jedoch Einschränkungen. Lesen Sie die Dokumentation zu Sparsespalten in SQL Server, um sich zu vergewissern, dass diese für Ihr Szenario geeignet sind.

Änderungen an der HasConversion-API

GitHub-Issue: 25468.

Vor EF Core 6.0 haben die generischen Überladungen der HasConversion-Methoden den generischen Parameter verwendet, um HasConversion. Betrachten Sie beispielsweise eine Currency-Enumeration:

public enum Currency
{
    UsDollars,
    PoundsSterling,
    Euros
}

EF Core kann so konfiguriert werden, um Werte dieser Enumeration mit HasConversion<string> als die Zeichenfolgen „UsDollars“, „PoundsStirling“ und „Euros“ zu speichern. Beispiel:

modelBuilder.Entity<TestEntity1>()
    .Property(e => e.Currency)
    .HasConversion<string>();

Ab EF Core 6.0 kann der generische Typ stattdessen einen Wertkonvertertyp angeben. Dies kann einer der integrierten Wertkonverter sein. So speichern sie beispielsweise die Enumerationswerte als 16-Bit-Zahlen in der Datenbank:

modelBuilder.Entity<TestEntity2>()
    .Property(e => e.Currency)
    .HasConversion<EnumToNumberConverter<Currency, short>>();

Alternativ kann es auch ein benutzerdefinierter Wertkonvertertyp sein. Denken Sie beispielsweise an einen Konverter, der die Enumerationswerte als Währungssymbole speichert:

public class CurrencyToSymbolConverter : ValueConverter<Currency, string>
{
    public CurrencyToSymbolConverter()
        : base(
            v => v == Currency.PoundsSterling ? "£" : v == Currency.Euros ? "€" : "$",
            v => v == "£" ? Currency.PoundsSterling : v == "€" ? Currency.Euros : Currency.UsDollars)
    {
    }
}

Dies kann jetzt über die generische HasConversion-Methode konfiguriert werden:

modelBuilder.Entity<TestEntity3>()
    .Property(e => e.Currency)
    .HasConversion<CurrencyToSymbolConverter>();

Einfachere Konfiguration für n:n-Beziehungen

GitHub-Issue: 21535.

Eindeutige m:n-Beziehungen zwischen zwei Entitätstypen werden standardmäßig gefunden. Bei Bedarf können die Navigationen explizit angegeben werden. Beispiel:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats);

In beiden Fällen erstellt EF Core eine freigegebene Entität, die auf Dictionary<string, object> basiert, um als Joinentität zwischen den beiden Typen zu fungieren. Ab EF Core 6.0 kann UsingEntity zur Konfiguration hinzugefügt werden, um nur diesen Typ zu ändern, ohne dass zusätzliche Konfigurationen erforderlich sind. Beispiel:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>();

Darüber hinaus kann der Joinentitätstyp zusätzlich konfiguriert werden, ohne dass die linke und rechte Beziehung explizit angegeben werden muss. Beispiel:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>(
        e => e.HasKey(e => new { e.CatsId, e.HumansId }));

Schließlich kann die vollständige Konfiguration bereitgestellt werden. Beispiel:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>(
        e => e.HasOne<Human>().WithMany().HasForeignKey(e => e.CatsId),
        e => e.HasOne<Cat>().WithMany().HasForeignKey(e => e.HumansId),
        e => e.HasKey(e => new { e.CatsId, e.HumansId }));

Zulassen, dass Wertkonverter NULL-Werte konvertieren

GitHub-Issue: 13850.

Wichtig

Aufgrund der unten beschriebenen Probleme wurden die Konstruktoren für ValueConverter, die die Konvertierung von NULL-Werte zulassen, für das EF Core 6.0-Release mit [EntityFrameworkInternal] markiert. Wenn Sie diese Konstruktoren verwenden, erhalten Sie jetzt eine Buildwarnung.

Wertkonverter lassen im Allgemeinen die Konvertierung von NULL in einen anderen Wert nicht zu. Dies liegt daran, dass der gleiche Wertkonverter sowohl für Nullable-Typen als auch für Nicht-Nullable-Typen verwendet werden kann. Dies ist sehr nützlich für PK/FK-Kombinationen, bei denen der FK oft NULL-Werte zu übergibt und der PK nicht.

Ab EF Core 6.0 kann ein Wertkonverter erstellt werden, der NULL-Werte konvertiert. Bei der Überprüfung hat sich dieses Feature in der Praxis jedoch als sehr problematisch herausgestellt, weil es zahlreiche Fallstricke aufwies. Beispiel:

Dies sind keine trivialen Probleme, und für die Abfrageprobleme sind sie nicht einfach zu erkennen. Daher haben wir dieses Feature für EF Core 6.0 als intern markiert. Sie können es weiterhin verwenden, aber Sie erhalten eine Compilerwarnung. Die Warnung kann mit #pragma warning disable EF1001 deaktiviert werden.

Die Konvertierung von NULL-Werten kann beispielsweise nützlich sein, wenn die Datenbank NULL-Werte enthält, der Entitätstyp jedoch einen anderen Standardwert für die Eigenschaft verwenden möchte. Betrachten Sie beispielsweise eine Enumeration, deren Standardwert „Unknown“ (Unbekannt) ist:

public enum Breed
{
    Unknown,
    Burmese,
    Tonkinese
}

Die Datenbank kann jedoch NULL-Werte enthalten, wenn die Hunderasse unbekannt ist. In EF Core 6.0 kann ein Wertkonverter verwendet werden, um dies zu berücksichtigen:

    public class BreedConverter : ValueConverter<Breed, string>
    {
#pragma warning disable EF1001
        public BreedConverter()
            : base(
                v => v == Breed.Unknown ? null : v.ToString(),
                v => v == null ? Breed.Unknown : Enum.Parse<Breed>(v),
                convertsNulls: true)
        {
        }
#pragma warning restore EF1001
    }

Für Katzen, deren Rasse nicht bekannt („unknown“) ist, wird die Breed-Spalte auf NULL in der Datenbank festgelegt. Beispiel:

context.AddRange(
    new Cat { Name = "Mac", Breed = Breed.Unknown },
    new Cat { Name = "Clippy", Breed = Breed.Burmese },
    new Cat { Name = "Sid", Breed = Breed.Tonkinese });

context.SaveChanges();

Dadurch werden die folgenden Einfügeanweisungen in SQL Server generiert:

info: 9/27/2021 19:43:55.966 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (16ms) [Parameters=[@p0=NULL (Size = 4000), @p1='Mac' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Burmese' (Size = 4000), @p1='Clippy' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Tonkinese' (Size = 4000), @p1='Sid' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

Verbesserungen der DbContext-Factory

AddDbContextFactory registriert auch DbContext direkt.

GitHub-Issue: 25164.

Manchmal ist es nützlich, einen DbContext-Typ und eine Factory für Kontexte dieses Typs zu verwenden, die beide im DI-Container (Dependency Injection, DI) für Anwendungen registriert sind. Dadurch kann beispielsweise eine bereichsbezogene Instanz von DbContext aus dem Anforderungsbereich aufgelöst werden, während die Factory verwendet werden kann, um bei Bedarf mehrere unabhängige Instanzen zu erstellen.

AddDbContextFactory registriert jetzt auch den DbContext-Typ als bereichsbezogenen Dienst, um diesen Schritt zu unterstützen. Sehen Sie sich diese Registrierung beispielsweise im DI-Container der Anwendung an:

var container = services
    .AddDbContextFactory<SomeDbContext>(
        builder => builder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample"))
    .BuildServiceProvider();

Bei dieser Registrierung kann die Factory wie in früheren Versionen vom DI-Stammcontainer aus aufgelöst werden:

var factory = container.GetService<IDbContextFactory<SomeDbContext>>();
using (var context = factory.CreateDbContext())
{
    // Contexts obtained from the factory must be explicitly disposed
}

Beachten Sie, dass von der Factory erstellte Kontextinstanzen explizit verworfen werden müssen.

Darüber hinaus kann eine DbContext-Instanz direkt aus einem Containerbereich aufgelöst werden:

using (var scope = container.CreateScope())
{
    var context = scope.ServiceProvider.GetService<SomeDbContext>();
    // Context is disposed when the scope is disposed
}

In diesem Fall wird die Kontextinstanz verworfen, wenn der Containerbereich verworfen wird. Der Kontext sollte nicht explizit verworfen werden.

Im Allgemeinen bedeutet dies, dass der DbContext der Factory in andere DI-Typen eingefügt werden kann. Zum Beispiel:

private class MyController2
{
    private readonly IDbContextFactory<SomeDbContext> _contextFactory;

    public MyController2(IDbContextFactory<SomeDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public void DoSomething()
    {
        using var context1 = _contextFactory.CreateDbContext();
        using var context2 = _contextFactory.CreateDbContext();

        var results1 = context1.Blogs.ToList();
        var results2 = context2.Blogs.ToList();
        
        // Contexts obtained from the factory must be explicitly disposed
    }
}

Oder:

private class MyController1
{
    private readonly SomeDbContext _context;

    public MyController1(SomeDbContext context)
    {
        _context = context;
    }

    public void DoSomething()
    {
        var results = _context.Blogs.ToList();

        // Injected context is disposed when the request scope is disposed
    }
}

DbContextFactory ignoriert den parameterlosen DbContext-Konstruktor

GitHub-Issue: 24124

EF Core 6.0 ermöglicht jetzt sowohl einen parameterlosen DbContext-Konstruktor als auch einen Konstruktor, der erfordert, dass DbContextOptions für denselben Kontexttyp verwendet wird, wenn die Factory über AddDbContextFactory registriert wurde. Beispielsweise enthält der in den obigen Beispielen verwendete Kontext beide Konstruktoren:

public class SomeDbContext : DbContext
{
    public SomeDbContext()
    {
    }

    public SomeDbContext(DbContextOptions<SomeDbContext> options)
        : base(options)
    {
    }
    
    public DbSet<Blog> Blogs { get; set; }
}

Das DbContext-Pooling kann ohne Abhängigkeitsinjektion verwendet werden.

GitHub-Issue: 24137

Der PooledDbContextFactory-Typ wurde öffentlich gemacht, sodass er als eigenständiger Pool für DbContext-Instanzen verwendet werden kann, ohne dass Ihre Anwendung über einen Abhängigkeitsinjektionscontainer verfügen muss. Der Pool wird mit einer Instanz von DbContextOptions erstellt, die zum Erstellen von Kontextinstanzen verwendet wird:

var options = new DbContextOptionsBuilder<SomeDbContext>()
    .EnableSensitiveDataLogging()
    .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample")
    .Options;

var factory = new PooledDbContextFactory<SomeDbContext>(options);

Die Factory kann dann verwendet werden, um Instanzen zu erstellen und zu poolen. Beispiel:

for (var i = 0; i < 2; i++)
{
    using var context1 = factory.CreateDbContext();
    Console.WriteLine($"Created DbContext with ID {context1.ContextId}");

    using var context2 = factory.CreateDbContext();
    Console.WriteLine($"Created DbContext with ID {context2.ContextId}");
}

Instanzen werden an den Pool zurückgegeben, wenn sie verworfen werden.

Sonstige Verbesserungen

Für EF Core sind noch weitere Verbesserungen vorhanden, die in den Bereichen oben nicht abgedeckt werden.

Verwenden von [ColumnAttribute.Order] beim Erstellen von Tabellen

GitHub-Issue: 10059.

Die Order-Eigenschaft von ColumnAttribute kann jetzt verwendet werden, um Spalten beim Erstellen einer Tabelle mit Migrationen zu sortieren. Betrachten Sie beispielsweise das folgende Modell:

public class EntityBase
{
    public int Id { get; set; }
    public DateTime UpdatedOn { get; set; }
    public DateTime CreatedOn { get; set; }
}

public class PersonBase : EntityBase
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Employee : PersonBase
{
    public string Department { get; set; }
    public decimal AnnualSalary { get; set; }
    public Address Address { get; set; }
}

[Owned]
public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }

    [Required]
    public string Postcode { get; set; }
}

Standardmäßig sortiert EF Core zuerst nach Primärschlüsselspalten, dann nach Eigenschaften des Entitätstyps und der zugehörigen Typen und schließlich nach Eigenschaften von Basistypen. Beispielsweise wird in SQL Server die folgende Tabelle erstellt:

CREATE TABLE [EmployeesWithoutOrdering] (
    [Id] int NOT NULL IDENTITY,
    [Department] nvarchar(max) NULL,
    [AnnualSalary] decimal(18,2) NOT NULL,
    [Address_House] nvarchar(max) NULL,
    [Address_Street] nvarchar(max) NULL,
    [Address_City] nvarchar(max) NULL,
    [Address_Postcode] nvarchar(max) NULL,
    [UpdatedOn] datetime2 NOT NULL,
    [CreatedOn] datetime2 NOT NULL,
    [FirstName] nvarchar(max) NULL,
    [LastName] nvarchar(max) NULL,
    CONSTRAINT [PK_EmployeesWithoutOrdering] PRIMARY KEY ([Id]));

In EF Core 6.0 kann ColumnAttribute verwendet werden, um eine andere Spaltenreihenfolge anzugeben. Beispiel:

public class EntityBase
{
    [Column(Order = 1)]
    public int Id { get; set; }

    [Column(Order = 98)]
    public DateTime UpdatedOn { get; set; }

    [Column(Order = 99)]
    public DateTime CreatedOn { get; set; }
}

public class PersonBase : EntityBase
{
    [Column(Order = 2)]
    public string FirstName { get; set; }

    [Column(Order = 3)]
    public string LastName { get; set; }
}

public class Employee : PersonBase
{
    [Column(Order = 20)]
    public string Department { get; set; }

    [Column(Order = 21)]
    public decimal AnnualSalary { get; set; }

    public Address Address { get; set; }
}

[Owned]
public class Address
{
    [Column("House", Order = 10)]
    public string House { get; set; }

    [Column("Street", Order = 11)]
    public string Street { get; set; }

    [Column("City", Order = 12)]
    public string City { get; set; }

    [Required]
    [Column("Postcode", Order = 13)]
    public string Postcode { get; set; }
}

In SQL Server wird jetzt folgende Tabelle generiert:

CREATE TABLE [EmployeesWithOrdering] (
    [Id] int NOT NULL IDENTITY,
    [FirstName] nvarchar(max) NULL,
    [LastName] nvarchar(max) NULL,
    [House] nvarchar(max) NULL,
    [Street] nvarchar(max) NULL,
    [City] nvarchar(max) NULL,
    [Postcode] nvarchar(max) NULL,
    [Department] nvarchar(max) NULL,
    [AnnualSalary] decimal(18,2) NOT NULL,
    [UpdatedOn] datetime2 NOT NULL,
    [CreatedOn] datetime2 NOT NULL,
    CONSTRAINT [PK_EmployeesWithOrdering] PRIMARY KEY ([Id]));

Dadurch werden die Spalten FistName und LastName nach oben verschoben, obwohl sie in einem Basistyp definiert sind. Beachten Sie, dass die Werte der Spaltenreihenfolge Lücken aufweisen können. So können Bereiche verwendet werden, um Spalten immer am Ende zu platzieren, selbst wenn sie von mehreren abgeleiteten Typen verwendet werden.

In diesem Beispiel wird auch gezeigt, wie dasselbe ColumnAttribute verwendet werden kann, um sowohl den Spaltennamen als auch die Reihenfolge anzugeben.

Die Spaltenreihenfolge kann auch mithilfe der ModelBuilder-API in OnModelCreating konfiguriert werden. Beispiel:

modelBuilder.Entity<UsingModelBuilder.Employee>(
    entityBuilder =>
    {
        entityBuilder.Property(e => e.Id).HasColumnOrder(1);
        entityBuilder.Property(e => e.FirstName).HasColumnOrder(2);
        entityBuilder.Property(e => e.LastName).HasColumnOrder(3);

        entityBuilder.OwnsOne(
            e => e.Address,
            ownedBuilder =>
            {
                ownedBuilder.Property(e => e.House).HasColumnName("House").HasColumnOrder(4);
                ownedBuilder.Property(e => e.Street).HasColumnName("Street").HasColumnOrder(5);
                ownedBuilder.Property(e => e.City).HasColumnName("City").HasColumnOrder(6);
                ownedBuilder.Property(e => e.Postcode).HasColumnName("Postcode").HasColumnOrder(7).IsRequired();
            });

        entityBuilder.Property(e => e.Department).HasColumnOrder(8);
        entityBuilder.Property(e => e.AnnualSalary).HasColumnOrder(9);
        entityBuilder.Property(e => e.UpdatedOn).HasColumnOrder(10);
        entityBuilder.Property(e => e.CreatedOn).HasColumnOrder(11);
    });

Die Sortierung in Model Builder mit HasColumnOrder hat Vorrang vor jeder durch ColumnAttribute angegebenen Reihenfolge. Daher kann mit HasColumnOrder die durch Attribute festgelegte Reihenfolge überschrieben werden. Zudem lassen sich Konflikte lösen, bei denen Attribute für verschiedene Eigenschaften dieselbe Reihenfolgennummer angeben.

Wichtig

Beachten Sie, dass die meisten Datenbanken das Sortieren von Spalten nur beim Erstellen der Tabelle unterstützen. Dies bedeutet, dass das Attribut für die Spaltenreihenfolge nicht verwendet werden kann, um Spalten in einer vorhandenen Tabelle neu zu sortieren. Eine wichtige Ausnahme ist SQLite. Hierbei wird bei Migrationen die gesamte Tabelle mit neuen Spaltenreihenfolgen neu erstellt.

Minimale EF Core-API

GitHub-Issue: 25192.

.NET Core 6.0 enthält aktualisierte Vorlagen mit vereinfachten „minimalen APIs“, die einen Großteil des Codebausteins entfernen, der normalerweise in .NET-Anwendungen benötigt wird.

EF Core 6.0 enthält eine neue Erweiterungsmethode, die einen DbContext-Typ registriert und die Konfiguration für einen Datenbankanbieter in einer einzigen Zeile bereitstellt. Beispiel:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSqlite<MyDbContext>("Data Source=mydatabase.db");
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSqlServer<MyDbContext>(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase");
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCosmos<MyDbContext>(
    "https://localhost:8081",
    "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==");

Diese sind genau gleichbedeutend mit den Folgenden:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseSqlite("Data Source=mydatabase.db"));
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase"));
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="));

Hinweis

Die minimalen EF Core-APIs unterstützen nur die sehr einfache Registrierung und Konfiguration eines DbContext-Typs und -Anbieters. Verwenden Sie beispielsweise AddDbContext, AddDbContextPool und AddDbContextFactory, um auf alle Registrierungs- und Konfigurationstypen zuzugreifen, die in EF Core verfügbar sind.

Sehen Sie sich diese Ressourcen an, um mehr über minimale APIs zu erfahren:

Beibehalten des Synchronisierungskontexts in SaveChangesAsync

GitHub-Issue: 23971.

Wir haben den EF Core-Code in Version 5.0 geändert, um an allen Stellen, an denen für asynchronen Code verwendet wird, Task.ConfigureAwait auf festzulegen. Dies ist im Allgemeinen eine bessere Wahl für die Verwendung von EF Core. Allerdings ist SaveChangesAsync ein Sonderfall, weil EF Core generierte Werte in nachverfolgten Entitäten festlegt, nachdem der asynchrone Datenbankvorgang beendet wurde. Diese Änderungen können dann Benachrichtigungen auslösen, die beispielsweise für den Benutzeroberflächenthread ausgeführt werden müssen. Daher wird diese Änderung in EF Core 6.0 nur für die SaveChangesAsync-Methode wieder zurückgesetzt.

In-Memory-Datenbank: Überprüfen erforderlicher Eigenschaften auf NULL-Werte

GitHub-Issue: 10613. Dieses Feature wurde von @fagnercarvalho beigetragen. Danke vielmals!

Die In-Memory-Datenbank von EF Core löst jetzt eine Ausnahme aus, wenn versucht wird, einen NULL-Wert für eine erforderliche Eigenschaft zu speichern. Nehmen Sie beispielsweise an, dass ein User-Typ mit der erforderlichen Eigenschaft Username vorliegt:

public class User
{
    public int Id { get; set; }

    [Required]
    public string Username { get; set; }
}

Wenn Sie versuchen, eine Entität mit einem NULL-Wert für Username zu speichern, wird die folgende Ausnahme ausgelöst:

Microsoft.EntityFrameworkCore.DbUpdateException: Required properties '{'Username'}' are missing for the instance of entity type 'User' with the key value '{Id: 1}'. (Die erforderliche Eigenschaft „{Username}“ fehlt für die Instanz des Entitätstyps „User“ mit dem Schlüsselwert {Id: 1}).

Diese Überprüfung kann bei Bedarf deaktiviert werden. Beispiele:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .LogTo(Console.WriteLine, new[] { InMemoryEventId.ChangesSaved })
        .UseInMemoryDatabase("UserContextWithNullCheckingDisabled", b => b.EnableNullChecks(false));
}

Informationen zur Befehlsquelle für Diagnosen und Interceptors

GitHub-Issue: 23719. Dieses Feature wurde von @Giorgi beigetragen. Danke vielmals!

Die für Diagnosequellen und Interceptors bereitgestellte CommandEventData-Klasse enthält jetzt einen Enumerationswert, der angibt, welcher Teil von EF für die Erstellung des Befehls verantwortlich war. Diese kann als Filter in der Diagnose oder im Interceptor verwendet werden. Es wird beispielsweise ein Interceptor gewünscht, der nur für Befehle gilt, die von SaveChanges stammen:

public class CommandSourceInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<DbDataReader> ReaderExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        if (eventData.CommandSource == CommandSource.SaveChanges)
        {
            Console.WriteLine($"Saving changes for {eventData.Context!.GetType().Name}:");
            Console.WriteLine();
            Console.WriteLine(command.CommandText);
        }

        return result;
    }
}

Dadurch wird der Interceptor nur für SaveChanges-Ereignisse gefiltert, wenn er in einer Anwendung verwendet wird, die ebenso Migrationen und Abfragen generiert. Beispiel:

Saving changes for CustomersContext:

SET NOCOUNT ON;
INSERT INTO [Customers] ([Name])
VALUES (@p0);
SELECT [Id]
FROM [Customers]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

Bessere Verarbeitung temporärer Werte

GitHub-Issue: 24245.

EF Core macht keine temporären Werte für Entitätstypinstanzen verfügbar. Nehmen Sie beispielsweise an, dass ein Blog-Entitätstyp mit einem vom Speicher generierten Schlüssel vorliegt:

public class Blog
{
    public int Id { get; set; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

Die Schlüsseleigenschaft Id ruft einen temporären Wert ab, sobald ein Blog vom Kontext nachverfolgt wird. Ein Beispiel ist der Aufruf von DbContext.Add:

var blog = new Blog();
context.Add(blog);

Der temporäre Wert kann aus der Kontextänderungsnachverfolgung abgerufen werden, ist jedoch nicht in der Entitätsinstanz festgelegt. Ein Beispiel ist der folgende Code:

Console.WriteLine($"Blog.Id value on entity instance = {blog.Id}");
Console.WriteLine($"Blog.Id value tracked by EF = {context.Entry(blog).Property(e => e.Id).CurrentValue}");

Generiert folgende Ausgabe:

Blog.Id value on entity instance = 0
Blog.Id value tracked by EF = -2147482647

Das ist gut, weil dadurch verhindert wird, dass der temporäre Code im Anwendungscode verloren geht, in dem er versehentlich als nicht temporär behandelt werden kann. Manchmal ist es jedoch hilfreich, mit temporären Werten direkt umzugehen. Eine Anwendung sollte eigene temporäre Werte für einen Graph mit Entitäten erstellen, bevor sie nachverfolgt werden, damit sie zum Bilden von Beziehungen mithilfe von Fremdschlüsseln verwendet werden können. Dies kann erreicht werden, indem die Werte explizit als temporär gekennzeichnet werden. Beispiel:

var blog = new Blog { Id = -1 };
var post1 = new Post { Id = -1, BlogId = -1 };
var post2 = new Post { Id = -2, BlogId = -1 };

context.Add(blog).Property(e => e.Id).IsTemporary = true;
context.Add(post1).Property(e => e.Id).IsTemporary = true;
context.Add(post2).Property(e => e.Id).IsTemporary = true;

Console.WriteLine($"Blog has explicit temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has explicit temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has explicit temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");

In EF Core 6.0 verbleibt der Wert auf der Entitätsinstanz, obwohl er jetzt als temporär markiert ist. Der obige Code generiert z. B. die folgende Ausgabe:

Blog has explicit temporary ID = -1
Post 1 has explicit temporary ID = -1 and FK to Blog = -1
Post 2 has explicit temporary ID = -2 and FK to Blog = -1

Ebenso können temporäre Werte, die von EF Core generiert werden, explizit auf Entitätsinstanzen festgelegt und als temporäre Werte markiert werden. Diese Methode kann verwendet werden, um Beziehungen zwischen neuen Entitäten mit ihren temporären Schlüsselwerten explizit festzulegen. Beispiel:

var post1 = new Post();
var post2 = new Post();

var blogIdEntry = context.Entry(blog).Property(e => e.Id);
blog.Id = blogIdEntry.CurrentValue;
blogIdEntry.IsTemporary = true;

var post1IdEntry = context.Add(post1).Property(e => e.Id);
post1.Id = post1IdEntry.CurrentValue;
post1IdEntry.IsTemporary = true;
post1.BlogId = blog.Id;

var post2IdEntry = context.Add(post2).Property(e => e.Id);
post2.Id = post2IdEntry.CurrentValue;
post2IdEntry.IsTemporary = true;
post2.BlogId = blog.Id;

Console.WriteLine($"Blog has generated temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has generated temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has generated temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");

Ergebnis:

Blog has generated temporary ID = -2147482647
Post 1 has generated temporary ID = -2147482647 and FK to Blog = -2147482647
Post 2 has generated temporary ID = -2147482646 and FK to Blog = -2147482647

EF Core für C#-Nullverweistypen mit Anmerkungen

GitHub-Issue: 19007.

Die EF Core-Codebasis verwendet jetzt generell C#-Nullverweistypen (NRTs). Dies bedeutet, dass Sie die richtigen Compilerhinweise für die NULL-Verwendung erhalten, wenn Sie EF Core 6.0 aus Ihrem eigenen Code verwenden.

Microsoft.Data.Sqlite 6.0

Tipp

Sie können alle im Folgenden gezeigten Beispiele ausführen und debuggen, indem Sie den Beispielcode von GitHub herunterladen.

Verbindungspooling

GitHub-Issue: 13837.

Es ist üblich, Datenbankverbindungen nur so kurz wie möglich offen zu lassen. Dies trägt dazu bei, den Konflikt bei der Verbindungsressource zu reduzieren. Aus diesem Grund öffnen Bibliotheken wie EF Core die Verbindung erst direkt vor dem Ausführen eines Datenbankvorgangs und schließen sie danach sofort wieder. Betrachten Sie beispielsweise das folgende Beispiel für den EF Core-Code:

Console.WriteLine("Starting query...");
Console.WriteLine();

var users = context.Users.ToList();

Console.WriteLine();
Console.WriteLine("Query finished.");
Console.WriteLine();

foreach (var user in users)
{
    if (user.Username.Contains("microsoft"))
    {
        user.Username = "msft:" + user.Username;

        Console.WriteLine("Starting SaveChanges...");
        Console.WriteLine();

        context.SaveChanges();

        Console.WriteLine();
        Console.WriteLine("SaveChanges finished.");
    }
}

Die Ausgabe dieses Codes mit aktivierter Protokollierung der Verbindungen ist:

Starting query...

dbug: 8/27/2021 09:26:57.810 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
      Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
      Closed connection to database 'main' on server 'test.db'.

Query finished.

Starting SaveChanges...

dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
      Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.814 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
      Closed connection to database 'main' on server 'test.db'.

SaveChanges finished.

Beachten Sie, dass die Verbindung für jeden Vorgang schnell geöffnet und geschlossen wird.

Für die meisten Datenbanksysteme ist das Öffnen einer physischen Verbindung mit der Datenbank jedoch ein aufwendiger Vorgang. Daher erstellen die meisten ADO.NET-Anbieter einen Pool physischer Verbindungen und vermieten sie bei Bedarf an DbConnection-Instanzen.

SQLite ist etwas anders, da der Datenbankzugriff in der Regel nur eine Datei betrifft. Dies bedeutet, dass eine Verbindung mit einer SQLite-Datenbank in der Regel sehr schnell geöffnet wird. Dies ist jedoch nicht immer der Fall. Beispielsweise kann das Öffnen einer Verbindung mit einer verschlüsselten Datenbank sehr lange dauern. Daher werden SQLite-Verbindungen bei Verwendung von Microsoft.Data.Sqlite 6.0 jetzt in einem Pool gespeichert.

Unterstützung von DateOnly und TimeOnly

GitHub-Issue: 24506.

Microsoft.Data.Sqlite 6.0 unterstützt die neuen DateOnly- und TimeOnly-Typen von .NET 6. Diese können auch in EF Core 6.0 mit dem SQLite-Anbieter verwendet werden. Wie bei SQLite bedeutet das native Typsystem, dass die Werte dieser Typen als einer der vier unterstützten Typen gespeichert werden müssen. Microsoft.Data.Sqlite speichert sie als TEXT. Ein Beispiel ist eine Entität, die diese Typen verwendet:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    
    public DateOnly Birthday { get; set; }
    public TimeOnly TokensRenewed { get; set; }
}

Eine Zuordnung erfolgt zur folgenden Tabelle in der SQLite-Datenbank:

CREATE TABLE "Users" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY AUTOINCREMENT,
    "Username" TEXT NULL,
    "Birthday" TEXT NOT NULL,
    "TokensRenewed" TEXT NOT NULL);

Werte können anschließend wie gewohnt gespeichert, abgefragt und aktualisiert werden. Nehmen Sie diese LINQ-Abfrage in EF Core als Beispiel:

var users = context.Users.Where(u => u.Birthday < new DateOnly(1900, 1, 1)).ToList();

Diese Abfrage wird in SQLite wie folgt übersetzt:

SELECT "u"."Id", "u"."Birthday", "u"."TokensRenewed", "u"."Username"
FROM "Users" AS "u"
WHERE "u"."Birthday" < '1900-01-01'

Die Ausgabe besteht nur aus Fällen, bei denen Geburtstage vor dem Jahr 1900 liegen.

Found 'ajcvickers'
Found 'wendy'

Sicherungspunkte-API

GitHub-Issue: 20228.

Eine gängige API für Sicherungspunkte in ADO.NET-Anbietern wurde standardisiert. Microsoft.Data.Sqlite unterstützt jetzt diese API, einschließlich der folgenden Methoden:

Durch die Verwendung eines Sicherungspunkts kann für einen Teil der Transaktion anstatt für die gesamte Transaktion ein Rollback ausgeführt werden. Dieser Code erfüllt beispielsweise die folgenden Funktionen:

  • Transaktion erstellen
  • Update an die Datenbank senden
  • Sicherungspunkt erstellen
  • Weiteres Update an die Datenbank senden
  • Rollback auf den zuvor erstellten Sicherungspunkt ausführen
  • Transaktion committen
using var connection = new SqliteConnection("Command Timeout=60;DataSource=test.db");
connection.Open();

using var transaction = connection.BeginTransaction();

using (var command = connection.CreateCommand())
{
    command.CommandText = @"UPDATE Users SET Username = 'ajcvickers' WHERE Id = 1";
    command.ExecuteNonQuery();
}

transaction.Save("MySavepoint");

using (var command = connection.CreateCommand())
{
    command.CommandText = @"UPDATE Users SET Username = 'wfvickers' WHERE Id = 2";
    command.ExecuteNonQuery();
}

transaction.Rollback("MySavepoint");

transaction.Commit();

Dadurch wird das erste Update an die Datenbank committet. Das zweite Update wird jedoch nicht committet, da für den Sicherungspunkt ein Rollback ausgeführt wurde, bevor die Transaktion committet wurde.

Befehlstimeout in der Verbindungszeichenfolge

GitHub-Issue: 22505. Dieses Feature wurde von @nmichels beigetragen. Danke vielmals!

ADO.NET-Anbieter unterstützen zwei Timeouts:

  • Das Verbindungstimeout, das über die maximale Wartezeit bestimmt, wenn eine Verbindung mit der Datenbank hergestellt wird
  • Das Befehlstimeout, das über die maximale Wartezeit bestimmt, um die Ausführung eines Befehls abzuschließen

Das Befehlstimeout kann im Code mithilfe von DbCommand.CommandTimeout festgelegt werden. Viele Anbieter machen dieses Befehlstimeout jetzt auch in der Verbindungszeichenfolge verfügbar. Microsoft.Data.Sqlite folgt dieser Entwicklung mit dem Schlüsselwort Command Timeout für Verbindungszeichenfolgen. Beispielsweise verwendet "Command Timeout=60;DataSource=test.db" ein Standardtimeout von 60 Sekunden für Befehle, die von der Verbindung erstellt werden.

Tipp

In SQLite sind Default Timeout und Command Timeout synonym. Die Begriffe können daher austauschbar verwendet werden.