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.
Dieses Dokument behandelt verschiedene Features und Szenarien, die die Änderungsnachverfolgung betreffen.
Tipp
In diesem Dokument wird davon ausgegangen, dass Entitätszustände und die Grundlagen der EF Core-Änderungsnachverfolgung verstanden werden. Weitere Informationen zu diesen Themen finden Sie unter Change Tracking in EF Core .
Tipp
Sie können den gesamten Code in diesem Dokument ausführen und debuggen, indem Sie den Beispielcode von GitHub herunterladen.
Add
im Vergleich zu AddAsync
Entity Framework Core (EF Core) stellt asynchrone Methoden bereit, wenn diese Methode verwendet wird, was zu einer Datenbankinteraktion führen kann. Synchrone Methoden werden auch bereitgestellt, um mehr Aufwand zu vermeiden, wenn Datenbanken verwendet werden, die keinen asynchronen Zugriff mit hoher Leistung unterstützen.
DbContext.Add und DbSet<TEntity>.Add greifen normalerweise nicht auf die Datenbank zu, da diese Methoden inhärent nur die Nachverfolgung von Entitäten starten. Einige Formen der Wertgenerierung können jedoch auf die Datenbank zugreifen, um einen Schlüsselwert zu generieren. Der einzige Wertgenerator, der dies tut und mit EF Core ausgeliefert wird, ist HiLoValueGenerator<TValue>. Die Verwendung dieses Generators ist ungewöhnlich; sie ist nie standardmäßig konfiguriert. Das bedeutet, dass die meisten Anwendungen Add
verwenden sollten und nicht AddAsync
.
Andere ähnliche Methoden wie Update
, Attach
, und Remove
verfügen nicht über asynchrone Überladungen, da sie nie neue Schlüsselwerte generieren und daher niemals auf die Datenbank zugreifen müssen.
AddRange
, UpdateRange
, AttachRange
und RemoveRange
DbSet<TEntity> und DbContext bieten alternative Versionen von Add
, Update
, , Attach
und Remove
die mehrere Instanzen in einem einzigen Aufruf akzeptieren. Diese Methoden sind AddRange, UpdateRange, AttachRangeund RemoveRange jeweils.
Diese Methoden werden als Komfort bereitgestellt. Die Verwendung einer "Range"-Methode hat dieselbe Funktionalität wie mehrere Aufrufe der entsprechenden Nicht-"Range"-Methode. Es gibt keinen signifikanten Leistungsunterschied zwischen den beiden Ansätzen.
Hinweis
Dies unterscheidet sich von EF6, wo sowohl AddRange
als auch Add
automatisch DetectChanges
aufriefen, jedoch führte das mehrfache Aufrufen von Add
dazu, dass DetectChanges mehrfach statt einmal aufgerufen wurde. Dies hat AddRange
in EF6 effizienter gemacht. In EF Core rufen keine dieser Methoden automatisch auf DetectChanges
.
DbContext- und DbSet-Methoden
Viele Methoden, einschließlich Add
, Update
, Attach
und Remove
, haben Implementierungen für beide DbSet<TEntity> und DbContext. Diese Methoden weisen genau das gleiche Verhalten für normale Entitätstypen auf. Dies liegt daran, dass der CLR-Typ der Entität einem und nur einem Entitätstyp im EF Core-Modell zugeordnet ist. Daher definiert der CLR-Typ vollständig, wo die Entität in das Modell passt, und so kann das zu verwendende DbSet implizit bestimmt werden.
Die Ausnahme dieser Regel ist die Verwendung von Shared-Type-Entitätstypen, die in erster Linie für Viele-zu-Viele-Verknüpfungsentitäten verwendet werden. Bei Verwendung eines Entitätstyps vom Typ "Shared-Type" muss zunächst ein DbSet für den verwendeten EF Core-Modelltyp erstellt werden. Methoden wie Add
, Update
, Attach
und Remove
können dann ohne jegliche Mehrdeutigkeit auf das DbSet angewendet werden, um den spezifischen EF Core-Modelltyp zu verwenden.
Gemeinsame Entitätstypen werden standardmäßig für die Verknüpfungsentitäten in m:n-Beziehungen verwendet. Ein gemeinsam genutzter Entitätstyp kann auch explizit für die Verwendung in einer m:n-Beziehung konfiguriert werden. Der folgende Code konfiguriert Dictionary<string, int>
als Verknüpfungsentitätstyp:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.SharedTypeEntity<Dictionary<string, int>>(
"PostTag",
b =>
{
b.IndexerProperty<int>("TagId");
b.IndexerProperty<int>("PostId");
});
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<Dictionary<string, int>>(
"PostTag",
j => j.HasOne<Tag>().WithMany(),
j => j.HasOne<Post>().WithMany());
}
Das Ändern von Fremdschlüsseln und Navigationen zeigt, wie zwei Entitäten zugeordnet werden, indem eine neue Join-Entitätsinstanz nachverfolgt wird. Der folgende Code führt dies für den Dictionary<string, int>
Shared-Type Entitätstyp aus, der für die Verknüpfungsentität verwendet wird.
using var context = new BlogsContext();
var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);
var joinEntitySet = context.Set<Dictionary<string, int>>("PostTag");
var joinEntity = new Dictionary<string, int> { ["PostId"] = post.Id, ["TagId"] = tag.Id };
joinEntitySet.Add(joinEntity);
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
await context.SaveChangesAsync();
Beachten Sie, dass DbContext.Set<TEntity>(String) zum Erstellen eines DbSet für den Entitätstyp PostTag
verwendet wird. Dieses DbSet kann dann verwendet werden, um Add
mit der neuen Join-Entitätsinstanz aufzurufen.
Von Bedeutung
Der CLR-Typ, der für die Verknüpfung von Entitätstypen nach Konvention verwendet wird, kann sich in zukünftigen Versionen ändern, um die Leistung zu verbessern. Hängen Sie nicht von einem bestimmten Verknüpfungsentitätstyp ab, es sei denn, sie wurde explizit wie Dictionary<string, int>
im obigen Code konfiguriert.
Property versus Feldzugriff
Der Zugriff auf Entitätseigenschaften verwendet standardmäßig das Hintergrundfeld der Eigenschaft. Dies ist effizient und verhindert, dass Nebenwirkungen von Aufrufen von Eigenschaften-Getters und Settern ausgelöst werden. So ist beispielsweise das Lazy-Loading in der Lage, das Auslösen von Endlosschleifen zu vermeiden. Für weitere Informationen zur Konfiguration von Rückabdeckungsfeldern im Modell, siehe Rückabdeckungsfelder.
Manchmal kann es wünschenswert sein, dass EF Core Nebenwirkungen generiert, wenn sie Eigenschaftswerte ändert. Wenn beispielsweise Datenbindungen an Entitäten durchgeführt werden, kann das Festlegen einer Eigenschaft Benachrichtigungen an die Benutzeroberfläche auslösen, die nicht auftreten, wenn das Feld direkt festgelegt wird. Dies kann erreicht werden, indem Sie das PropertyAccessMode wie folgt ändern:
- Alle Entitätstypen im Modell mit ModelBuilder.UsePropertyAccessMode
- Alle Eigenschaften und Navigationsmöglichkeiten eines bestimmten Entitätstyps mit EntityTypeBuilder<TEntity>.UsePropertyAccessMode
- Eine bestimmte Eigenschaft mit PropertyBuilder.UsePropertyAccessMode
- Eine bestimmte Navigation mithilfe von NavigationBuilder.UsePropertyAccessMode
Eigenschaftszugriffsmodi Field
und PreferField
führen dazu, dass EF Core über das Sicherungsfeld auf den Eigenschaftswert zugreift. Ebenso werden Property
und PreferProperty
EF Core veranlassen, auf den Eigenschaftswert über dessen Getter und Setter zuzugreifen.
Wenn Field
oder Property
verwendet werden und EF Core nicht über das Feld oder den Eigenschaften-Getter/Setter auf den Wert zugreifen kann, löst EF Core eine Ausnahme aus. Dadurch wird sichergestellt, dass EF Core immer den Feld-/Eigenschaftszugriff verwendet, wenn Sie es erwarten.
Andererseits fällt der PreferField
-Modus und der PreferProperty
-Modus auf die Verwendung der Eigenschaft und/oder des Sicherungsfelds zurück, wenn es nicht möglich ist, den bevorzugten Zugriff zu verwenden.
PreferField
ist die Standardeinstellung. Dies bedeutet, dass EF Core Felder verwendet, wann immer es möglich ist, aber nicht fehlschlägt, wenn stattdessen über den Getter oder Setter auf eine Eigenschaft zugegriffen werden muss.
FieldDuringConstruction
und PreferFieldDuringConstruction
konfigurieren EF Core für die Verwendung von Backing Fields nur beim Erstellen von Entitätsinstanzen. Dadurch können Abfragen ohne Getter- und Setter-Nebenwirkungen ausgeführt werden, während spätere Eigenschaftenänderungen durch EF Core diese Nebenwirkungen verursachen.
Die verschiedenen Eigenschaftenzugriffsmodi sind in der folgenden Tabelle zusammengefasst:
PropertyAccessMode | Vorliebe | Einstellung zum Erstellen von Entitäten | Ausweichplan | Fallback beim Erstellen von Entitäten |
---|---|---|---|---|
Field |
Feld | Feld | Würfe | Würfe |
Property |
Eigentum | Eigentum | Würfe | Würfe |
PreferField |
Feld | Feld | Eigentum | Eigentum |
PreferProperty |
Eigentum | Eigentum | Feld | Feld |
FieldDuringConstruction |
Eigentum | Feld | Feld | Würfe |
PreferFieldDuringConstruction |
Eigentum | Feld | Feld | Eigentum |
Temporäre Werte
EF Core erstellt temporäre Schlüsselwerte beim Nachverfolgen neuer Entitäten, die echte Schlüsselwerte aufweisen, die von der Datenbank generiert werden, wenn SaveChanges aufgerufen wird. Eine Übersicht darüber, wie diese temporären Werte verwendet werden, finden Sie unter Change Tracking in EF Core .
Zugreifen auf temporäre Werte
Temporäre Werte werden im Änderungstracker gespeichert und nicht direkt auf Entitätsinstanzen festgelegt. Diese temporären Werte werden jedoch bei Verwendung der verschiedenen Mechanismen für den Zugriff auf nachverfolgte Entitäten verfügbar gemacht. Der folgende Code greift beispielsweise auf einen temporären Wert zu:EntityEntry.CurrentValues
using var context = new BlogsContext();
var blog = new Blog { Name = ".NET Blog" };
context.Add(blog);
Console.WriteLine($"Blog.Id set on entity is {blog.Id}");
Console.WriteLine($"Blog.Id tracked by EF is {context.Entry(blog).Property(e => e.Id).CurrentValue}");
Die Ausgabe aus diesem Code lautet:
Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643
PropertyEntry.IsTemporary kann verwendet werden, um auf temporäre Werte zu überprüfen.
Bearbeiten temporärer Werte
Es ist manchmal nützlich, explizit mit temporären Werten zu arbeiten. Beispielsweise kann eine Sammlung neuer Entitäten auf einem Webclient erstellt und dann wieder auf den Server serialisiert werden. Fremdschlüsselwerte sind eine Möglichkeit zum Einrichten von Beziehungen zwischen diesen Entitäten. Im folgenden Code wird dieser Ansatz verwendet, um ein Diagramm neuer Entitäten nach Fremdschlüssel zuzuordnen, während beim Aufrufen von SaveChanges weiterhin echte Schlüsselwerte generiert werden können.
var blogs = new List<Blog> { new Blog { Id = -1, Name = ".NET Blog" }, new Blog { Id = -2, Name = "Visual Studio Blog" } };
var posts = new List<Post>
{
new Post
{
Id = -1,
BlogId = -1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = -2,
BlogId = -2,
Title = "Disassembly improvements for optimized managed debugging",
Content = "If you are focused on squeezing out the last bits of performance for your .NET service or..."
}
};
using var context = new BlogsContext();
foreach (var blog in blogs)
{
context.Add(blog).Property(e => e.Id).IsTemporary = true;
}
foreach (var post in posts)
{
context.Add(post).Property(e => e.Id).IsTemporary = true;
}
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
await context.SaveChangesAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Beachten Sie Folgendes:
- Negative Zahlen werden als temporäre Schlüsselwerte verwendet; dies ist nicht erforderlich, sondern eine gemeinsame Konvention, um wichtige Konflikte zu verhindern.
- Die
Post.BlogId
FK-Eigenschaft erhält denselben negativen Wert wie die PK des zugeordneten Blogs. - Die PK-Werte werden durch Festlegen IsTemporary als temporär gekennzeichnet, nachdem jede Entität nachverfolgt wurde. Dies ist erforderlich, da jeder schlüsselwert, der von der Anwendung bereitgestellt wird, als realer Schlüsselwert angenommen wird.
Wenn Sie sich die Debugansicht der Änderungsverfolgung vor dem Aufrufen von SaveChanges ansehen, wird gezeigt, dass die PK-Werte als temporäre markiert sind und Beiträge den richtigen Blogs zugeordnet sind, einschließlich der Korrektur von Navigationen:
Blog {Id: -2} Added
Id: -2 PK Temporary
Name: 'Visual Studio Blog'
Posts: [{Id: -2}]
Blog {Id: -1} Added
Id: -1 PK Temporary
Name: '.NET Blog'
Posts: [{Id: -1}]
Post {Id: -2} Added
Id: -2 PK Temporary
BlogId: -2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: -2}
Tags: []
Post {Id: -1} Added
Id: -1 PK Temporary
BlogId: -1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: -1}
Nach dem Aufrufen SaveChangeswurden diese temporären Werte durch echte Werte ersetzt, die von der Datenbank generiert werden:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}]
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Posts: [{Id: 2}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Tags: []
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: 2}
Tags: []
Arbeiten mit Standardwerten
EF Core ermöglicht es einer Eigenschaft, den Standardwert aus der Datenbank abzurufen, wenn SaveChanges sie aufgerufen wird. Wie bei generierten Schlüsselwerten verwendet EF Core nur einen Standardwert aus der Datenbank, wenn kein Wert explizit festgelegt wurde. Betrachten Sie beispielsweise den folgenden Entitätstyp:
public class Token
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime ValidFrom { get; set; }
}
Die ValidFrom
Eigenschaft ist so konfiguriert, dass ein Standardwert aus der Datenbank abgerufen wird:
modelBuilder
.Entity<Token>()
.Property(e => e.ValidFrom)
.HasDefaultValueSql("CURRENT_TIMESTAMP");
Beim Einfügen einer Entität dieses Typs lässt EF Core die Datenbank den Wert generieren, es sei denn, es wurde stattdessen ein expliziter Wert festgelegt. Beispiel:
using var context = new BlogsContext();
context.AddRange(
new Token { Name = "A" },
new Token { Name = "B", ValidFrom = new DateTime(1111, 11, 11, 11, 11, 11) });
await context.SaveChangesAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
In der Debugansicht der Änderungsüberwachung wird gezeigt, dass das erste Token von der Datenbank generiert wurde ValidFrom
, während das zweite Token den Wert explizit festgelegt hat:
Token {Id: 1} Unchanged
Id: 1 PK
Name: 'A'
ValidFrom: '12/30/2020 6:36:06 PM'
Token {Id: 2} Unchanged
Id: 2 PK
Name: 'B'
ValidFrom: '11/11/1111 11:11:11 AM'
Hinweis
Für die Verwendung von Datenbankstandardwerten muss für die Datenbankspalte eine Standardwerteinschränkung konfiguriert sein. Dies erfolgt automatisch durch EF Core-Migrationen bei der Verwendung von HasDefaultValueSql oder HasDefaultValue. Stellen Sie sicher, dass Sie die Standardeinschränkung für die Spalte auf andere Weise erstellen, wenn Sie **nicht** EF Core-Migrationen verwenden.
Verwenden von nullablen Eigenschaften
EF Core kann ermitteln, ob eine Eigenschaft festgelegt wurde, indem der Eigenschaftswert mit dem CLR-Standardwert für diesen Typ verglichen wird. Dies funktioniert in den meisten Fällen gut, bedeutet jedoch, dass der CLR-Standardwert nicht explizit in die Datenbank eingefügt werden kann. Betrachten Sie beispielsweise eine Entität mit einer ganzzahligen Eigenschaft:
public class Foo1
{
public int Id { get; set; }
public int Count { get; set; }
}
Wenn diese Eigenschaft so konfiguriert ist, dass sie auf einen Datenbankstandardwert von -1 eingestellt ist:
modelBuilder
.Entity<Foo1>()
.Property(e => e.Count)
.HasDefaultValue(-1);
Die Absicht besteht darin, dass der Standardwert von -1 verwendet wird, wenn kein expliziter Wert festgelegt wird. Das Festlegen des Werts auf 0 (der Standardwert für CLR-Ganzzahlen) ist für EF Core jedoch nicht von dem Nicht-Festlegen eines Werts zu unterscheiden. Dies bedeutet, dass es nicht möglich ist, für diese Eigenschaft 0 einzufügen. Beispiel:
using var context = new BlogsContext();
var fooA = new Foo1 { Count = 10 };
var fooB = new Foo1 { Count = 0 };
var fooC = new Foo1();
context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);
Beachten Sie, dass die Instanz, bei der Count
explizit auf 0 festgelegt wurde, weiterhin den Standardwert aus der Datenbank erhält, was nicht in unserem Sinne war. Eine einfache Möglichkeit, dies zu bewältigen, besteht darin, die Count
Eigenschaft null zu machen:
public class Foo2
{
public int Id { get; set; }
public int? Count { get; set; }
}
Dadurch wird der CLR-Standardwert Null anstelle von 0, das bedeutet, dass jetzt 0 eingefügt wird, wenn es explizit festgelegt ist.
using var context = new BlogsContext();
var fooA = new Foo2 { Count = 10 };
var fooB = new Foo2 { Count = 0 };
var fooC = new Foo2();
context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
Verwendung von nullablen Rückfeldern
Das Problem beim Nullfähig-Machen der Eigenschaft ist, dass sie im Domänenmodell möglicherweise nicht konzeptuell nullfähig sein sollte. Das Erzwingen, dass die Eigenschaft auf null gesetzt wird, kompromittiert somit das Modell.
Die Eigenschaft kann als nicht-nullfähig belassen werden, wobei nur das unterstützende Feld nullfähig ist. Beispiel:
public class Foo3
{
public int Id { get; set; }
private int? _count;
public int Count
{
get => _count ?? -1;
set => _count = value;
}
}
Dadurch kann der CLR-Standardwert (0) eingefügt werden, wenn die Eigenschaft explizit auf 0 gesetzt wird, ohne die Eigenschaft im Domänenmodell als nullfähig darstellen zu müssen. Beispiel:
using var context = new BlogsContext();
var fooA = new Foo3 { Count = 10 };
var fooB = new Foo3 { Count = 0 };
var fooC = new Foo3();
context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
Nullfähige Sicherungsfelder für Boolean-Eigenschaften
Dieses Muster ist besonders nützlich, wenn Bool-Eigenschaften mit vom Store generierten Standardwerten verwendet werden. Da die CLR-Standardeinstellung " bool
false" lautet, bedeutet dies, dass "false" nicht explizit mithilfe des normalen Musters eingefügt werden kann. Ein Beispiel dafür ist folgender User
-Entitätstyp:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
private bool? _isAuthorized;
public bool IsAuthorized
{
get => _isAuthorized ?? true;
set => _isAuthorized = value;
}
}
Die IsAuthorized
Eigenschaft ist mit einem Datenbankstandardwert von "true" konfiguriert:
modelBuilder
.Entity<User>()
.Property(e => e.IsAuthorized)
.HasDefaultValue(true);
Die IsAuthorized
Eigenschaft kann vor dem Einfügen explizit auf "true" oder "false" festgelegt werden, oder sie kann nicht festgelegt werden, in diesem Fall wird der Datenbankstandard verwendet:
using var context = new BlogsContext();
var userA = new User { Name = "Mac" };
var userB = new User { Name = "Alice", IsAuthorized = true };
var userC = new User { Name = "Baxter", IsAuthorized = false }; // Always deny Baxter access!
context.AddRange(userA, userB, userC);
await context.SaveChangesAsync();
Die Ausgabe von SaveChanges bei Verwendung von SQLite zeigt, dass der Standardwert der Datenbank für Mac verwendet wird, während für Alice und Baxter explizit Werte festgelegt werden.
-- Executed DbCommand (0ms) [Parameters=[@p0='Mac' (Size = 3)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("Name")
VALUES (@p0);
SELECT "Id", "IsAuthorized"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p0='True' (DbType = String), @p1='Alice' (Size = 5)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p0='False' (DbType = String), @p1='Baxter' (Size = 6)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
Nur Schemastandardwerte
Manchmal ist es nützlich, Standardeinstellungen im Datenbankschema zu haben, das von EF Core-Migrationen erstellt wurde, ohne EF Core jemals diese Werte für Einfügungen zu verwenden. Dies kann erreicht werden, indem die Eigenschaft wie folgt PropertyBuilder.ValueGeneratedNever konfiguriert wird:
modelBuilder
.Entity<Bar>()
.Property(e => e.Count)
.HasDefaultValue(-1)
.ValueGeneratedNever();