Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
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.
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
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]))');
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.
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 wieTemporalFromTo
, 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
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
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.
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.
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");
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.");
});
}
Für kompilierte Modelle gelten einige Einschränkungen:
- Globale Abfragefilter werden nicht unterstützt.
- Proxys für verzögertes Laden und Änderungsnachverfolgung werden nicht unterstützt.
- Benutzerdefinierte IModelCacheKeyFactory-Implementierungen werden nicht unterstützt. Sie können jedoch mehrere Modelle kompilieren und das entsprechende Modell nach Bedarf laden.
- Das Modell muss manuell synchronisiert werden, indem es jedes Mal neu generiert wird, wenn sich die Modelldefinition oder die Konfiguration ändert.
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.
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.
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.
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.
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.
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.
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.
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
, , RTRIM
TRIM
, UPPER
und 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()))
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
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"]
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'
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);
});
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);
});
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.
EF Core 6.0 enthält mehrere Verbesserungen beim Reverse Engineering eines EF-Modells aus einer vorhandenen Datenbank.
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 Tags
und 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");
});
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!;
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; }
}
EF Core 6.0 enthält mehrere Verbesserungen bei der Übersetzung und Ausführung von LINQ-Abfragen.
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; }
}
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
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')
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.
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)
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''))
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();
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'
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]
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]
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%')
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 Address
Klasse 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:
- 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.
- Stellen Sie sicher, dass die abhängige mindestens eine erforderliche Eigenschaft enthält, wie oben beschrieben.
- 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".
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.
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.
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]));
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.
Neben neuen Zuordnungsattributen enthält EF Core 6.0 mehrere weitere Verbesserungen am Modellbauprozess.
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.
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>();
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 }));
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:
- Bei der Wertkonvertierung in NULL im Speicher werden ungültige Abfragen generiert.
- Bei der Wertkonvertierung von NULL im Speicher werden ungültige Abfragen generiert.
- Wertkonverter behandeln keine Fälle, in denen die Datenbankspalte mehrere unterschiedliche Werte enthält, die in denselben Wert konvertiert werden
- Zulassen, dass Wertkonverter die Nullierbarkeit von Spalten ändern
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();
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
}
}
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; }
}
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.
Und schließlich enthält EF Core mehrere Verbesserungen in bereichen, die oben nicht behandelt werden.
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 OnModelCreating
konfiguriert werden. Beispiel:
modelBuilder.Entity<UsingModelBuilder.Employee>(
entityBuilder =>
{
entityBuilder.Property(e => e.Id).HasColumnOrder(1);
entityBuilder.Property(e => e.FirstName).HasColumnOrder(2);
entityBuilder.Property(e => e.LastName).HasColumnOrder(3);
entityBuilder.OwnsOne(
e => e.Address,
ownedBuilder =>
{
ownedBuilder.Property(e => e.House).HasColumnName("House").HasColumnOrder(4);
ownedBuilder.Property(e => e.Street).HasColumnName("Street").HasColumnOrder(5);
ownedBuilder.Property(e => e.City).HasColumnName("City").HasColumnOrder(6);
ownedBuilder.Property(e => e.Postcode).HasColumnName("Postcode").HasColumnOrder(7).IsRequired();
});
entityBuilder.Property(e => e.Department).HasColumnOrder(8);
entityBuilder.Property(e => e.AnnualSalary).HasColumnOrder(9);
entityBuilder.Property(e => e.UpdatedOn).HasColumnOrder(10);
entityBuilder.Property(e => e.CreatedOn).HasColumnOrder(11);
});
Die 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.
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
, , AddDbContextFactory
usw. 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:
- Die Präsentation Minimal APIs in .NET 6 von Maria Naggaga
- Ein Playground für ein Todo-Beispiel mit .NET 6 Minimal API auf Scott Hanselmans Blog
- Gist Minimal APIs auf einen Blick von David Fowler
- Ein minimaler API-Playground von Damian Edwards auf GitHub
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.
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));
}
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();
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
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.
Tipp
Sie können alle unten gezeigten Beispiele ausführen und debuggen, indem Sie den Beispielcode von GitHub herunterladen.
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.
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'
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:
- Save(String) zum Erstellen eines Speicherpunkts in der Transaktion
- Rollback(String) um ein Rollback auf einen vorherigen Speicherpunkt zurückzuführen
- Release(String) um einen Speicherpunkt freizugeben
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.
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.
Feedback zu .NET
.NET ist ein Open Source-Projekt. Wählen Sie einen Link aus, um Feedback zu geben: