Auf Englisch lesen

Freigeben über


Neuerungen in EF Core 6.0

EF Core 6.0 wurde nach NuGet ausgeliefert. Diese Seite enthält eine Übersicht über interessante Änderungen, die in dieser Version eingeführt wurden.

Tipp

Sie können die unten gezeigten Beispiele ausführen und debuggen, indem Sie den Beispielcode von GitHub herunterladen.

SQL Server Zeittabellen

GitHub-Problem: #4693.

Zeitliche SQL Server-Tabellen verfolgen automatisch alle Daten, die jemals in einer Tabelle gespeichert sind, auch nachdem diese Daten aktualisiert oder gelöscht wurden. Dies wird erreicht, indem eine parallele "Verlaufstabelle" erstellt wird, in der zeitstempelte Verlaufsdaten gespeichert werden, wenn eine Änderung an der Haupttabelle vorgenommen wird. Auf diese Weise können historische Daten abgefragt werden, z. B. für die Prüfung, oder wiederhergestellt, z. B. für die Wiederherstellung nach versehentlicher Änderung oder Löschung.

EF Core unterstützt jetzt:

  • Erstellung temporaler Tabellen mithilfe von Migrationen
  • Transformation vorhandener Tabellen in zeitliche Tabellen, wieder mithilfe von Migrationen
  • Abfragen von historischen Daten
  • Wiederherstellen von Daten aus einem bestimmten Punkt in der Vergangenheit

Konfigurieren einer zeitlichen Tabelle

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

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

Bei Verwendung von EF Core zum Erstellen der Datenbank wird die neue Tabelle als zeitliche Tabelle mit den SQL Server-Standardwerten für die Zeitstempel und verlaufstabelle konfiguriert. Betrachten Sie z. B. einen Entitätstyp Employee :

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 zeitliche 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 erstellt, namens PeriodEnd und PeriodStart. 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 später in Abfragen verwendet werden können.

Wichtig

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

Beachten Sie auch, dass eine zugeordnete Verlaufstabelle namens EmployeeHistory automatisch erstellt wird. Die Namen der Periodenspalten und der Verlaufs- bzw. Historientabelle können durch zusätzliche Konfiguration am 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 spiegelt sich in der tabelle wider, 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 von zeitlichen Tabellen

Die meiste Zeit werden zeitliche Tabellen wie jede andere Tabelle verwendet. Dies bedeutet, dass die Periodenspalten und historischen Daten von SQL Server transparent behandelt werden, sodass sie von der Anwendung ignoriert werden können. Beispielsweise können neue Entitäten auf normale Weise 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
    });

await context.SaveChangesAsync();

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

var employee = await context.Employees.SingleAsync(e => e.Name == "Rainbow Dash");
context.Remove(employee);
await context.SaveChangesAsync();

Außerdem kann nach einer normalen Nachverfolgungsabfrage über die nachverfolgten Entitäten auf die Werte aus den Zeitraumspalten der aktuellen Daten zugegriffen werden. Beispiel:

var employees = await context.Employees.ToListAsync();
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 druckt:

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 aufgerufen PeriodEnd) den datetime2 maximalen Wert enthält. Dies ist immer der Fall für die aktuellen Zeilen in der Tabelle. Die ValidFrom Spalten (standardmäßig genannt PeriodStart) enthalten die UTC-Zeit, zu der die Zeile eingefügt wurde.

Abfragen von historischen Daten

EF Core unterstützt Abfragen, die historische Daten über mehrere neue Abfrageoperatoren enthalten:

  • TemporalAsOf: Gibt Zeilen zurück, die zur angegebenen UTC-Zeit aktiv (aktuell) waren. Dies ist eine einzelne Zeile aus der aktuellen Tabelle oder Verlaufstabelle für einen bestimmten Primärschlüssel.
  • TemporalAll: Gibt sämtliche Zeilen in den historischen Daten zurück. Dies ist in der Regel viele Zeilen aus der Verlaufstabelle und/oder der aktuellen Tabelle für einen bestimmten Primärschlüssel.
  • TemporalFromTo: Gibt alle Zeilen zurück, die zwischen zwei angegebenen UTC-Zeiten aktiv waren. Dies kann viele Zeilen aus der Verlaufstabelle und/oder der aktuellen Tabelle für einen bestimmten Primärschlüssel sein.
  • TemporalBetween: Dasselbe wie TemporalFromTo, mit der Ausnahme, dass Zeilen enthalten sind, die an der oberen Grenze aktiv wurden.
  • TemporalContainedIn: Gibt alle Zeilen zurück, die innerhalb des Zeitraums zwischen zwei bestimmten UTC-Zeiten sowohl aktiv geworden als auch ihre Aktivität beendet haben. Dies kann viele Zeilen aus der Verlaufstabelle und/oder der aktuellen Tabelle 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 zeitlichen Tabellen in SQL Server .

Nach einigen Aktualisierungen und Löschungen an unseren Daten können wir beispielsweise eine Abfrage TemporalAll ausführen, um die historischen Daten anzuzeigen:

var history = await 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")
        })
    .ToListAsync();

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 verwendet werden kann, um auf Werte aus den Zeitspalten zuzugreifen. Dies wird in der OrderBy Klausel zum Sortieren der Daten und dann in einer Projektion verwendet, um diese Werte 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 Zeile am 26.08.2021 um 16:44:59 Uhr nicht mehr aktiv war. Dies liegt daran, dass die Zeile 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 können mit TemporalFromTo, TemporalBetween oder TemporalContainedIn geschrieben werden. Beispiel:

var history = await 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")
        })
    .ToListAsync();

Diese Abfrage gibt die folgenden Zeilen 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 historischen Daten

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

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

context.Add(employee);
await context.SaveChangesAsync();

Diese Abfrage gibt eine einzelne Zeile für Rainbow Dash zurück, wie sie zur angegebenen UTC-Zeit war. Alle Abfragen, die zeitliche Operatoren verwenden, sind standardmäßig nicht nachverfolgt, sodass 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 sie dann auf SaveChanges.

Nach dem erneuten Einfügen der Zeile Rainbow Dash zeigt die Abfrage der historischen Daten an, dass die Zeile wiederhergestellt wurde, wie sie zu der angegebenen UTC-Zeit war:

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

Migrationspakete

GitHub-Problem: #19693.

EF Core-Migrationen werden verwendet, um Datenbankschemaaktualisierungen basierend auf Änderungen am EF-Modell zu generieren. Diese Schemaupdates sollten zur Anwendungsbereitstellungszeit angewendet werden, häufig als Teil eines kontinuierlichen Integrations-/fortlaufenden Bereitstellungssystems (C.I./C.D.).

EF Core bietet jetzt eine neue Möglichkeit zum Anwenden dieser Schemaupdates: Migrationspakete. Ein Migrationspaket ist eine kleine ausführbare Datei mit Migrationen und dem Code, der zum Anwenden dieser Migrationen auf die Datenbank erforderlich ist.

Hinweis

Eine ausführlichere Erläuterung zu Migrationen, Bündeln und Bereitstellungen finden Sie in der Einführung in devOps-freundliche EF Core-Migrationspakete im .NET-Blog.

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

Ein Bündel benötigt Migrationen, um einbezogen zu werden. Diese werden unter Verwendung von dotnet ef migrations add erstellt, wie in der Migrationsdokumentation beschrieben. Nachdem Sie migrationen bereit für die Bereitstellung haben, erstellen Sie ein Bundle mit dem dotnet ef migrations 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, also erhalte ich ein efbundle.exe in meinem lokalen Ordner. Wenn Sie diese ausführbare Datei ausführen, 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 angewendet wurden. Wenn Sie beispielsweise dasselbe Bundle erneut ausführen, passiert nichts, da keine neuen Migrationen angewendet 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 mehr Migrationen generiert werdendotnet ef migrations add, können diese in einer neuen ausführbaren Datei gebündelt werden, die bereit zur Anwendung ist. 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 die --force Option verwendet werden kann, um das vorhandene Bundle mit einem neuen zu überschreiben.

Wenn Sie dieses neue Bundle ausführen, werden diese 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 Bündel die Datenbankverbindungszeichenfolge aus der Konfiguration Ihrer Anwendung. Eine andere Datenbank kann jedoch migriert werden, indem die 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 diesmal alle drei Migrationen angewendet wurden, da noch keines auf die Produktionsdatenbank angewendet wurde.

Weitere 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 verwendet werden soll, wenn das Projekt mehrere Kontexttypen enthält.
  • --project um das zu verwendende Projekt anzugeben. Standardmäßig wird das aktuelle Arbeitsverzeichnis verwendet.
  • --startup-project um das zu verwendende Startprojekt anzugeben. Die Standardeinstellung ist das aktuelle Arbeitsverzeichnis.
  • --no-build um zu verhindern, dass das Projekt erstellt wird, bevor der Befehl ausgeführt wird. Dies sollte nur verwendet werden, wenn das Projekt als up-to-Datum bekannt ist.
  • --verbose um detaillierte Informationen zu den Aktionen des Befehls anzuzeigen. Verwenden Sie diese Option, wenn Informationen in Fehlerberichte eingeschlossen werden.

Verwenden Sie dotnet ef migrations bundle --help , um alle verfügbaren Optionen anzuzeigen.

Beachten Sie, dass jede Migration standardmäßig in einer eigenen Transaktion angewendet wird. Unter GitHub-Problem Nr. 22616 finden Sie eine Diskussion über mögliche zukünftige Verbesserungen in diesem Bereich.

Konfigurierung des Modells vor der Konvention

GitHub-Problem: #12229.

In früheren Versionen von EF Core muss die Zuordnung für jede Eigenschaft eines bestimmten Typs explizit konfiguriert werden, wenn sich diese Zuordnung von der Standardeinstellung unterscheidet. Dazu gehören "Facetten" wie die maximale Länge von Zeichenfolgen und die Dezimalgenauigkeit sowie die Umwandlung von Werten gemäß dem Eigenschaftstyp.

Dazu ist entweder Folgendes erforderlich:

  • Modell-Generator-Konfiguration für jede Eigenschaft
  • Ein Zuordnungsattribut für jede Eigenschaft
  • Explizite Iteration über alle 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 schwer robust zu erledigen ist, da die Liste der Entitätstypen und zugeordneten Eigenschaften zum Zeitpunkt dieser Iteration möglicherweise nicht abgeschlossen ist.

EF Core 6.0 ermöglicht es, diese Zuordnungskonfiguration einmal für einen bestimmten Typ anzugeben. Sie wird dann auf alle Eigenschaften dieses Typs im Modell angewendet. Dies wird als "Vorkonventionsmodellkonfiguration" bezeichnet, da sie Aspekte des Modells konfiguriert, die dann von den Modellbaukonventionen verwendet werden. Diese Konfiguration wird angewendet, indem Sie ConfigureConventions in Ihrem DbContext überschreiben.

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

Betrachten Sie beispielsweise die folgenden 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 so konfiguriert werden, dass sie ANSI (anstelle von Unicode) sind und eine maximale Länge von 1024 aufweisen:

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

Alle Datum/Zeit-Eigenschaften können in der Datenbank in 64-Bit-Ganzzahlen umgewandelt werden, indem die Standardkonvertierung von Datum/Zeit zu long verwendet wird.

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

Alle bool'schen Eigenschaften können mithilfe eines der integrierten Wertkonverter in die ganzen Zahlen 0 oder 1 umgewandelt werden.

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

Angenommen, es handelt sich Session um eine vorübergehende Eigenschaft der Entität und sollte nicht beibehalten werden, kann sie überall im Modell ignoriert werden:

configurationBuilder
    .IgnoreAny<Session>();

Die Konfiguration des Vorkonventionsmodells ist beim Arbeiten mit Wertobjekten sehr nützlich. Der Typ Money im obigen Modell wird beispielsweise 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
}

Dies 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 tabellen, die für SQL Server mithilfe von Migrationen erstellt wurden, 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 der Abfrage auf eine Weise verwendet wird, die nicht mit einer zugeordneten Eigenschaft des Modells verknüpft ist.

Hinweis

Unter "Ankündigung von Entity Framework Core 6.0 Preview 6: Konfigurieren von Konventionen im .NET-Blog" finden Sie weitere Diskussionen und Beispiele für die Konfiguration von Vorkonventionsmodellen.

Kompilierte Modelle

GitHub-Problem: #1906.

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

Startzeit bedeutet die Zeit, die benötigt wird, um die erste Operation in einem DbContext auszuführen, wenn dieser DbContext-Typ zum ersten Mal in der Anwendung genutzt wird. Beachten Sie, dass beim Erstellen einer DbContext-Instanz das EF-Modell nicht 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 dieses Befehls enthält einen Code, den Sie in Ihre DbContext-Konfiguration kopieren und einfügen können, um EF Core zur Verwendung des kompilierten Modells zu veranlassen. 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 einen Benchmark darauf auszuführen, indem Sie den Beispielcode von GitHub herunterladen.

Das Modell im oben referenzierten GitHub-Repository enthält 449 Entitätstypen, 6390 Eigenschaften und 720 Beziehungen. Dies ist ein mittelgroßes Modell. Mit BenchmarkDotNet zum Messen beträgt die durchschnittliche Zeit für die erste Abfrage 1,02 Sekunden auf einem vernünftigen Laptop. Die Verwendung kompilierter Modelle bringt dies auf 117 Millisekunden auf die gleiche Hardware. Eine Verbesserung um den Faktor 8 bis 10 bleibt relativ konstant, während die Modellgröße zunimmt.

Kompilierte Modellleistungsverbesserung

Hinweis

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

Verbesserte Performance bei TechEmpower Fortunes

GitHub-Problem: #23611.

Wir haben erhebliche Verbesserungen an der Abfrageleistung für EF Core 6.0 vorgenommen. Dies gilt insbesondere in folgenden Fällen:

  • Ef Core 6.0 Leistung ist jetzt 70% schneller auf dem Branchenstandard TechEmpower Fortunes Benchmark, verglichen mit 5,0.
    • Dies ist die Full-Stack-Leistungsverbesserung, einschließlich Verbesserungen im Benchmark-Code, der .NET-Runtime etcetera.
  • EF Core 6.0 selbst ist 31% schnellere Ausführung nicht nachverfolgter Abfragen.
  • Heap-Zuordnungen wurden bei der Ausführung von Abfragen um 43% reduziert.

Nach diesen Verbesserungen wurde die Lücke zwischen dem beliebten "micro-ORM" Dapper und EF Core im TechEmpower Fortunes-Benchmark von 55% auf etwas unter 5%eingegrenzt.

Hinweis

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

Verbesserungen des Azure Cosmos DB-Anbieters

EF Core 6.0 enthält viele Verbesserungen des Azure Cosmos DB-Datenbankanbieters.

Tipp

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

Standardmäßiger impliziter Besitz

GitHub-Problem: #24803.

Beim Erstellen eines Modells für den Azure Cosmos DB-Anbieter kennzeichnet EF Core 6.0 standardmäßig untergeordnete Entitätstypen als von ihrer übergeordneten Entität besessen. Dadurch wird die Notwendigkeit der vielen OwnsMany und OwnsOne Aufrufe im Azure Cosmos DB-Modell aufgehoben. Dadurch ist es einfacher, Kind-Typen in das Dokument für den übergeordneten Typ einzubetten, was in der Regel der geeignete Weg ist, um Parent- und Child-Elemente in einer Dokumentendatenbank zu modellieren.

Betrachten Sie z. B. diese 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 wären diese Typen für Azure Cosmos DB mit der folgenden Konfiguration modelliert worden:

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 und reduziert die Modellkonfiguration auf:

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

Die resultierenden Azure Cosmos DB-Dokumente haben die Eltern, Kinder, Haustiere und Adresse der Familie in das Familiendokument eingebettet. 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

Es ist wichtig zu beachten, dass die OwnsOne/OwnsMany Konfiguration verwendet werden muss, wenn Sie diese eigenen Typen weiter konfigurieren müssen.

Auflistungen von Grundtypen

GitHub-Problem: #14762.

EF Core 6.0 ordnet Sammlungen primitiver Typen bei Verwendung des Azure Cosmos DB-Datenbankanbieters nativ zu. Betrachten Sie z. B. diesen Entitätstyp:

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 befü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);
await context.SaveChangesAsync();

Dies führt zum folgenden 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 Auflistungen können dann wieder auf normale Weise aktualisiert werden:

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

await context.SaveChangesAsync();

Einschränkungen:

  • Es werden nur Wörterbücher mit Zeichenfolgenschlüsseln unterstützt.
  • Das Abfragen in den Inhalt von primitiven Auflistungen wird derzeit nicht unterstützt. Stimmen Sie für #16926, #25700 und #25701 , wenn diese Features für Sie wichtig sind.

Übersetzungen zu integrierten Funktionen

GitHub-Problem: #16143.

Der Azure Cosmos DB-Anbieter übersetzt jetzt mehr Methoden der Base Class Library (BCL) in die eingebauten Funktionen von Azure Cosmos DB. Die folgenden Tabellen zeigen Übersetzungen, die in EF Core 6.0 neu sind.

Zeichenfolgenübersetzungen

BCL-Methode Integrierte Funktion Hinweise
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 STRINGEQUALS Nur fallunabhängige Aufrufe

Übersetzungen für LOWER, LTRIM, , RTRIMTRIM, UPPERund SUBSTRING wurden von @Marusyk beigetragen. Danke vielmals!

Zum Beispiel:

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

Dies bedeutet:

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!

Zum Beispiel:

var hypotenuse = 42.42;
var mathResults = await 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))
    .ToListAsync();

Dies bedeutet:

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!

Zum Beispiel:

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

Dies bedeutet:

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

Raw SQL-Abfragen mit FromSql

GitHub-Problem: #17311.

Manchmal ist es erforderlich, eine unformatierte SQL-Abfrage auszuführen, anstatt LINQ zu verwenden. Dies wird jetzt durch den Anbieter Azure Cosmos DB mithilfe der FromSql-Methode unterstützt. Dies funktioniert genauso wie bei relationalen Anbietern. Beispiel:

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

Dies wird wie folgt ausgeführt:

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

Unterschiedliche Abfragen

GitHub-Problem: #16144.

Einfache Abfragen mit Distinct werden jetzt übersetzt. Beispiel:

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

Dies bedeutet:

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

Diagnostik

GitHub-Problem: #17298.

Der Azure Cosmos DB-Anbieter protokolliert jetzt weitere Diagnoseinformationen, einschließlich Ereignisse zum Einfügen, Abfragen, Aktualisieren und Löschen von Daten aus der Datenbank. Die Anforderungseinheiten (RU) sind bei Bedarf in diesen Ereignissen enthalten.

Hinweis

In den Protokollen wird EnableSensitiveDataLogging() verwendet, damit ID-Werte angezeigt werden.

Durch einfügen eines Elements in die Azure Cosmos DB-Datenbank wird das CosmosEventId.ExecutedCreateItem Ereignis generiert. Beispiel für diesen Code:

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

Protokolliert das folgende 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'

Das Abrufen von Elementen aus der Azure Cosmos DB-Datenbank mithilfe einer Abfrage generiert das CosmosEventId.ExecutingSqlQuery Ereignis und dann ein oder CosmosEventId.ExecutedReadNext mehrere Ereignisse für die gelesenen Elemente. Beispiel für diesen Code:

var equilateral = await context.Triangles.SingleAsync(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

Durch das Abrufen eines einzelnen Elements aus der Azure Cosmos DB-Datenbank mit einem Partitionsschlüssel Find werden die Ereignisse CosmosEventId.ExecutingReadItem und CosmosEventId.ExecutedReadItem generiert. Beispiel für diesen Code:

var isosceles = await context.Triangles.FindAsync("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'

Wenn Sie ein aktualisiertes Element in der Azure Cosmos DB-Datenbank speichern, wird das CosmosEventId.ExecutedReplaceItem Ereignis generiert. Beispiel für diesen Code:

triangle.Angle2 = 89;
await context.SaveChangesAsync();

Protokolliert das folgende 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'

Durch das Löschen eines Elements aus der Azure Cosmos DB-Datenbank wird das CosmosEventId.ExecutedDeleteItem Ereignis generiert. Beispiel für diesen Code:

context.Remove(triangle);
await context.SaveChangesAsync();

Protokolliert das folgende 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-Problem: #17301.

Das Azure Cosmos DB-Modell kann jetzt mit einem manuellen oder automatischen Skalierungsdurchsatz konfiguriert werden. Diese Werte stellen den Durchsatz für die Datenbank bereit. Beispiel:

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

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.HasManualThroughput(5000);
        entityTypeBuilder.HasAutoscaleThroughput(3000);
    });

Lebensdauer konfigurieren

GitHub-Problem: #17307.

Entitätstypen im Azure Cosmos DB-Modell können jetzt mit der Standardlebensdauer und der Lebensdauer des Analysespeichers konfiguriert werden. Beispiel:

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

HTTP-Clientfactory auflösen.

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

Der vom Azure Cosmos DB-Anbieter verwendete HttpClientFactory kann jetzt explizit festgelegt werden. Dies kann besonders hilfreich beim Testen sein, z. B. um die Zertifikatüberprüfung bei Verwendung des Azure Cosmos DB-Emulators unter Linux zu umgehen:

optionsBuilder
    .EnableSensitiveDataLogging()
    .UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
        "PrimitiveCollections",
        cosmosOptionsBuilder =>
        {
            cosmosOptionsBuilder.HttpClientFactory(
                () => new HttpClient(
                    new HttpClientHandler
                    {
                        ServerCertificateCustomValidationCallback =
                            HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
                    }));
        });

Hinweis

Sehen Sie sich auf dem .NET-Blog das EF Core Azure Cosmos DB Provider Test Drive an, um ein detailliertes Beispiel dafür zu erhalten, wie Verbesserungen des Azure Cosmos DB-Anbieters auf eine bestehende Anwendung angewendet werden können.

Verbesserungen beim Erstellen von Gerüsten aus einer vorhandenen Datenbank

EF Core 6.0 enthält mehrere Verbesserungen beim Reverse Engineering eines EF-Modells aus einer vorhandenen Datenbank.

Unterstützung von Viele-zu-Viele-Beziehungen

GitHub-Problem: #22475.

EF Core 6.0 erkennt einfache Join-Tabellen und generiert automatisch eine viele-zu-viele-Zuordnung für sie. Betrachten Sie beispielsweise Tabellen für Posts und Tagsund eine Verknüpfungstabelle 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,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([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 [Tags] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([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; }
}

Und 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; }
}

Aber keine Klasse für die PostTag Tabelle. Stattdessen wird die Konfiguration für eine viele-zu-viele-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");
            });

Anleitung für nullable C#-Verweistypen

GitHub-Problem: #15520.

EF Core 6.0 erstellt jetzt ein Gerüst für ein EF-Modell und für Entitätstypen, die C#-nullable Referenztypen (NRTs) verwenden. Die Verwendung von NRT wird automatisch bereitgestellt, wenn die NRT-Unterstützung im C#-Projekt aktiviert ist, in das der Code eingegliedert wird.

Die folgende Tags Tabelle enthält zum Beispiel sowohl nullbare als auch nicht-nullbare 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 zu entsprechenden nullablen und nicht nullablen Zeichenfolgeneigenschaften in der generierten Klasse:

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; }
}

Ebenso haben 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 zur Gerüsterstellung von nicht nullablen (erforderlichen) Beziehungen 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; }
}

Und Beiträge:

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!;

Datenbankkommentare werden als Codekommentare generiert.

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

Kommentare zu SQL-Tabellen und -Spalten werden jetzt in die erstellten Entitätstypen eingebettet, die beim Reverse Engineering eines EF Core-Modells aus einer vorhandenen SQL Server-Datenbank erzeugt werden.

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

LINQ-Abfrageverbesserungen

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

Verbesserte GroupBy-Unterstützung

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

EF Core 6.0 enthält eine bessere Unterstützung für GroupBy Abfragen. Insbesondere EF Core jetzt:

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

Im Folgenden sind Beispielabfragen von Kundenberichten und deren Übersetzung auf SQL Server aufgeführt.

Beispiel 1:

var people = await context.People
    .Include(e => e.Shoes)
    .GroupBy(e => e.FirstName)
    .Select(
        g => g.OrderBy(e => e.FirstName)
            .ThenBy(e => e.LastName)
            .FirstOrDefault())
    .ToListAsync();
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 = await context.People
    .Select(
        p => new
        {
            p.FirstName,
            FullName = p.FirstName + " " + p.MiddleInitial + " " + p.LastName
        })
    .GroupBy(p => p.FirstName)
    .Select(g => g.First())
    .FirstAsync();
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 = await context.People
    .Where(e => e.MiddleInitial == "Q" && e.Age == 20)
    .GroupBy(e => e.LastName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e.Length)
    .ToListAsync();
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 = await (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()
               })
    .ToListAsync();
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 = await context.People
    .GroupBy(e => e.FirstName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e)
    .ToListAsync();
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 = await context.People
    .Where(e => e.Age == 20)
    .GroupBy(e => e.Id)
    .Select(g => g.First().MiddleInitial)
    .OrderBy(e => e)
    .ToListAsync();
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
    = await 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),
            })
        .ToListAsync();
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 = await 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)
        })
    .CountAsync();
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 = await context.People
    .GroupBy(n => n.FirstName)
    .Select(g => new
    {
        Feet = g.Key,
        Total = g.Sum(n => n.Feet.Size)
    })
    .ToListAsync();
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 = await 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)
    .ToListAsync();
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 = await 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()})
    .ToListAsync();
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 = await context.People
    .GroupBy(m => new {m.FirstName, m.MiddleInitial })
    .Select(am => new
    {
        Key = am.Key,
        Items = am.ToList()
    })
    .ToListAsync();
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

Die für diese Beispiele verwendeten Entitätstypen sind:

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; }
}

String.Concat mit mehreren Argumenten übersetzen

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

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

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

Wird bei Verwendung von SQL Server in das folgende SQL übersetzt:

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-Problem: #24041.

Das System.Linq.Async-Paket fügt clientseitige asynchrone LINQ-Verarbeitung hinzu. Die Verwendung dieses Pakets mit früheren Versionen von EF Core war aufgrund eines Namespacekonflikts für die asynchronen LINQ-Methoden umständlich. In EF Core 6.0 haben wir den C#-Musterabgleich für IAsyncEnumerable<T> genutzt, sodass der bereitgestellte EF Core DbSet<TEntity> 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-Problem: #23921.

In EF Core 6.0 haben wir die Parameteranforderungen für FreeText(DbFunctions, String, String) und Contains gelockert. Auf diese Weise können diese Funktionen mit binären Spalten oder mit Spalten verwendet werden, die mit einem Wertkonverter zugeordnet sind. Betrachten Sie z. B. einen Entitätstyp mit einer Name Eigenschaft, 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; }
}

Dies wird in der Datenbank JSON 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 jetzt mithilfe von Contains oder FreeText ausgeführt werden, obwohl der Typ der Eigenschaft Name und nicht string ist. Beispiel:

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

Dadurch wird die folgende SQL generiert, wenn SQL Server verwendet wird:

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

Übersetzen von ToString auf SQLite

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

Aufrufe an ToString() werden jetzt bei Verwendung des SQLite-Datenbankanbieters in SQL-Anweisungen umgewandelt. Dies kann für Textsuchen hilfreich sein, die nicht Zeichenfolgenspalten umfassen. Ziehen Sie beispielsweise einen User Entitätstyp in Betracht, 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 Zahl in eine Zeichenfolge in der Datenbank zu konvertieren. Anschließend können wir diese Zeichenfolge mit einer Funktion verwenden, z LIKE . B. um Zahlen zu finden, die einem Muster entsprechen. Um beispielsweise alle Zahlen zu finden, die 555 enthalten:

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

Dies wird bei Verwendung einer SQLite-Datenbank in das folgende SQL übersetzt:

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

Beachten Sie, dass die Übersetzung für ToString() SQL Server bereits in EF Core 5.0 unterstützt wird und möglicherweise auch von anderen Datenbankanbietern unterstützt wird.

EF.Functions.Random

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

EF.Functions.Random ordnet einer Datenbankfunktion zu, die eine Pseudo-Zufallszahl zwischen 0 und 1 exklusiv zurückgibt. Übersetzungen wurden im EF Core-Repository für SQL Server, SQLite und Azure Cosmos DB implementiert. Betrachten Sie z. B. einen User Entitätstyp mit einer Popularity Eigenschaft:

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 einer EF.Functions.Random Abfrage können wir eine Abfrage schreiben, um alle Benutzer mit einer zufällig ausgewählten Beliebtheit zurückzugeben:

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

Dies wird bei Verwendung einer SQL Server-Datenbank in die folgende SQL-Datenbank übersetzt:

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

Verbesserte SQL Server-Übersetzung für IsNullOrWhitespace

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

Betrachten Sie die folgende Abfrage:

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

Vor EF Core 6.0 wurde dies auf SQL Server wie folgt ü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''))

Diese Übersetzung wurde für EF Core 6.0 um folgende Punkte 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-Problem: #24600.

Eine neue Methode ToInMemoryQuery kann verwendet werden, um eine definierende Abfrage für die In-Memory-Datenbank für einen bestimmten Entitätstyp zu schreiben. Dies ist am nützlichsten für das Erstellen der Entsprechung von Ansichten in der Speicherdatenbank, insbesondere wenn diese Ansichten schlüssellose Entitätstypen zurückgeben. Betrachten Sie beispielsweise eine Kundendatenbank für Kunden, die im Vereinigten Königreich ansässig sind. 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 jetzt vor, wir möchten eine Ansicht über diese Daten, die zeigt, wie viele Kunden in jedem Postcodebereich vorhanden sind. Wir können einen schlüssellosen Entitätstyp erstellen, um folgendes darzustellen:

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

Und definieren Sie eine DbSet-Eigenschaft für sie im DbContext zusammen mit Sätzen für andere Entitätstypen der obersten Ebene:

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

Anschließend können wir 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 wie jede andere DbSet-Eigenschaft abgefragt werden:

var results = await context.CustomerDensities.ToListAsync();

Übersetze Teilzeichenfolge mit einem Einzelparameter

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

** EF Core 6.0 übersetzt jetzt Anwendungen von string.Substring mit einem einzigen Argument. Beispiel:

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

Dies wird bei Verwendung von SQL Server in die folgende SQL-Datei ü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'

Split-Abfragen für Nicht-Navigationssammlungen

GitHub-Problem: #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 nicht navigationsfreie Auflistungen in der Abfrageprojektion enthalten sind.

Im Folgenden sehen Sie Beispielabfragen, die die Übersetzung auf SQL Server in einer einzelnen Abfrage oder mehreren Abfragen anzeigen.

Beispiel 1:

LINQ-Abfrage:

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

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:

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

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:

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

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]

Letzte ORDER BY-Klausel bei der Verknüpfung mit der Sammlung entfernen

GitHub-Problem: #19828.

Beim Laden verwandter eins-zu-viele-Entitäten fügt EF Core ORDER BY-Klauseln hinzu, um sicherzustellen, dass alle zugehörigen Entitäten einer bestimmten Entität zusammengefasst werden. Die letzte ORDER BY-Klausel ist jedoch nicht erforderlich, damit EF die erforderlichen Gruppierungen generiert und auswirkungen auf die Leistung haben kann. Daher wird diese Klausel von EF Core 6.0 entfernt.

Betrachten Sie z. B. diese Abfrage:

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

Mit EF Core 5.0 auf SQL Server wird diese Abfrage in Folgendes ü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]

Unter EF Core 6.0 wird dies stattdessen zu:

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 Dateinamen und Zeilennummer

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

Abfragetags ermöglichen das Hinzufügen eines Texturtags zu einer LINQ-Abfrage, sodass sie dann in der generierten SQL-Datei enthalten ist. In EF Core 6.0 kann dies verwendet werden, um Abfragen mit dem Dateinamen und der Zeilennummer des LINQ-Codes zu kategorisieren. Beispiel:

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

Dies führt zu der folgenden generierten SQL-Datei bei Verwendung von SQL Server:

-- 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 der Behandlung von optionalen abhängigen Personen im Besitz

GitHub-Problem: #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 gemeinsam verwendet. Dies liegt daran, dass es in der Tabelle eine Zeile für den Abhängigen gibt, da der Prinzipal sie benötigt, unabhängig davon, ob der Abhängige existiert oder nicht. Der Weg, dies eindeutig zu behandeln, besteht darin sicherzustellen, dass die abhängige Entität mindestens eine erforderliche Eigenschaft hat. Da eine erforderliche Eigenschaft nicht null sein kann, bedeutet dies, wenn der Wert in der Spalte für diese Eigenschaft null ist, dann ist die abhängige Entität nicht vorhanden.

Ziehen Sie beispielsweise eine Customer Klasse in Betracht, in der jeder Kunde über eine eigene AddressKlasse 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 sie gültig ist, um einen Kunden ohne Adresse zu speichern:

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

Wenn ein Kunde jedoch über eine Adresse verfügt, muss diese Adresse mindestens eine Nicht-NULL-Postleitzahl aufweisen:

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

Dies wird sichergestellt, indem die Eigenschaft Postcode als Required gekennzeichnet wird.

Wenn Kunden abgefragt werden und die Spalte "Postcode" leer ist, bedeutet dies, dass der Kunde keine Adresse hat und die Navigations-Eigenschaft Customer.Address null bleibt. Beispiel: Durchlaufen der Kunden und Überprüfen, ob die Adresse null ist:

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

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

Generiert die folgenden Ergebnisse:

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

Erwägen Sie stattdessen den Fall, in dem keine Eigenschaft aus 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 einen Kunden ohne Adresse als auch einen Kunden 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 zu unterscheiden, da wir sehen können, indem wir die Datenbankspalten direkt abfragen:

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 jetzt beim Speichern eines optional abhängigen Elements, bei dem alle Eigenschaften null sind. Beispiel:

warn: 9/27/2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) Die Entität vom Typ "Address" mit Primärschlüsselwerten {CustomerId: -2147482646} ist eine optionale abhängige Verwendung der Tabellenfreigabe. Die Entität verfügt nicht über eine Eigenschaft mit einem Nicht-Standardwert, um zu ermitteln, ob die Entität vorhanden ist. Dies bedeutet, dass beim Abfragen keine Objektinstanz erstellt wird, sondern eine Instanz, bei der alle Eigenschaften auf Standardwerte festgelegt sind. Auch geschachtelte Abhängige gehen verloren. Speichern Sie entweder keine Instanz mit nur Standardwerten, oder markieren Sie die eingehende Navigation als erforderlich im Modell.

Dies wird noch komplizierter, wenn das optionale abhängige Element selbst als Hauptkomponente für ein weiteres optionales abhängiges Element fungiert, das ebenfalls derselben Tabelle zugeordnet ist. Statt nur zu warnen, verbietet EF Core 6.0 nur Fälle von geschachtelten optionalen Abhängigen. Betrachten Sie z. B. das folgende Modell, in dem ContactInfo sich im Besitz von Customer befindet und Address wiederum im Besitz von ContactInfo ist.

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 null ist, wird EF Core keine Instanz von Address erstellen, wenn die Beziehung optional ist, auch wenn die Adresse selbst Daten enthalten könnte. Für diese Art von Modell löst EF Core 6.0 die folgende Ausnahme aus:

System.InvalidOperationException: Der Entitätstyp 'ContactInfo' ist eine optionale Entität, die eine Tabellenfreigabe verwendet und enthält andere Abhängige, ohne eine erforderliche, nicht freigegebene Eigenschaft, um festzustellen, ob die Entität existiert. Wenn alle nullablen Eigenschaften einen Nullwert in der Datenbank enthalten, wird in der Abfrage keine Objektinstanz erstellt, sodass die Werte geschachtelter Abhängiger verloren gehen. Fügen Sie eine erforderliche Eigenschaft hinzu, um Instanzen mit NULL-Werten für andere Eigenschaften zu erstellen oder die eingehende Navigation als erforderlich zu markieren, um immer eine Instanz zu erstellen.

Das Fazit hier ist, den Fall zu vermeiden, in dem ein optionaler Abhängiger alle nullbare Eigenschaftswerte enthalten kann und eine Tabelle mit seinem Hauptobjekt teilt. Es gibt drei einfache Möglichkeiten, dies zu vermeiden:

  1. Stellen Sie die abhängigen Erforderlichen fest. Dies bedeutet, dass die abhängige Entität immer einen Wert aufweist, nachdem sie abgefragt wurde, auch wenn alle Ihre Eigenschaften null sind.
  2. Stellen Sie sicher, dass die abhängige mindestens eine erforderliche Eigenschaft enthält, wie oben beschrieben.
  3. Speichern Sie optionale abhängige Datensätze in einer eigenen Tabelle, anstatt eine Tabelle mit dem Hauptobjekt zu teilen.

Ein Abhängiger kann erforderlich gemacht werden, indem das Attribut Required auf seiner Navigation verwendet wird.

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; }
}

Oder durch Angabe, dass sie erforderlich ist in OnModelCreating:

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

Abhängige können in einer anderen Tabelle gespeichert werden, indem die 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, darunter auch jene mit geschachtelten optionalen Abhängigkeiten, finden Sie auf GitHub im Beispiel "OptionalDependentsSample".

Neue Zuordnungsattribute

EF Core 6.0 enthält mehrere neue Attribute, die auf Code angewendet werden können, um die Art und Weise zu ändern, wie sie der Datenbank zugeordnet wird.

Unicode-Attribut

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

Ab EF Core 6.0 kann eine Zeichenfolgeneigenschaft nun einer Nicht-Unicode-Spalte mithilfe eines Zuordnungsattributs zugeordnet werden, ohne den Datenbanktyp direkt anzugeben. Betrachten Sie beispielsweise einen Book Entitätstyp mit einer Eigenschaft für die International Standard Book Number (ISBN) in Form "ISBN 978-3-16-148410-0":

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 Nicht-Unicode-Zeichen enthalten kann, führt das Unicode Attribut dazu, dass ein Nicht-Unicode-Zeichenfolgentyp verwendet wird. Darüber hinaus wird verwendet, MaxLength um die Größe der Datenbankspalte zu begrenzen. Wenn Sie z. B. SQL Server verwenden, führt dies zu einer Datenbankspalte von 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 nur Unicode-Typen unterstützt.

Präzisionsattribut

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

Die Genauigkeit und Skalierung einer Datenbankspalte kann jetzt mithilfe von Zuordnungsattributen konfiguriert werden , ohne den Datenbanktyp direkt anzugeben. Betrachten Sie beispielsweise einen Product Entitätstyp mit einer Dezimaleigenschaft Price :

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

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

EF Core zuordnen diese Eigenschaft einer Datenbankspalte mit Genauigkeit 10 und Skalierung 2. 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-Problem: #23163. Dieses Feature wurde von @KaloyanIT beigetragen. Danke vielmals!

IEntityTypeConfiguration<TEntity> Instanzen ermöglichen es, die ModelBuilder Konfiguration für jeden Entitätstyp in einer eigenen Konfigurationsklasse vorzunehmen. 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 von DbContext.OnModelCreating aufgerufen werden. Beispiel:

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

Ab EF Core 6.0 kann ein EntityTypeConfigurationAttribute Entitätstyp platziert werden, sodass EF Core geeignete Konfigurationen finden und verwenden kann. Beispiel:

[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 Book Entitätstyp in einem Modell enthalten ist. Der Entitätstyp ist in einem Modell enthalten, das einen der normalen Mechanismen verwendet. Beispielsweise durch Erstellen einer DbSet<TEntity> Eigenschaft für den Entitätstyp:

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

    //...

Oder indem Sie es 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 ermittelt wird.

Verbesserungen bei der Modellerstellung

Neben neuen Zuordnungsattributen enthält EF Core 6.0 mehrere weitere Verbesserungen am Modellbauprozess.

Unterstützung für spärliche SQL Server-Spalten

GitHub-Problem: #8023.

SQL Server dünnbesetzte Spalten sind gewöhnliche Spalten, die zum Speichern von Nullwerten optimiert sind. Dies kann nützlich sein, wenn Sie die TPH-Vererbungszuordnung verwenden, bei der Eigenschaften eines selten verwendeten Untertyps für die meisten Zeilen in der Tabelle null-Spaltenwerte ergeben. Betrachten Sie beispielsweise eine ForumModerator Klasse, die von ForumUser erweitert wird.

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

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

Es kann Millionen von Nutzern geben, mit nur einer Handvoll davon als Moderatoren. Dies bedeutet, dass es sinnvoll sein könnte, das ForumName hier als spärlich abzubilden. Dies kann jetzt mithilfe von IsSparse in OnModelCreating konfiguriert werden. Beispiel:

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

EF Core-Migrationen markieren dann die Spalte als dünn besetzt. Beispiel:

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

Geringe Spalten weisen Einschränkungen auf. Lesen Sie unbedingt die Dokumentation zu spärlichen Spalten in SQL Server , um sicherzustellen, dass sparse Spalten die richtige Wahl für Ihr Szenario sind.

Verbesserungen an der HasConversion-API

GitHub-Problem: #25468.

Vor EF Core 6.0 haben die generischen Überladungen der HasConversion Methoden den generischen Parameter verwendet, um den Typ anzugeben, in den konvertiert werden soll. Betrachten Sie z. B. ein Currency Enum:

public enum Currency
{
    UsDollars,
    PoundsSterling,
    Euros
}

EF Core kann so konfiguriert werden, dass die Werte dieses Enums als Zeichenfolgen "UsDollars", "PoundsStirling" und "Euros" gespeichert werden, mit HasConversion<string>. 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. Um beispielsweise die Enumerationswerte als 16-Bit-Zahlen in der Datenbank zu speichern:

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

Oder es kann ein benutzerdefinierter Wertkonvertertyp sein. Ziehen Sie beispielsweise einen Konverter in Betracht, 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 mithilfe der generischen HasConversion Methode konfiguriert werden:

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

Weniger Konfiguration für Viele-zu-Viele-Beziehungen

GitHub-Problem: #21535.

Eindeutige m:n-Beziehungen zwischen zwei Entitätstypen werden konventionslos ermittelt. Falls notwendig oder gewünscht, können die Navigationswege explizit angegeben werden. Beispiel:

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

In beiden Fällen erstellt EF Core eine gemeinsame Entitätstypisierung basierend auf Dictionary<string, object>, um als Verknüpfungsentität zwischen den beiden Typen zu fungieren. Ab EF Core 6.0 kann der Konfiguration hinzugefügt werden, UsingEntity um nur diesen Typ zu ändern, ohne dass zusätzliche Konfiguration erforderlich ist. Beispiel:

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

Darüber hinaus kann der Verknüpfungsentitätstyp zusätzlich konfiguriert werden, ohne die links- und rechts-Beziehungen explizit angeben zu müssen. Beispiel:

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

Und 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 Nullwerte konvertieren

GitHub-Problem: #13850.

Wichtig

Aufgrund der unten beschriebenen Probleme wurden die Konstruktoren von ValueConverter, die die Konvertierung von NULL-Werten erlauben, für die Version EF Core 6.0 mit [EntityFrameworkInternal] markiert. Wenn Sie diese Konstruktoren verwenden, wird jetzt eine Warnung während des Builds generiert.

Wertkonverter lassen in der Regel die Konvertierung von NULL in einen anderen Wert nicht zu. Dies liegt daran, dass derselbe Wertkonverter sowohl für nullwerte als auch für nicht nullable Typen verwendet werden kann, was für PK/FK-Kombinationen sehr nützlich ist, bei denen der FK häufig nullfähig ist und die PK nicht.

Ab EF Core 6.0 kann ein Wertkonverter erstellt werden, der Nullwerte konvertiert. Die Validierung dieser Funktion hat jedoch gezeigt, dass sie in der Praxis mit vielen Fallstricken sehr problematisch ist. Beispiel:

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

Ein Beispiel dafür, wo die Konvertierung von Nullen hilfreich sein kann, ist, wenn die Datenbank Nullen enthält, aber der Entitätstyp einen anderen Standardwert für die Eigenschaft verwenden möchte. Ziehen Sie beispielsweise eine Enumeration in Betracht, bei der der Standardwert "Unbekannt" lautet:

public enum Breed
{
    Unknown,
    Burmese,
    Tonkinese
}

Die Datenbank hat jedoch möglicherweise Nullwerte, wenn die Rasse unbekannt ist. In EF Core 6.0 kann ein Wertkonverter verwendet werden, um folgendes 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
    }

Katzen mit einer Rasse von "Unbekannt" haben ihre Breed Spalte in der Datenbank auf NULL 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 });

await context.SaveChangesAsync();

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 an der DbContext-Fabrik

AddDbContextFactory registriert dbContext auch direkt

GitHub-Problem: #25164.

Manchmal ist es nützlich, sowohl einen DbContext-Typ als auch eine Factory für Kontexte dieses Typs zu verwenden, die beide im Container für Abhängigkeitsinjektion (D.I.) der Anwendungen registriert sind. Auf diese Weise kann z. B. eine bereichsbezogene Instanz des DbContext aus dem Anforderungsbereich aufgelöst werden, während die Factory verwendet werden kann, um bei Bedarf mehrere unabhängige Instanzen zu erstellen.

Um dies zu unterstützen, AddDbContextFactory registriert sie nun auch den DbContext-Typ als bereichsbezogenen Dienst. Betrachten Sie diese Registrierung beispielsweise im D.I.-Container der Anwendung:

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

Mit dieser Registrierung kann die Factory vom D.I.-Stammcontainer wie in früheren Versionen 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 Kontextinstanzen, die von der Factory erstellt wurden, explizit entsorgt 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 beendet, wenn der Containerbereich beendet wird. Der Kontext sollte nicht explizit beendet werden.

Auf höherer Ebene bedeutet dies, dass entweder der DbContext der Fabrik in andere D.I.-Typen eingefügt werden kann. Beispiel:

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

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

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

        var results1 = await context1.Blogs.ToListAsync();
        var results2 = await context2.Blogs.ToListAsync();

        // Contexts obtained from the factory must be explicitly disposed
    }
}

Oder:

private class MyController1
{
    private readonly SomeDbContext _context;

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

    public async Task DoSomething()
    {
        var results = await _context.Blogs.ToListAsync();

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

DbContextFactory ignoriert den parameterlosen DbContext-Konstruktor

GitHub-Problem: #24124.

EF Core 6.0 ermöglicht jetzt sowohl einen parameterlosen DbContext-Konstruktor als auch einen Konstruktor, der DbContextOptions verwendet, für denselben Kontexttyp zu verwenden, wenn die Factory über AddDbContextFactory registriert wird. 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; }
}

DbContext-Pooling kann ohne Abhängigkeitsinjektion verwendet werden

GitHub-Problem: #24137.

Der PooledDbContextFactory Typ wurde öffentlich gemacht, damit er als eigenständiger Pool für DbContext-Instanzen verwendet werden kann, ohne dass die Anwendung über einen Abhängigkeitseinfügungscontainer 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;ConnectRetryCount=0")
    .Options;

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

Die Fabrik kann dann zum Erstellen und Poolen von Instanzen verwendet werden. 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 freigegeben werden.

Sonstige Verbesserungen

Und schließlich enthält EF Core mehrere Verbesserungen in bereichen, die oben nicht behandelt werden.

[ColumnAttribute.Order] beim Erstellen von Tabellen verwenden

GitHub-Problem: #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 Primärschlüsselspalten zuerst, gefolgt von Eigenschaften des Entitätstyps und besitzereigenen Typen und schließlich Eigenschaften von Basistypen. Die folgende Tabelle wird beispielsweise auf SQL Server 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 ColumnAttribute können Sie eine andere Spaltenreihenfolge angeben. 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; }
}

Auf SQL Server wird nun die generierte Tabelle wie folgt 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 Spaltenreihenfolgewerte Lücken aufweisen können, sodass Bereiche verwendet werden können, um Spalten immer am Ende zu platzieren, auch 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 OnModelCreatingkonfiguriert 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 Reihenfolge im Modell-Generator mit HasColumnOrder hat Vorrang vor jeder Reihenfolge, die mit ColumnAttribute angegeben ist. Dies bedeutet, dass HasColumnOrder die Sortierung, die mit Attributen erfolgt, außer Kraft setzen kann, einschließlich der Auflösung von Konflikten, wenn Attribute für unterschiedliche Eigenschaften dieselbe Bestellnummer angeben.

Wichtig

Beachten Sie, dass die meisten Datenbanken im Allgemeinen nur das Sortieren von Spalten unterstützen, wenn die Tabelle erstellt wird. Dies bedeutet, dass das Spaltenreihenfolge-Attribut nicht zum Erneuten Anordnen von Spalten in einer vorhandenen Tabelle verwendet werden kann. Eine bemerkenswerte Ausnahme hierfür ist SQLite, bei der Migrationen die gesamte Tabelle mit neuen Spaltenreihenfolgen neu erstellen.

EF Core Minimal API

GitHub-Problem: #25192.

.NET Core 6.0 enthält aktualisierte Vorlagen, die vereinfachte "minimale APIs" enthalten, die viele der in .NET-Anwendungen herkömmlichen Codebausteine entfernen.

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==");

Dies entspricht genau folgendem:

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;ConnectRetryCount=0"));
var builder = WebApplication.CreateBuilder(args);

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

Hinweis

Die Minimal-APIs von EF Core unterstützen nur eine sehr grundlegende Registrierung und Konfiguration eines DbContexts und eines Anbieters. Verwenden Sie AddDbContext, AddDbContextPool, , AddDbContextFactoryusw. für den Zugriff auf alle Arten von Registrierung und Konfiguration, die in EF Core verfügbar sind.

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

Beibehalten des Synchronisierungskontexts in SaveChangesAsync

GitHub-Problem: #23971.

Wir haben den EF Core-Code in der Version 5.0 so geändert , dass er an allen Stellen festgelegt wurde Task.ConfigureAwaitfalse , an denen wir await asynchronen Code haben. Dies ist in der Regel eine bessere Wahl für die EF Core-Nutzung. Dies ist jedoch ein Sonderfall, da EF Core nach Abschluss des asynchronen Datenbankvorgangs generierte Werte in nachverfolgte Entitäten festlegen wird. Diese Änderungen können dann Benachrichtigungen auslösen, die beispielsweise im U.I.-Thread ausgeführt werden müssen. Daher setzen wir diese Änderung in EF Core 6.0 nur für die SaveChangesAsync Methode zurück.

In-Memory-Datenbank: Überprüfen Sie, dass erforderliche Eigenschaften nicht null sind.

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

Die EF Core-In-Memory-Datenbank löst nun eine Ausnahme aus, wenn versucht wird, einen NULL-Wert für eine Eigenschaft zu speichern, die als erforderlich gekennzeichnet ist. Betrachten Sie z. B. einen User Typ mit einer erforderlichen Username Eigenschaft:

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

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

Der Versuch, eine Entität mit null Username zu speichern, führt zu der folgenden Ausnahme:

Microsoft.EntityFrameworkCore.DbUpdateException: Erforderliche Eigenschaften '{'Username'}' fehlen für die Instanz des Entitätstyps 'User' mit dem Schlüsselwert '{ID: 1}'.

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

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

Informationen über Befehlsquellen für Diagnose- und Abfangvorrichtungen

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

Die CommandEventData bereitgestellten Diagnosequellen und Interceptors enthalten nun einen Enumerationswert, der angibt, welcher Teil von EF für das Erstellen des Befehls verantwortlich war. Dies kann als Filter in der Diagnose oder im Interceptor verwendet werden. Beispielsweise möchten wir einen Interceptor, 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 so gefiltert, dass er ausschließlich SaveChanges-Ereignisse behandelt, wenn er in einer Anwendung verwendet wird, die auch 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 Behandlung temporärer Werte

GitHub-Problem: #24245.

EF Core macht temporäre Werte für Entitätstypinstanzen nicht verfügbar. Betrachten Sie z. B. einen Blog Entitätstyp mit einem vom Store generierten Schlüssel:

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

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

Die Id Schlüsseleigenschaft erhält einen temporären Wert, sobald ein Blog Wert vom Kontext nachverfolgt wird. Zum Beispiel, wenn DbContext.Add aufgerufen wurde:

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. Beispiel für diesen 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

Dies ist gut, da dadurch verhindert wird, dass der temporäre Wert in Anwendungscode verloren geht, in dem er versehentlich als nicht temporär behandelt werden kann. Manchmal ist es jedoch hilfreich, temporäre Werte direkt zu behandeln. Beispielsweise kann eine Anwendung eigene temporäre Werte für ein Diagramm von Entitäten generieren, bevor sie nachverfolgt werden, damit sie zum Bilden von Beziehungen mithilfe von Fremdschlüsseln verwendet werden können. Dies kann durch explizites Markieren der Werte als temporär erfolgen. 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 in 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. Dies kann verwendet werden, um explizit Beziehungen zwischen neuen Entitäten mithilfe ihrer temporären Schlüsselwerte 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 mit Anmerkungen für C#-Nullwerte-Referenztypen

GitHub-Problem: #19007.

Die EF Core-Codebasis verwendet jetzt C#-Nullwerte-Referenztypen (NRTs). Dies bedeutet, dass Sie die richtigen Compileranzeigen 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 unten gezeigten Beispiele ausführen und debuggen, indem Sie den Beispielcode von GitHub herunterladen.

Verbindungspooling

GitHub-Problem: #13837.

Es ist üblich, Datenbankverbindungen so wenig wie möglich offen zu halten. Dies hilft, Konflikte bei der Nutzung der Verbindungsressource zu vermeiden. Aus diesem Grund öffnen Bibliotheken wie EF Core die Verbindung unmittelbar vor dem Ausführen eines Datenbankvorgangs, und schließen Sie sie unmittelbar danach erneut. Betrachten Sie z. B. diesen EF Core-Code:

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

var users = await context.Users.ToListAsync();

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();

        await context.SaveChangesAsync();

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

Die Ausgabe dieses Codes mit aktivierter Protokollierung für Verbindungen lautet:

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 teurer Vorgang. Daher erstellen die meisten ADO.NET Anbieter einen Pool physischer Verbindungen und vermieten sie bei Bedarf an DbConnection Instanzen.

SQLite unterscheidet sich etwas, da der Datenbankzugriff in der Regel nur auf eine Datei zugreift. Dies bedeutet, dass das Öffnen einer Verbindung mit einer SQLite-Datenbank in der Regel sehr schnell ist. Dies ist jedoch nicht immer der Fall. Das Öffnen einer Verbindung mit einer verschlüsselten Datenbank kann z. B. sehr langsam sein. Daher werden SQLite-Verbindungen jetzt bei Verwendung von Microsoft.Data.Sqlite 6.0 zusammengefasst.

Unterstützung für DateOnly und TimeOnly

GitHub-Problem: #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 immer bei SQLite bedeutet das systemeigene Typsystem, dass die Werte dieser Typen als einer der vier unterstützten Typen gespeichert werden müssen. Microsoft.Data.Sqlite speichert sie als TEXT. Beispiel: eine Entität mit den folgenden Typen:

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

    public DateOnly Birthday { get; set; }
    public TimeOnly TokensRenewed { get; set; }
}

Entspricht der 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 dann normal gespeichert, abgefragt und aktualisiert werden. Beispiel: Diese EF Core LINQ-Abfrage:

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

Wird in folgendes auf SQLite übersetzt:

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

Und gibt nur Verwendungen mit Geburtstagen vor 1900 CE zurück:

Found 'ajcvickers'
Found 'wendy'

Savepoints-API

GitHub-Problem: #20228.

Wir haben eine allgemeine API für Speicherpunkte in ADO.NET Anbietern standardisiert. Microsoft.Data.Sqlite unterstützt jetzt diese API, einschließlich:

Durch die Verwendung eines Speicherpunkts kann ein Teil einer Transaktion rückgängig gemacht werden, ohne die gesamte Transaktion zurückgesetzt zu müssen. Beispiel: Der folgende Code:

  • Erstellt eine Transaktion
  • Sendet eine Aktualisierung an die Datenbank.
  • Erstellt einen Speicherpunkt.
  • Sendet eine weitere Aktualisierung an die Datenbank.
  • Rollback zum zuvor erstellten Speicherpunkt
  • Führt die Transaktion aus
using var connection = new SqliteConnection("Command Timeout=60;DataSource=test.db");
await connection.OpenAsync();

await using var transaction = await connection.BeginTransactionAsync();

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

await transaction.SaveAsync("MySavepoint");

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

await transaction.RollbackAsync("MySavepoint");

await transaction.CommitAsync();

Dies führt dazu, dass das erste Update der Datenbank zugesichert wird, während das zweite Update nicht zugesichert wird, da der Speicherpunkt zurückgerollt wurde, bevor die Transaktion committet wurde.

Befehlstimeout in der Verbindungszeichenfolge

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

ADO.NET Anbieter unterstützen zwei unterschiedliche Timeouts:

  • Das Verbindungstimeout, das die maximale Wartezeit bestimmt, wenn eine Verbindung mit der Datenbank hergestellt wird.
  • Das Befehlstimeout legt fest, wie lange maximal auf die Fertigstellung eines Befehls gewartet wird.

Das Befehlstimeout kann über Code festgelegt werden.DbCommand.CommandTimeout Viele Anbieter stellen jetzt auch dieses Befehlstimeout in der Verbindungszeichenfolge offen. Microsoft.Data.Sqlite folgt diesem Trend mit dem Command Timeout Schlüsselwort "Verbindungszeichenfolge". Beispielsweise wird "Command Timeout=60;DataSource=test.db" 60 Sekunden als Standardtimeout für Befehle verwenden, die von der Verbindung erstellt wurden.

Tipp

Sqlite behandelt Default Timeout als Synonym für Command Timeout und kann stattdessen verwendet werden, wenn dies bevorzugt wird.


Zusätzliche Ressourcen