Freigeben über


Zusätzliche Features zur Änderungsnachverfolgung

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, AttachRangeund RemoveRange

DbSet<TEntity> und DbContext bieten alternative Versionen von Add, Update, , Attachund 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, Attachund 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:

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