Обнаружение изменений и уведомления

Каждый экземпляр DbContext отслеживает изменения, внесенные в сущности. Эти отслеживаемые сущности, в свою очередь, записывают изменения в базу данных при вызове SaveChanges. Это рассматривается в Отслеживание изменений в EF Core, и в этом документе предполагается, что состояния сущностей и основы отслеживания изменений Entity Framework Core (EF Core).

Для отслеживания изменений свойств и связей требуется, чтобы DbContext мог обнаруживать эти изменения. В этом документе описывается, как это обнаружение происходит, а также как использовать уведомления о свойствах или прокси-серверы отслеживания изменений для принудительного обнаружения изменений.

Совет

Вы можете запустить и отладить весь код, используемый в этой документации, скачав пример кода из GitHub.

Отслеживание изменений по моментальному снимку

По умолчанию EF Core создает моментальный снимок значений свойств каждой сущности при первом отслеживании экземпляром DbContext. Затем значения, хранящиеся в этом моментальном снимке, сравниваются с текущими значениями сущности, чтобы определить, какие значения свойств изменились.

Это обнаружение изменений происходит при вызове SaveChanges, чтобы убедиться, что все измененные значения обнаружены перед отправкой обновлений в базу данных. Однако обнаружение изменений также происходит в другое время, чтобы убедиться, что приложение работает с актуальной информацией об отслеживании. Обнаружение изменений может быть принудительно принудительно в любое время путем вызова ChangeTracker.DetectChanges().

Если требуется обнаружение изменений

Обнаружение изменений необходимо, если свойство или навигация были изменены без использования 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
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);

Просмотр представления отладки отслеживания изменений перед вызовом ChangeTracker.DetectChanges() показывает, что внесенные изменения не обнаружены, поэтому не отражаются в состояниях сущности и измененных данных свойств:

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}

В частности, состояние записи блога по-прежнему Unchanged, и новая запись не отображается как отслеживаемая сущность. (В astute будут замечать, что свойства сообщают о новых значениях, даже если эти изменения еще не обнаружены EF Core. Это связано с тем, что представление отладки считывает текущие значения непосредственно из экземпляра сущности.)

Сравните это с представлением отладки после вызова 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}

Теперь блог помечается правильно, как Modified и обнаружена новая запись и отслеживается как Added.

В начале этого раздела мы заявили, что обнаружение изменений необходимо при не использовании EF Core для внесения изменений. Это то, что происходит в приведенном выше коде. То есть изменения свойства и навигации вносятся непосредственно в экземпляры сущностей, а не с помощью методов EF Core.

В отличие от следующего кода, который изменяет сущности таким же образом, но на этот раз с помощью методов 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);

В этом случае представление отладки средства отслеживания изменений показывает, что известны все состояния сущности и изменения свойств, даже если обнаружение изменений не произошло. Это связано с тем, что PropertyEntry.CurrentValue это метод EF Core, который означает, что EF Core сразу же знает об изменениях, внесенных этим методом. Аналогичным образом вызов DbContext.Add позволяет EF Core немедленно знать о новой сущности и отслеживать ее соответствующим образом.

Совет

Не пытайтесь избежать обнаружения изменений с помощью методов EF Core для внесения изменений сущностей. Это часто является более громоздким и работает менее хорошо, чем внесение изменений в сущности обычным образом. Цель этого документа заключается в том, чтобы сообщить об обнаружении изменений и когда это не так. Цель заключается в том, чтобы не поощрять предотвращение обнаружения изменений.

Методы, которые автоматически обнаруживают изменения

DetectChanges() вызывается автоматически методами, в которых это может повлиять на результаты. Этими методами являются:

Есть также некоторые места, где обнаружение изменений происходит только в одном экземпляре сущности, а не на всем графе отслеживаемых сущностей. Ниже перечислены следующие места:

  • При использовании DbContext.Entryубедитесь, что состояние и измененные свойства сущности актуальны.
  • При использовании EntityEntry таких методов, как Property, CollectionReference или Member для обеспечения изменений свойств, текущих значений и т. д. актуальны.
  • При удалении зависимой или дочерней сущности из-за того, что необходимая связь была удалена. Это определяет, когда сущность не должна быть удалена, так как она была повторно родительской.

Локальное обнаружение изменений для одной сущности можно активировать явным образом путем вызова EntityEntry.DetectChanges().

Примечание.

Локальные обнаруженные изменения могут пропустить некоторые изменения, которые будут находиться в полном обнаружении. Это происходит, когда каскадные действия, возникающие из-за незамеченных изменений в других сущностях, оказывают влияние на сущность, поддающуюся вопросу. В таких ситуациях приложению может потребоваться принудительное полное сканирование всех сущностей путем явного вызова ChangeTracker.DetectChanges().

Отключение автоматического обнаружения изменений

Производительность обнаружения изменений не является узким местом для большинства приложений. Однако обнаружение изменений может стать проблемой производительности для некоторых приложений, отслеживающих тысячи сущностей. (Точное число будет зависеть от многих вещей, таких как количество свойств в сущности.) По этой причине автоматическое обнаружение изменений может быть отключено с помощью ChangeTracker.AutoDetectChangesEnabled. Например, рассмотрите возможность обработки сущностей соединения в связи "многие ко многим" с полезными данными:

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

Как мы знаем из предыдущего раздела, и ChangeTracker.Entries<TEntity>()DbContext.SaveChanges автоматически обнаруживаем изменения. Однако после вызова записей код не вносит никаких изменений в состояние сущности или свойства. (Установка обычных значений свойств в добавленных сущностях не приводит к изменениям состояния.) Поэтому код отключает ненужное автоматическое обнаружение изменений при вызове в базовый метод SaveChanges. Код также использует блок try/finally, чтобы убедиться, что параметр по умолчанию восстановлен, даже если SaveChanges завершается сбоем.

Совет

Не предполагайте, что код должен отключить автоматическое обнаружение изменений для правильного выполнения. Это необходимо только при профилировании отслеживания приложений многих сущностей указывает, что производительность обнаружения изменений является проблемой.

Обнаружение изменений и преобразований значений

Чтобы использовать отслеживание изменений моментальных снимков с типом сущности, EF Core должен иметь возможность:

  • Создание моментального снимка каждого значения свойства при отслеживании сущности
  • Сравните это значение с текущим значением свойства
  • Создание хэш-кода для значения

Это обрабатывается автоматически EF Core для типов, которые можно напрямую сопоставить с базой данных. Однако, когда преобразователь значений используется для сопоставления свойства, этот преобразователь должен указать, как выполнять эти действия. Это достигается с помощью средства сравнения значений и подробно описано в документации по сравнениям значений.

Сущности уведомлений

Отслеживание изменений моментальных снимков рекомендуется для большинства приложений. Однако приложения, которые отслеживают множество сущностей и /или вносят много изменений в эти сущности, могут воспользоваться реализацией сущностей, которые автоматически уведомляют EF Core при изменении значений свойств и навигации. Они называются "сущностями уведомлений".

Реализация сущностей уведомлений

Сущности уведомлений используют INotifyPropertyChanging интерфейсы и INotifyPropertyChanged интерфейсы, которые являются частью библиотеки базовых классов .NET (BCL). Эти интерфейсы определяют события, которые должны быть запущены до и после изменения значения свойства. Например:

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

Кроме того, все навигации по коллекции должны реализовываться INotifyCollectionChanged; в приведенном выше примере это удовлетворено с помощью ObservableCollection<T> записей. EF Core также поставляется с ObservableHashSet<T> реализацией, которая имеет более эффективные поиски за счет стабильного упорядочения.

Большая часть этого кода уведомлений обычно перемещается в несопоставленный базовый класс. Например:

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

Настройка сущностей уведомлений

Ef Core не может проверить, INotifyPropertyChanging что или INotifyPropertyChanged полностью реализовано для использования с EF Core. В частности, некоторые из этих интерфейсов делают это с уведомлениями только по определенным свойствам, а не ко всем свойствам (включая навигации) в соответствии с требованиями EF Core. По этой причине EF Core не автоматически подключается к этим событиям.

Вместо этого EF Core необходимо настроить для использования этих сущностей уведомлений. Обычно это делается для всех типов сущностей путем вызова ModelBuilder.HasChangeTrackingStrategy. Например:

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

(Стратегия также может быть задана по-разному для различных типов сущностей, но EntityTypeBuilder.HasChangeTrackingStrategyэто обычно контрпродуктивно, так как DetectChanges по-прежнему требуется для тех типов, которые не являются сущностями уведомлений.)

Для полного отслеживания изменений уведомлений требуется, чтобы INotifyPropertyChanging оба и INotifyPropertyChanged были реализованы. Это позволяет сохранять исходные значения непосредственно перед изменением значения свойства, избегая необходимости создания моментального снимка EF Core при отслеживании сущности. Типы сущностей, реализующие только эти типы, можно использовать только INotifyPropertyChanged с EF Core. В этом случае EF по-прежнему создает моментальный снимок при отслеживании сущности для отслеживания исходных значений, но затем использует уведомления для немедленного обнаружения изменений, а не необходимости вызывать DetectChanges.

В следующей таблице приведены различные ChangeTrackingStrategy значения.

ChangeTrackingStrategy Необходимые интерфейсы Потребности DetectChanges Исходные значения моментальных снимков
Снимок None Да Да
ИзмененныеNotifications INotifyPropertyChanged No Да
ИзменениеAndChangedNotifications INotifyPropertyChanged и INotifyPropertyChanging No No
ИзменениеAndChangedNotificationsWithOriginalValues INotifyPropertyChanged и INotifyPropertyChanging No Да

Использование сущностей уведомлений

Сущности уведомлений ведут себя как любые другие сущности, за исключением того, что внесение изменений в экземпляры сущностей не требует вызова для ChangeTracker.DetectChanges() обнаружения этих изменений. Например:

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

В обычных сущностях представление отладки отслеживания изменений показало, что эти изменения не были обнаружены до вызова DetectChanges. Просмотр представления отладки при использовании сущностей уведомлений показывает, что эти изменения были обнаружены немедленно:

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}

Прокси-серверы отслеживания изменений

EF Core может динамически создавать типы прокси-серверов, реализующие INotifyPropertyChanging и INotifyPropertyChanged. Для этого требуется установить пакет NuGet Microsoft.EntityFrameworkCore.Proxies и включить прокси-серверы отслеживания изменений, UseChangeTrackingProxies например:

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

Создание динамического прокси-сервера включает создание нового динамического типа .NET (с помощью реализации прокси-серверов Castle.Core ), который наследует от типа сущности, а затем переопределяет все методы задания свойств. Таким образом, типы сущностей для прокси-серверов должны быть типами, которые могут быть унаследованы от и должны иметь свойства, которые можно переопределить. Кроме того, навигации коллекции, созданные явным образом, должны реализовать INotifyCollectionChanged :

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

Одним из существенных недостатков прокси-серверов отслеживания изменений является то, что EF Core всегда должен отслеживать экземпляры прокси-сервера, никогда не экземпляры базового типа сущности. Это связано с тем, что экземпляры базового типа сущности не будут создавать уведомления, что означает, что изменения, внесенные в эти сущности, будут пропущены.

EF Core автоматически создает экземпляры прокси-сервера при запросе к базе данных, поэтому эта недостатка обычно ограничивается отслеживанием новых экземпляров сущностей. Эти экземпляры должны создаваться с помощью CreateProxy методов расширения, а не в обычном режиме.new Это означает, что код из предыдущих примеров теперь должен использовать 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);

События отслеживания изменений

EF Core запускает ChangeTracker.Tracked событие при первом отслеживании сущности. Будущие изменения состояния сущности приводят к событиям ChangeTracker.StateChanged . Дополнительные сведения см. в статье о событиях .NET в EF Core.

Примечание.

Событие StateChanged не запускается при первом отслеживании сущности, даже если состояние изменилось с Detached одного из других состояний. Убедитесь, что вы прослушиваете StateChanged оба события, Tracked чтобы получить все соответствующие уведомления.