Zusätzliche Features für die Änderungsnachverfolgung
In diesem Dokument werden verschiedene Features und Szenarien im Zusammenhang mit der Änderungsnachverfolgung behandelt.
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 Änderungsnachverfolgung in EF Core.
Tipp
Sie können den gesamten Code in dieser Dokumentation ausführen und debuggen, indem Sie den Beispielcode von GitHub herunterladen.
Add
im Vergleich mit AddAsync
Entity Framework Core (EF Core) stellt asynchrone Methoden bereit, deren Verwendung zu Datenbankinteraktionen führen kann. Synchrone Methoden werden ebenfalls bereitgestellt, um Mehraufwand zu vermeiden, wenn die verwendeten Datenbanken keinen asynchronen Hochleistungszugriff 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. Mit EF Core wird nur ein solcher Wertgenerator ausgeliefert: HiLoValueGenerator<TValue>. Dieser Generator wird nur selten verwendet und niemals standardmäßig konfiguriert. Dies bedeutet, dass die überwiegende Mehrheit der Anwendungen Add
verwenden sollten, 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, AttachRange und RemoveRange.
Sie werden aus Gründen der Benutzerfreundlichkeit bereitgestellt. Eine „Range“-Methode bietet dieselbe Funktionalität wie mehrere Aufrufe der entsprechenden Methode ohne Bereich. Es gibt keinen signifikanten Leistungsunterschied zwischen beiden Ansätzen.
Hinweis
Dies stellt einen Unterschied zu EF6 dar, bei dem AddRange
und Add
automatisch DetectChanges
aufrufen, aber Mehrfachaufrufe von Add
dazu führen, dass DetectChanges mehrmals statt einmal aufgerufen wird. Dadurch war AddRange
in EF6 effizienter. In EF Core ruft kein dieser Methoden automatisch DetectChanges
auf.
Vergleich der Methoden DbContext und DbSet
Viele Methoden, einschließlich Add
, Update
, Attach
und Remove
, haben Implementierungen für DbSet<TEntity> und DbContext. Diese Methoden weisen genau dasselbe Verhalten für normale Entitätstypen auf. Dies liegt daran, dass der CLR-Typ der Entität genau einem Entitätstyp im EF Core-Modell zugeordnet ist. Daher definiert der CLR-Typ vollständig, wo die Entität in das Modell passt, sodass das zu verwendende DbSet implizit bestimmt werden kann.
Eine Ausnahme zu dieser Regel bildet die Verwendung von freigegebener Entitätstypen, die in erster Linie für m:n-Verknüpfungsentitäten verwendet werden. Bei Verwendung eines freigegebenen Entitätstyps muss zunächst ein DbSet für den verwendeten EF Core-Modelltyp erstellt werden. Methoden wie Add
, Update
, Attach
und Remove
können dann für das DbSet ohne Mehrdeutigkeit beim verwendeten EF Core-Modelltyp verwendet werden.
Freigegebene Entitätstypen werden standardmäßig für Verknüpfungsentitäten in m:n-Beziehungen verwendet. Ein freigegebener Entitätstyp kann auch explizit für die Verwendung in einer m:n-Beziehung konfiguriert werden. Der folgende Code konfiguriert z. B. 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());
}
Unter Ändern von Fremdschlüsseln und Navigationen wird gezeigt, wie zwei Entitäten zugeordnet werden, indem eine neue Verknüpfungsentitätsinstanz nachverfolgt wird. Im folgende Code wird dies für den freigegebenen Entitätstyp Dictionary<string, int>
ausgeführt, der für die Verknüpfungsentität verwendet wird:
using var context = new BlogsContext();
var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(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);
context.SaveChanges();
Beachten Sie, dass DbContext.Set<TEntity>(String) zum Erstellen eines DbSet für den Entitätstyp PostTag
verwendet wird. Dieses DbSet kann dann zum Aufrufen von Add
mit der neuen Verknüpfungsentitätsinstanz verwendet werden.
Wichtig
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. Verlassen Sie sich nicht auf einen bestimmten Verknüpfungsentitätstyp, es sei denn, er wurde explizit wie für Dictionary<string, int>
im obigen Code konfiguriert.
Eigenschaften- und Feldzugriff
Beim Zugriff auf Entitätseigenschaften wird standardmäßig das Unterstützungsfeld der Eigenschaft verwendet. Dieses Vorgehen ist effizient und verhindert Nebenwirkungen durch Aufrufe der Getter und Setter von Eigenschaften. So kann beispielsweise durch verzögertes Laden das Auslösen von Endlosschleifen vermieden werden. Weitere Informationen zum Konfigurieren von Unterstützungsfeldern im Modell finden Sie unter Unterstützungsfelder.
Manchmal kann es wünschenswert sein, dass EF Core Nebenwirkungen generiert, wenn Eigenschaftswerte geändert werden. Wenn beispielsweise Datenbindungen an Entitäten festgelegt werden, generiert das Festlegen einer Eigenschaft möglicherweise Benachrichtigungen an die Benutzeroberfläche, die beim direkten Festlegen des Felds nicht erfolgen. Dies kann erreicht werden, indem Sie PropertyAccessMode für Folgendes ändern:
- Alle Entitätstypen im Modell mit ModelBuilder.UsePropertyAccessMode
- Alle Eigenschaften und Navigationen eines bestimmten Entitätstyps mit EntityTypeBuilder<TEntity>.UsePropertyAccessMode
- Eine bestimmte Eigenschaft mit PropertyBuilder.UsePropertyAccessMode
- Eine bestimmte Navigation mit NavigationBuilder.UsePropertyAccessMode
Die Eigenschaftszugriffsmodi Field
und PreferField
führen dazu, dass EF Core über das Unterstützungsfeld auf den Eigenschaftswert zugreift. Ebenso bewirken Property
und PreferProperty
, dass EF Core über den Getter und Setter auf den Eigenschaftswert zugreift.
Wenn Field
oder Property
verwendet werden und EF Core nicht über das Feld oder den Getter/Setter der Eigenschaft auf den Wert zugreifen kann, löst EF Core eine Ausnahme aus. Dadurch wird sichergestellt, dass EF Core immer wie erwartet auf das Feld/die Eigenschaft zugreift.
Bei den Modi PreferField
und PreferProperty
erfolgt hingegen ein Fallback auf die Verwendung der Eigenschaft bzw. des Unterstützungsfelds, wenn der bevorzugte Zugriff nicht möglich ist. PreferField
ist die Standardoption. Dies bedeutet, dass EF Core Felder verwendet, wenn dies möglich ist, aber keine Fehler auslöst, wenn stattdessen über den Getter oder Setter auf eine Eigenschaft zugegriffen werden muss.
FieldDuringConstruction
und PreferFieldDuringConstruction
konfigurieren EF Core für die Verwendung von Unterstützungsfeldern, aber nur beim Erstellen von Entitätsinstanzen. Dadurch können Abfragen ohne die Nebenwirkungen von Gettern und Settern 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 | Einstellung | Voreinstellung für das Erstellen von Entitäten | Fallback | Fallback beim Erstellen von Entitäten |
---|---|---|---|---|
Field |
Feld | Feld | Löst aus | Löst aus |
Property |
Eigenschaft | Eigenschaft | Löst aus | Löst aus |
PreferField |
Feld | Feld | Eigenschaft | Eigenschaft |
PreferProperty |
Eigenschaft | Eigenschaft | Feld | Feld |
FieldDuringConstruction |
Eigenschaft | Feld | Feld | Löst aus |
PreferFieldDuringConstruction |
Eigenschaft | Feld | Feld | Eigenschaft |
Temporäre Werte
EF Core erstellt temporäre Schlüsselwerte beim Nachverfolgen neuer Entitäten, für die von der Datenbank echte Schlüsselwerte generiert werden, wenn SaveChanges aufgerufen wird. Eine Übersicht darüber, wie diese temporären Werte verwendet werden, finden Sie unter Änderungsnachverfolgung in EF Core.
Zugreifen auf temporäre Werte
Temporäre Werte werden in der Änderungsnachverfolgung gespeichert und nicht direkt in 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 mit EntityEntry.CurrentValues auf einen temporären Wert zu:
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 dieses Codes lautet:
Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643
PropertyEntry.IsTemporary kann verwendet werden, um temporäre Werte zu überprüfen.
Bearbeiten temporärer Werte
Es ist manchmal nützlich, direkt mit temporären Werten zu arbeiten. Beispielsweise kann eine Auflistung neuer Entitäten auf einem Webclient erstellt und dann zurück auf den Server serialisiert werden. Fremdschlüsselwerte bieten eine Möglichkeit zum Einrichten von Beziehungen zwischen diesen Entitäten. Im folgenden Code wird dieser Ansatz verwendet, um einen Graph neuer Entitäten nach Fremdschlüssel zuzuordnen. Bei diesem Ansatz ist es trotzdem möglich, beim Aufrufen von SaveChanges echte Schlüsselwerte zu generieren.
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);
context.SaveChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Beachten Sie Folgendes:
- Negative Zahlen werden als temporäre Schlüsselwerte verwendet. Dies ist zwar nicht erforderlich, aber eine gängige Konvention, um Schlüsselkonflikte zu verhindern.
- Die FK-Eigenschaft
Post.BlogId
wird dem gleichen negativen Wert wie die PK des zugeordneten Blogs zugewiesen. - Die PK-Werte werden durch Festlegen von 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 Änderungsnachverfolgung vor dem Aufrufen von SaveChanges ansehen, können Sie erkennen, 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 von SaveChanges wurden 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: []
Verwenden von Standardwerten
EF Core ermöglicht einer Eigenschaft, ihren Standardwert aus der Datenbank abzurufen, wenn SaveChanges aufgerufen wird. Wie bei generierten Schlüsselwerten verwendet EF Core nur dann einen Standardwert aus der Datenbank, wenn kein Wert explizit festgelegt wurde. Sehen Sie sich beispielsweise folgende Entitätstypen an:
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, sofern nicht ein expliziter Wert festgelegt wurde. 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) });
context.SaveChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
In der Debugansicht der Änderungsnachverfolgung ist zu erkennen, dass das erste Token ValidFrom
von der Datenbank generieren lassen hat, 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 bei EF Core-Migrationen bei Verwendung von HasDefaultValueSql oder HasDefaultValue. Sie müssen darauf achten, dass Sie bei Verwendung von EF Core-Migrationen die Standardeinschränkung für die Spalte auf andere Weise erstellen.
Verwenden von Nullwerte zulassenden 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 Integer-Eigenschaft:
public class Foo1
{
public int Id { get; set; }
public int Count { get; set; }
}
Dabei ist diese Eigenschaft mit einem Datenbankstandardwert von -1 konfiguriert:
modelBuilder
.Entity<Foo1>()
.Property(e => e.Count)
.HasDefaultValue(-1);
Die Absicht ist, dass der Standardwert von -1 verwendet wird, wenn kein expliziter Wert festgelegt wurde. Wenn der Wert auf 0 (null, der CLR-Standardwert für Integer) festgelegt wird, ist keine Unterscheidung zu dem Fall möglich, dass EF Core gar keinen Wert festlegt. Dies bedeutet, dass es nicht möglich ist, für diese Eigenschaft 0 (null) 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);
context.SaveChanges();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);
Beachten Sie, dass im Fall einer expliziten Festlegung von Count
auf 0 (null) weiterhin der Standardwert aus der Datenbank abgerufen wird, was nicht beabsichtigt ist. Eine einfache Möglichkeit, dies zu umgehen, besteht darin, die Count
-Eigenschaft als Nullwerte zulassend zu kennzeichnen:
public class Foo2
{
public int Id { get; set; }
public int? Count { get; set; }
}
Dadurch wird der CLR-Standardwert auf NULL anstelle von 0 (null) festgelegt, sodass jetzt beim expliziten Festlegen 0 (null) eingefügt wird:
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);
context.SaveChanges();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
Verwenden von Nullwerte zulassenden Unterstützungsfeldern
Das Problem beim Festlegen einer Eigenschaft als Nullwerte zulassend ist, dass sie im Domänenmodell möglicherweise nicht automatisch Nullwerte zulassend ist. Wenn Sie erzwingen, dass die Eigenschaft Nullwerte zulassend ist, kompromittieren Sie daher das Modell.
Die Eigenschaft kann nicht Nullwerte zulassend bleiben, wenn nur das Unterstützungsfeld Nullwerte zulassend 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, null) eingefügt werden, wenn die Eigenschaft explizit auf 0 (null) festgelegt wurde, ohne die Eigenschaft im Domänenmodell als Nullwerte zulassend zu kennzeichnen. 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);
context.SaveChanges();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
Nullwerte zulassende Unterstützungsfelder für boolesche Eigenschaften
Dieses Muster ist besonders nützlich, wenn boolesche Eigenschaften mit vom Speicher generierten Standardwerten verwendet werden. Da die CLR-Standardeinstellung für bool
FALSE lautet, kann FALSE nicht explizit mithilfe des normalen Musters eingefügt werden. 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 dem Datenbankstandardwert 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 wird nicht festgelegt – in letzterem Fall wird der Datenbankstandardwert 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);
context.SaveChanges();
Die Ausgabe von SaveChanges bei Verwendung von SQLite zeigt, dass der Datenbankstandardwert für Mac verwendet wird, während für Alice und Baxter explizite 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 dass EF Core diese Werte jemals für Einfügungen verwendet. Dazu können Sie die Eigenschaft als PropertyBuilder.ValueGeneratedNever konfigurieren, wie im folgenden Beispiel gezeigt:
modelBuilder
.Entity<Bar>()
.Property(e => e.Count)
.HasDefaultValue(-1)
.ValueGeneratedNever();