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:

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ätenverfü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();