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

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

Совет

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

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

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

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

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

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

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

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

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

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> объекта post. 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 по-прежнему требуется для типов, которые не являются сущностями уведомлений.)

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

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

ChangeTrackingStrategy Необходимые интерфейсы Требуется DetectChanges Исходные значения моментальных снимков
Моментальный снимок Нет Да Да
ChangedNotifications INotifyPropertyChanged Нет Да
ChangingAndChangedNotifications INotifyPropertyChanged и INotifyPropertyChanging Нет Нет
ChangingAndChangedNotificationsWithOriginalValues INotifyPropertyChanged и INotifyPropertyChanging Нет Да

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

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