Änderungserkennung und Benachrichtigungen

Jede DbContext-Instanz verfolgt Änderungen nach, die an Entitäten vorgenommen wurden. Diese nachverfolgten Entitäten bestimmen wiederum die Änderungen an der Datenbank, wenn SaveChanges aufgerufen wird. Dies wird in Änderungsnachverfolgung in EF Core behandelt, und in diesem Dokument wird davon ausgegangen, dass Entitätszustände und die Grundlagen der Änderungsnachverfolgung von Entity Framework Core (EF Core) bekannt sind.

Das Nachverfolgen von Eigenschafts- und Beziehungsänderungen erfordert, dass der DbContext diese Änderungen erkennen kann. In diesem Dokument wird erläutert, wie diese Erkennung erfolgt und wie Eigenschaftsbenachrichtigungen oder Änderungsverfolgungsproxys verwendet werden, um die sofortige Erkennung von Änderungen zu erzwingen.

Tipp

Sie können den gesamten Code in dieser Dokumentation ausführen und debuggen, indem Sie den Beispielcode von GitHub herunterladen.

Änderungsnachverfolgung für Momentaufnahmen

Standardmäßig erstellt EF Core eine Momentaufnahme der Eigenschaftswerte jeder Entität, wenn sie zuerst von einer DbContext-Instanz nachverfolgt wird. Die in dieser Momentaufnahme gespeicherten Werte werden dann mit den aktuellen Werten der Entität verglichen, um zu bestimmen, welche Eigenschaftswerte geändert wurden.

Diese Erkennung von Änderungen erfolgt, wenn SaveChanges aufgerufen wird, um sicherzustellen, dass alle geänderten Werte erkannt werden, bevor Aktualisierungen an die Datenbank gesendet werden. Die Erkennung von Änderungen erfolgt jedoch auch zu anderen Zeiten, um sicherzustellen, dass die Anwendung mit aktuellen Nachverfolgungsinformationen arbeitet. Die Erkennung von Änderungen kann jederzeit durch Aufrufen von ChangeTracker.DetectChanges() erzwungen werden.

Wenn die Änderungserkennung erforderlich ist

Die Erkennung von Änderungen ist erforderlich, wenn eine Eigenschaft oder Navigation geändert wurde, ohne EF Core dafür zu verwenden. Ziehen Sie beispielsweise das Laden von Blogs und Beiträgen in Betracht, und nehmen Sie dann Änderungen an diesen Entitäten vor:

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Change a property value
blog.Name = ".NET Blog (Updated!)";

// Add a new entity to a navigation
blog.Posts.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
    });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Wenn die Debugansicht Änderungsnachverfolgung vor dem Aufrufen von ChangeTracker.DetectChanges() zeigt, dass die vorgenommenen Änderungen nicht erkannt wurden und sich daher nicht in den Entitätszuständen und geänderten Eigenschaftsdaten ausdrücken:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, <not found>]
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}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Insbesondere ist der Zustand des Blogeintrags noch Unchanged, und der neue Beitrag wird nicht als nachverfolgte Entität angezeigt. (Aufgeweckte werden feststellen, dass Eigenschaften ihre neuen Werte melden, obwohl diese Änderungen noch nicht von EF Core erkannt wurden. Dies liegt daran, dass die Debugansicht aktuelle Werte direkt aus der Entitätsinstanz liest.)

Kontrastieren Sie dies mit der Debugansicht nach dem Aufrufen von DetectChanges:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482643}]
Post {Id: -2147482643} Added
  Id: -2147482643 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 was released recently and has come with many...'
  Title: 'What's next for System.Text.Json?'
  Blog: {Id: 1}
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}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Jetzt ist der Blog korrekt als Modified gekennzeichnet, und der neue Beitrag wurde erkannt und wird als Added nachverfolgt.

Am Anfang dieses Abschnitts haben wir geäußert, dass das Erkennen von Änderungen erforderlich ist, wenn Sie nicht EF Core für die Änderung verwenden. Dies drückt der obige Code aus. Das heißt, die Änderungen an der Eigenschaft und Navigation werden direkt in den Entitätsinstanzen und nicht mithilfe von EF Core-Methoden vorgenommen.

Kontrastieren Sie dies mit dem folgenden Code, der die Entitäten auf die gleiche Weise ändert, aber diesmal mit EF Core-Methoden:

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Change a property value
context.Entry(blog).Property(e => e.Name).CurrentValue = ".NET Blog (Updated!)";

// Add a new entity to the DbContext
context.Add(
    new Post
    {
        Blog = blog,
        Title = "What’s next for System.Text.Json?",
        Content = ".NET 5.0 was released recently and has come with many..."
    });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

In diesem Fall zeigt die Debugansicht der Änderungsverfolgung, dass alle Entitätszustände und Eigenschaftenänderungen bekannt sind, auch wenn die Änderungen nicht erkannt wurden. Dies liegt daran, dass PropertyEntry.CurrentValue eine EF Core-Methode ist, was bedeutet, dass EF Core sofort über die von dieser Methode vorgenommene Änderung Kenntnis hat. Das Aufrufen von DbContext.Add ermöglicht es EF Core ebenso, sofort von der neuen Entität zu erfahren, und sie entsprechend zu verfolgen.

Tipp

Versuchen Sie nicht, das Erkennen von Änderungen zu vermeiden, indem Sie immer EF Core-Methoden verwenden, um Entitätsänderungen vorzunehmen. Dies ist oft mühsamer und funktioniert weniger gut als Änderungen an Entitäten auf normale Weise vorzunehmen. Die Absicht dieses Dokuments besteht darin, darüber zu informieren, wann Änderungen erkannt werden müssen und wann dies nicht der Fall ist. Die Absicht besteht nicht darin, das Vermeiden von Änderungserkennungen zu ermuntern.

Methoden, mit denen Änderungen automatisch erkannt werden

DetectChanges() wird automatisch von Methoden aufgerufen, bei denen sich dies wahrscheinlich auf die Ergebnisse auswirkt. Diese Methoden werden im Anschluss beschrieben:

Es gibt auch einige Orte, an denen die Erkennung von Änderungen nur in einer einzelnen Entitätsinstanz statt auf dem gesamten Diagramm der nachverfolgten Entitäten erfolgt. Zu solchen Umgebungen gehören:

  • Wenn Sie DbContext.Entry verwenden, stellen Sie sicher, dass der Zustand und die geänderten Eigenschaften der Entität auf dem neuesten Stand sind.
  • Wenn Sie EntityEntry-Methoden wie Property, Collection, Reference oder Member verwenden, um Eigenschaftenänderungen, aktuelle Werte usw. auf dem neuesten Stand zu halten.
  • Wenn eine abhängige bzw. untergeordnete Entität gelöscht wird, weil eine erforderliche Beziehung getrennt wurde. Dadurch wird erkannt, wann eine Entität nicht gelöscht werden soll, weil sie erneut übergeordnete Elemente enthält.

Die lokale Erkennung von Änderungen für eine einzelne Entität kann explizit durch Aufrufen von EntityEntry.DetectChanges() ausgelöst werden.

Hinweis

Eine lokale Änderungserkennung kann einige Änderungen übersehen, die eine vollständige Erkennung finden würde. Dies geschieht, wenn kaskadierende Aktionen, die sich aus nicht erkannten Änderungen an anderen Entitäten ergeben, Wirkungen auf die betreffende Entität haben. In solchen Situationen muss die Anwendung möglicherweise eine vollständige Überprüfung aller Entitäten erzwingen, indem sie ChangeTracker.DetectChanges() explizit aufruft.

Deaktivieren der automatischen Änderungserkennung

Die Erkennung von Änderungen stellt für die meisten Anwendungen keine Leistungseinschränkung dar. Das Erkennen von Änderungen kann jedoch zu einem Leistungsproblem für einige Anwendungen werden, die Tausende von Entitäten nachverfolgen. (Die genaue Zahl hängt von vielen Elementen ab, z. B. der Anzahl der Eigenschaften in der Entität.) Aus diesem Grund kann die automatische Erkennung von Änderungen mit ChangeTracker.AutoDetectChangesEnableddeaktiviert werden. Betrachten Sie beispielsweise die Verarbeitung von Verknüpfungsentitäten in einer Viele-zu-viele-Beziehung mit Nutzdaten:

public override int SaveChanges()
{
    foreach (var entityEntry in ChangeTracker.Entries<PostTag>()) // Detects changes automatically
    {
        if (entityEntry.State == EntityState.Added)
        {
            entityEntry.Entity.TaggedBy = "ajcvickers";
            entityEntry.Entity.TaggedOn = DateTime.Now;
        }
    }

    try
    {
        ChangeTracker.AutoDetectChangesEnabled = false;
        return base.SaveChanges(); // Avoid automatically detecting changes again here
    }
    finally
    {
        ChangeTracker.AutoDetectChangesEnabled = true;
    }
}

Wie wir aus dem vorherigen Abschnitt wissen, erkennen sowohl ChangeTracker.Entries<TEntity>() als auch DbContext.SaveChanges automatisch Änderungen. Nach dem Aufrufen von Einträgen nimmt der Code jedoch keine Änderungen von Entitäts- oder Eigenschaftszuständen vor. (Das Festlegen normaler Eigenschaftswerte für hinzugefügte Entitäten verursacht keine Zustandsänderungen.) Der Code deaktiviert daher die unnötige automatische Änderungserkennung beim Aufrufen der SaveChanges-Basismethode. Der Code verwendet außerdem einen Try/Finally-Block, um sicherzustellen, dass die Standardeinstellung wiederhergestellt wird, auch wenn SaveChanges fehlschlägt.

Tipp

Gehen Sie nicht davon aus, dass Ihr Code für eine gute Leistung die automatische Änderungserkennung deaktivieren muss. Dies ist nur erforderlich, wenn die Profilerstellung einer Anwendung, die viele Entitäten verfolgt, angibt, dass die Leistung der Änderungserkennung ein Problem darstellt.

Erkennen von Änderungen und Wertkonvertierungen

Um die Änderungsnachverfolgung durch Momentaufnahmen mit einem Entitätstyp zu verwenden, muss EF Core folgende Möglichkeiten haben:

  • eine Momentaufnahme jedes Eigenschaftswerts erstellen, wenn die Entität nachverfolgt wird
  • diesen Wert mit dem aktuellen Wert der Eigenschaft vergleichen
  • einen Hash für den Wert generieren

Dies erledigt EF Core automatisch für Typen, die direkt der Datenbank zugeordnet werden können. Wenn jedoch ein -Wertkonverter verwendet wird, um eine Eigenschaft zuzuordnen, muss dieser Konverter angeben, wie diese Aktionen ausgeführt werden sollen. Dies wird mit einem Wertabgleich erreicht und in der Dokumentation Wertabgleiche ausführlich beschrieben.

Benachrichtigungsentitäten

Die Änderungsnachverfolgung durch Momentaufnahmen wird für die meisten Anwendungen empfohlen. Anwendungen, die viele Entitäten nachverfolgen und/oder viele Änderungen an diesen Entitäten vornehmen, können jedoch von der Implementierung von Entitäten profitieren, die EF Core automatisch benachrichtigen, wenn sich ihre Eigenschafts- und Navigationswerte ändern. Diese werden als "Benachrichtigungsentitäten" bezeichnet.

Implementieren von Benachrichtigungsentitäten

Benachrichtigungsentitäten verwenden die Schnittstellen INotifyPropertyChanging und INotifyPropertyChanged, die Teil der .NET-Basisklassenbibliothek sind. Diese Schnittstellen definieren Ereignisse, die vor und nach dem Ändern eines Eigenschaftswerts ausgelöst werden müssen. Beispiel:

public class Blog : INotifyPropertyChanging, INotifyPropertyChanged
{
    public event PropertyChangingEventHandler PropertyChanging;
    public event PropertyChangedEventHandler PropertyChanged;

    private int _id;

    public int Id
    {
        get => _id;
        set
        {
            PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Id)));
            _id = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Id)));
        }
    }

    private string _name;

    public string Name
    {
        get => _name;
        set
        {
            PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Name)));
            _name = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
        }
    }

    public IList<Post> Posts { get; } = new ObservableCollection<Post>();
}

Darüber hinaus müssen alle Sammlungsnavigationen INotifyCollectionChanged implementieren; im obigen Beispiel ist dies mit einer ObservableCollection<T> von Beiträgen erfüllt. EF Core wird auch mit einer ObservableHashSet<T>-Implementierung ausgeliefert, die auf Kosten einer stabilen Sortierung über effizientere Lookup-Vorgänge verfügt.

Der Großteil dieses Benachrichtigungscodes wird in der Regel in eine nicht zugeordnete Basisklasse verschoben. Beispiel:

public class Blog : NotifyingEntity
{
    private int _id;

    public int Id
    {
        get => _id;
        set => SetWithNotify(value, out _id);
    }

    private string _name;

    public string Name
    {
        get => _name;
        set => SetWithNotify(value, out _name);
    }

    public IList<Post> Posts { get; } = new ObservableCollection<Post>();
}

public abstract class NotifyingEntity : INotifyPropertyChanging, INotifyPropertyChanged
{
    protected void SetWithNotify<T>(T value, out T field, [CallerMemberName] string propertyName = "")
    {
        NotifyChanging(propertyName);
        field = value;
        NotifyChanged(propertyName);
    }

    public event PropertyChangingEventHandler PropertyChanging;
    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyChanged(string propertyName)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    private void NotifyChanging(string propertyName)
        => PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}

Konfigurieren von Benachrichtigungsregeln

EF Core kann nicht überprüfen, ob INotifyPropertyChanging oder INotifyPropertyChanged für die Verwendung mit EF Core vollständig implementiert sind. Insbesondere verwenden einige dieser Schnittstellen Benachrichtigungen nur für bestimmte Eigenschaften, nicht für alle Eigenschaften (einschließlich Navigationen), wie von EF Core benötigt. Aus diesem Grund ist EF Core nicht automatisch in diese Ereignisse eingebunden.

Stattdessen muss EF Core für die Verwendung dieser Benachrichtigungsentitäten konfiguriert werden. Dies erfolgt in der Regel für alle Entitätstypen, indem ModelBuilder.HasChangeTrackingStrategy aufgerufen wird. Beispiel:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotifications);
}

(Die Strategie kann für unterschiedliche Entitätstypen mit EntityTypeBuilder.HasChangeTrackingStrategy auch unterschiedlich festgelegt werden. Dies ist jedoch in der Regel kontraproduktiv, da DetectChanges weiterhin für diese Typen erforderlich ist, die keine Benachrichtigungsentitäten sind.)

Die vollständige Änderungsnachverfolgung mit Benachrichtigungen erfordert, dass sowohl INotifyPropertyChanging als auch INotifyPropertyChanged implementiert werden. Auf diese Weise können ursprüngliche Werte direkt vor dem Ändern des Eigenschaftswerts gespeichert werden, sodass EF Core beim Nachverfolgen der Entität keine Momentaufnahme erstellen muss. Entitätstypen, die nur INotifyPropertyChanged implementieren, können auch mit EF Core verwendet werden. In diesem Fall erstellt EF immer noch eine Momentaufnahme, wenn eine Entität nachverfolgt wird, um die ursprünglichen Werte nachzuverfolgen, verwendet aber dann die Benachrichtigungen, um Änderungen sofort zu erkennen, anstatt DetectChanges aufrufen zu müssen.

Die verschiedenen ChangeTrackingStrategy-Werte werden in der folgenden Tabelle zusammengefasst.

ChangeTrackingStrategy Schnittstellen erforderlich Benötigt DetectChanges Momentaufnahmen der ursprünglichen Werte
Snapshot None Ja Ja
ChangedNotifications INotifyPropertyChanged Nein Ja
ChangingAndChangedNotifications INotifyPropertyChanged und INotifyPropertyChanging Nein Nein
ChangingAndChangedNotificationsWithOriginalValues INotifyPropertyChanged und INotifyPropertyChanging Nein Ja

Verwenden von Benachrichtigungsentitäten

Benachrichtigungsentitäten verhalten sich wie alle anderen Entitäten, mit der Ausnahme, dass Änderungen an den Entitätsinstanzen keinen Aufruf zu ChangeTracker.DetectChanges() um diese Veränderungen zu erkennen. Beispiel:

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Change a property value
blog.Name = ".NET Blog (Updated!)";

// Add a new entity to a navigation
blog.Posts.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
    });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Bei normalen Entitäten, die Debug-Ansicht des Änderungsverfolgers zeigte, dass diese Änderungen erst nach dem Aufruf von DetectChanges erkannt wurden. Wenn Sie die Debugansicht betrachten, wenn Benachrichtigungsentitäten verwendet werden, wird gezeigt, dass diese Änderungen sofort erkannt wurden:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482643}]
Post {Id: -2147482643} Added
  Id: -2147482643 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 was released recently and has come with many...'
  Title: 'What's next for System.Text.Json?'
  Blog: {Id: 1}
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}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Proxys zur Änderungsnachverfolgung

EF Core kann Proxytypen dynamisch generieren, die INotifyPropertyChanging und INotifyPropertyChangedimplementieren. Dies erfordert die Installation des Microsoft.EntityFrameworkCore.Proxies NuGet-Pakets und aktivieren Change-Tracking-Proxys mit UseChangeTrackingProxies Beispiel:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseChangeTrackingProxies();

Das Erstellen eines dynamischen Proxys umfasst das Erstellen eines neuen dynamischen .NET-Typs (unter Verwendung der Castle.Core Proxyimplementierung), der vom Entitätstyp erbt und dann alle Eigenschaftensetter überschreibt. Entitätstypen für Proxys müssen daher Typen sein, die geerbt werden können und über Eigenschaften verfügen, die überschrieben werden können. Auch explizit erstellte Sammlungsnavigationen müssen implementiert werden INotifyCollectionChanged Zum Beispiel:

public class Blog
{
    public virtual int Id { get; set; }
    public virtual string Name { get; set; }

    public virtual IList<Post> Posts { get; } = new ObservableCollection<Post>();
}

public class Post
{
    public virtual int Id { get; set; }
    public virtual string Title { get; set; }
    public virtual string Content { get; set; }

    public virtual int BlogId { get; set; }
    public virtual Blog Blog { get; set; }
}

Ein erheblicher Nachteil für Änderungsverfolgungsproxys besteht darin, dass EF Core immer Instanzen des Proxys nachverfolgen muss, niemals Instanzen des zugrunde liegenden Entitätstyps. Dies liegt daran, dass Instanzen des zugrunde liegenden Entitätstyps keine Benachrichtigungen generieren, was bedeutet, dass An diesen Entitäten vorgenommene Änderungen verpasst werden.

EF Core erstellt Proxyinstanzen automatisch beim Abfragen der Datenbank, sodass dieser Nachteil in der Regel auf das Nachverfolgen neuer Entitätsinstanzen beschränkt ist. Diese Instanzen müssen mithilfe der CreateProxy Erweiterungsmethoden erstellt werden, und nicht auf normale Weise mithilfe von new. Das bedeutet, dass der Code aus den vorherigen Beispielen gebrauch machen muss von CreateProxy:

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Change a property value
blog.Name = ".NET Blog (Updated!)";

// Add a new entity to a navigation
blog.Posts.Add(
    context.CreateProxy<Post>(
        p =>
        {
            p.Title = "What’s next for System.Text.Json?";
            p.Content = ".NET 5.0 was released recently and has come with many...";
        }));

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Änderungsnachverfolgung Ereignis

EF Core löst das ChangeTracker.Tracked Ereignis aus, wenn eine Entität zum ersten Mal nachverfolgt wird. Zukünftige Entitätsstatusänderungen führen zu ChangeTracker.StateChanged Ereignissen. Weitere Informationen finden Sie unter .NET-Ereignisse in EF Core.

Hinweis

Das StateChanged -Ereignis wird nicht ausgelöst, wenn eine Entität zuerst nachverfolgt wird, obwohl sich der Zustand von Detached in einen der anderen Zustände geändert hat. Achten Sie darauf, dass Sie auf beides StateChanged und auf Tracked Ereignisse achten, um alle relevanten Benachrichtigungen zu erhalten.