Wykrywanie zmian i powiadomienia

Każde wystąpienie obiektu DbContext śledzi zmiany wprowadzane w jednostkach. Te śledzone jednostki sterują z kolei zmianami w bazie danych przy wywoływaniu metody SaveChanges. Opisano to w temacie Change Tracking in EF Core (Śledzenie zmian w programie EF Core), a w tym dokumencie założono, że stany jednostek i podstawy śledzenia zmian platformy Entity Framework Core (EF Core) są zrozumiałe.

Śledzenie zmian właściwości i relacji wymaga, aby obiekt DbContext mógł wykryć te zmiany. W tym dokumencie opisano sposób wykrywania, a także sposób używania powiadomień dotyczących właściwości lub serwerów proxy śledzenia zmian w celu wymuszenia natychmiastowego wykrywania zmian.

Napiwek

Możesz uruchomić i debugować cały kod podany w tym dokumencie, pobierając przykładowy kod z serwisu GitHub.

Śledzenie zmian migawek

Domyślnie program EF Core tworzy migawkę wartości właściwości każdej jednostki, gdy jest ona najpierw śledzona przez wystąpienie DbContext. Wartości przechowywane w tej migawce są następnie porównywane z bieżącymi wartościami jednostki w celu określenia, które wartości właściwości uległy zmianie.

To wykrywanie zmian występuje po wywołaniu metody SaveChanges w celu upewnienia się, że wszystkie zmienione wartości zostaną wykryte przed wysłaniem aktualizacji do bazy danych. Jednak wykrywanie zmian odbywa się również w innym czasie, aby upewnić się, że aplikacja współpracuje z aktualnymi informacjami o śledzeniu. Wykrywanie zmian można wymusić w dowolnym momencie przez wywołanie metody ChangeTracker.DetectChanges().

Gdy jest wymagane wykrywanie zmian

Wykrywanie zmian jest wymagane, gdy właściwość lub nawigacja została zmieniona bez używania programu EF Core do wprowadzenia tej zmiany. Rozważ na przykład ładowanie blogów i wpisów, a następnie wprowadzanie zmian w następujących jednostkach:

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

Patrząc na widok debugowania śledzenia zmian przed wywołaniem ChangeTracker.DetectChanges() , widać, że wprowadzone zmiany nie zostały wykryte, dlatego nie są odzwierciedlane w stanach jednostki i zmodyfikowanych danych właściwości:

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}

W szczególności stan wpisu w blogu to , Unchangeda nowy wpis nie jest wyświetlany jako śledzona jednostka. (Informacje o właściwościach będą raportować ich nowe wartości, mimo że te zmiany nie zostały jeszcze wykryte przez program EF Core. Dzieje się tak, ponieważ widok debugowania odczytuje bieżące wartości bezpośrednio z wystąpienia jednostki).

Porównaj to z widokiem debugowania po wywołaniu funkcji 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}

Teraz blog jest poprawnie oznaczony jako Modified i nowy wpis został wykryty i jest śledzony jako Added.

Na początku tej sekcji stwierdziliśmy, że wykrywanie zmian jest wymagane, gdy nie jest używane program EF Core do wprowadzenia zmiany. Dzieje się tak w powyższym kodzie. Oznacza to, że zmiany właściwości i nawigacji są wprowadzane bezpośrednio w wystąpieniach jednostki, a nie przy użyciu żadnych metod platformy EF Core.

Porównaj to z następującym kodem, który modyfikuje jednostki w ten sam sposób, ale tym razem przy użyciu metod EF Core:

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

W tym przypadku widok debugowania monitora zmian pokazuje, że wszystkie stany jednostki i modyfikacje właściwości są znane, mimo że nie nastąpiło wykrywanie zmian. Wynika to z faktu PropertyEntry.CurrentValue , że jest to metoda EF Core, co oznacza, że program EF Core natychmiast wie o zmianie wprowadzonej przez tę metodę. Podobnie wywołanie DbContext.Add umożliwia programowi EF Core natychmiastowe poznanie nowej jednostki i odpowiednie śledzenie jej.

Napiwek

Nie próbuj unikać wykrywania zmian, zawsze używając metod EF Core do wprowadzania zmian jednostek. Takie działanie jest często bardziej kłopotliwe i działa mniej dobrze niż wprowadzanie zmian w jednostkach w normalny sposób. Celem tego dokumentu jest informowanie o tym, kiedy jest konieczne wykrywanie zmian, a kiedy nie. Celem nie jest zachęcanie do unikania wykrywania zmian.

Metody, które automatycznie wykrywają zmiany

DetectChanges() metoda jest wywoływana automatycznie przez metody, w których może to mieć wpływ na wyniki. Oto następujące metody:

Istnieją również pewne miejsca, w których wykrywanie zmian odbywa się tylko w pojedynczym wystąpieniu jednostki, a nie na całym grafie śledzonych jednostek. Te miejsca to:

  • W przypadku używania polecenia DbContext.Entryupewnij się, że stan i zmodyfikowane właściwości jednostki są aktualne.
  • W przypadku używania EntityEntry metod takich jak Property, CollectionReference lub Member w celu zapewnienia, że modyfikacje właściwości, bieżące wartości itp. są aktualne.
  • Gdy jednostka zależna/podrzędna zostanie usunięta, ponieważ wymagana relacja została zerwana. Wykrywa to, kiedy nie należy usuwać jednostki, ponieważ została ona ponownie nadrzędna.

Lokalne wykrywanie zmian dla pojedynczej jednostki może być wyzwalane jawnie przez wywołanie metody EntityEntry.DetectChanges().

Uwaga

Lokalne wykrywanie zmian może przegapić niektóre zmiany, które można znaleźć w pełnym wykryciu. Dzieje się tak, gdy akcje kaskadowe wynikające z niezakrytych zmian w innych jednostkach mają wpływ na określoną jednostkę. W takich sytuacjach aplikacja może wymagać wymuszenia pełnego skanowania wszystkich jednostek przez jawne wywołanie metody ChangeTracker.DetectChanges().

Wyłączanie automatycznego wykrywania zmian

Wydajność wykrywania zmian nie jest wąskim gardłem dla większości aplikacji. Jednak wykrywanie zmian może stać się problemem z wydajnością niektórych aplikacji, które śledzą tysiące jednostek. (Dokładna liczba będzie zależeć od wielu elementów, takich jak liczba właściwości w jednostce). Z tego powodu automatyczne wykrywanie zmian można wyłączyć przy użyciu polecenia ChangeTracker.AutoDetectChangesEnabled. Rozważmy na przykład przetwarzanie jednostek sprzężenia w relacji wiele-do-wielu z ładunkami:

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;
    }
}

Jak wiemy z poprzedniej sekcji, zarówno, jak ChangeTracker.Entries<TEntity>() i DbContext.SaveChanges automatycznie wykrywają zmiany. Jednak po wywołaniu pozycji kod nie wprowadza żadnych zmian stanu jednostki ani właściwości. (Ustawienie normalnych wartości właściwości w dodanych jednostkach nie powoduje żadnych zmian stanu). W związku z tym kod wyłącza niepotrzebne automatyczne wykrywanie zmian podczas wywoływania do podstawowej metody SaveChanges. Kod korzysta również z bloku try/finally, aby upewnić się, że ustawienie domyślne zostanie przywrócone, nawet jeśli funkcja SaveChanges zakończy się niepowodzeniem.

Napiwek

Nie zakładaj, że kod musi wyłączyć automatyczne wykrywanie zmian w celu zapewnienia dobrego działania. Jest to konieczne tylko wtedy, gdy profilowanie aplikacji śledzącej wiele jednostek wskazuje, że wydajność wykrywania zmian jest problemem.

Wykrywanie zmian i konwersji wartości

Aby korzystać ze śledzenia zmian migawki z typem jednostki, program EF Core musi mieć następujące możliwości:

  • Tworzenie migawki każdej wartości właściwości podczas śledzenia jednostki
  • Porównaj tę wartość z bieżącą wartością właściwości
  • Generowanie kodu skrótu dla wartości

Jest to obsługiwane automatycznie przez program EF Core dla typów, które można bezpośrednio zamapować do bazy danych. Jednak gdy konwerter wartości jest używany do mapowania właściwości, ten konwerter musi określić sposób wykonywania tych akcji. Jest to osiągane za pomocą porównania wartości i zostało szczegółowo opisane w dokumentacji funkcji porównywania wartości.

Jednostki powiadomień

W przypadku większości aplikacji zalecane jest śledzenie zmian migawek. Jednak aplikacje, które śledzą wiele jednostek i/lub wprowadzają wiele zmian w tych jednostkach, mogą korzystać z implementacji jednostek, które automatycznie powiadamiają program EF Core o zmianie ich właściwości i wartości nawigacji. Są one nazywane "jednostkami powiadomień".

Implementowanie jednostek powiadomień

Jednostki powiadomień korzystają z INotifyPropertyChanging interfejsów i INotifyPropertyChanged , które są częścią biblioteki klas bazowych platformy .NET (BCL). Te interfejsy definiują zdarzenia, które muszą zostać wyzwolone przed zmianą wartości właściwości i po jej zmianie. Przykład:

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

Ponadto wszystkie nawigacje kolekcji muszą implementować INotifyCollectionChanged; w powyższym przykładzie jest to zadowalające przy użyciu wpisów ObservableCollection<T> . Program EF Core jest również dostarczany z implementacją ObservableHashSet<T> , która ma bardziej wydajne wyszukiwanie kosztem stabilnego zamawiania.

Większość tego kodu powiadomień jest zwykle przenoszona do niezamapowanej klasy bazowej. Przykład:

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

Konfigurowanie jednostek powiadomień

Nie ma możliwości sprawdzenia, czy INotifyPropertyChanging program EF Core jest INotifyPropertyChanged w pełni zaimplementowany do użytku z platformą EF Core. W szczególności niektóre z tych interfejsów robią to z powiadomieniami tylko dla niektórych właściwości, a nie we wszystkich właściwościach (w tym nawigacji) zgodnie z wymaganiami platformy EF Core. Z tego powodu program EF Core nie jest automatycznie podłączany do tych zdarzeń.

Zamiast tego należy skonfigurować program EF Core do korzystania z tych jednostek powiadomień. Zwykle odbywa się to dla wszystkich typów jednostek przez wywołanie metody ModelBuilder.HasChangeTrackingStrategy. Przykład:

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

(Strategię można również ustawić inaczej dla różnych typów jednostek przy użyciu metody EntityTypeBuilder.HasChangeTrackingStrategy, ale zwykle jest to sprzeczne z produktem, ponieważ funkcja DetectChanges jest nadal wymagana dla tych typów, które nie są jednostkami powiadomień).

Pełne śledzenie zmian powiadomień wymaga zaimplementowania obu INotifyPropertyChanging tych elementów i INotifyPropertyChanged . Umożliwia to zapisanie oryginalnych wartości tuż przed zmianą wartości właściwości, co pozwala uniknąć konieczności utworzenia migawki przez program EF Core podczas śledzenia jednostki. Typy jednostek implementujące INotifyPropertyChanged tylko mogą być używane z programem EF Core. W tym przypadku program EF nadal tworzy migawkę podczas śledzenia jednostki w celu śledzenia oryginalnych wartości, ale następnie używa powiadomień do natychmiastowego wykrywania zmian, a nie konieczności wywoływanie funkcji DetectChanges.

Różne ChangeTrackingStrategy wartości zostały podsumowane w poniższej tabeli.

ChangeTrackingStrategy Wymagane interfejsy Wymaga wykrywania zmian Migawki oryginalnych wartości
Snapshot None Tak Tak
ZmienionoNotyfikacje Inotifypropertychanged Nie. Tak
ZmianaandChangedNotifications INotifyPropertyChanged i INotifyPropertyChanging Nie. Nie.
ChangingAndChangedNotificationsWithOriginalValues INotifyPropertyChanged i INotifyPropertyChanging Nie. Tak

Korzystanie z jednostek powiadomień

Jednostki powiadomień zachowują się jak inne jednostki, z tą różnicą, że wprowadzanie zmian w wystąpieniach jednostki nie wymaga wywołania w celu ChangeTracker.DetectChanges() wykrycia tych zmian. Przykład:

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

W przypadku normalnych jednostek widok debugowania monitora zmian pokazał, że te zmiany nie zostały wykryte do momentu wywołania funkcji DetectChanges. Patrząc na widok debugowania, gdy używane są jednostki powiadomień, widać, że te zmiany zostały wykryte natychmiast:

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}

Serwery proxy śledzenia zmian

Program EF Core może dynamicznie generować typy serwerów proxy implementujące INotifyPropertyChanging i INotifyPropertyChanged. Wymaga to zainstalowania pakietu NuGet Microsoft.EntityFrameworkCore.Proxies i włączenia serwerów UseChangeTrackingProxies proxy śledzenia zmian za pomocą polecenia Na przykład:

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

Tworzenie dynamicznego serwera proxy obejmuje utworzenie nowego, dynamicznego typu platformy .NET (przy użyciu implementacji serwerów proxy Castle.Core ), który dziedziczy z typu jednostki, a następnie zastępuje wszystkie zestawy właściwości. W związku z tym typy jednostek dla serwerów proxy muszą być typami, które mogą być dziedziczone z i muszą mieć właściwości, które mogą być zastępowane. Ponadto utworzone jawnie nawigacje kolekcji muszą implementować INotifyCollectionChanged na przykład:

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; }
}

Jednym z znaczących wad śledzenia zmian serwerów proxy jest to, że program EF Core musi zawsze śledzić wystąpienia serwera proxy, nigdy wystąpienia bazowego typu jednostki. Dzieje się tak, ponieważ wystąpienia bazowego typu jednostki nie będą generować powiadomień, co oznacza, że zmiany wprowadzone w tych jednostkach zostaną pominięte.

Program EF Core automatycznie tworzy wystąpienia serwera proxy podczas wykonywania zapytań względem bazy danych, więc ta wada jest zwykle ograniczona do śledzenia nowych wystąpień jednostek. Te wystąpienia muszą być tworzone przy użyciu CreateProxy metod rozszerzeń, a nie w normalny sposób przy użyciu metody new. Oznacza to, że kod z poprzednich przykładów musi teraz używać elementu 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);

Zdarzenia śledzenia zmian

Program EF Core uruchamia ChangeTracker.Tracked zdarzenie, gdy jednostka jest śledzona po raz pierwszy. Przyszłe zmiany stanu jednostki powodują zdarzenia ChangeTracker.StateChanged . Aby uzyskać więcej informacji, zobacz Zdarzenia platformy .NET w programie EF Core.

Uwaga

Zdarzenie StateChanged nie jest wyzwalane, gdy jednostka jest najpierw śledzona, mimo że stan zmienił się z Detached na jeden z innych stanów. Pamiętaj, aby nasłuchiwać zarówno zdarzeń, StateChanged jak i Tracked , aby otrzymywać wszystkie odpowiednie powiadomienia.