Share via


變更偵測和通知

每個 DbContext 執行個體都會追蹤實體的變更。 然後,這些被追蹤的實體會在呼叫 SaveChanges 時推動資料庫變更。 這涵蓋在 EF Core 變更追蹤中,本檔假設可以瞭解 Entity Framework Core (EF Core) 變更追蹤的實體狀態和基本概念。

追蹤屬性和關聯性變更需要 DbCoNtext 能夠偵測這些變更。 本檔涵蓋此偵測的發生方式,以及如何使用屬性通知或變更追蹤 Proxy 來強制立即偵測變更。

提示

您可以從 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 時,若要確保實體的狀態和修改的屬性是最新的。
  • 使用 EntityEntryCollectionReferenceMemberProperty 方法來確保屬性修改、目前值等是最新的。
  • 將刪除相依/子實體時,因為已切斷必要的關聯性。 這會偵測實體何時不應該刪除,因為它已重新上層。

您可以藉由呼叫 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 之後,程式碼就不會進行任何實體或屬性狀態變更。 (在 [新增實體] 上設定一般屬性值不會造成任何狀態變更。因此,當呼叫 Base 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 無法驗證或 INotifyPropertyChangingINotifyPropertyChanged 完全實作以搭配 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 快照集原始值
快照式 Yes Yes
ChangedNotifications INotifyPropertyChanged \(英文\) No Yes
ChangingAndChangedNotifications INotifyPropertyChanged 和 INotifyPropertyChanging No No
ChangingAndChangedNotificationsWithOriginalValues INotifyPropertyChanged 和 INotifyPropertyChanging No Yes

使用通知實體

通知實體的行為就像任何其他實體一樣,不同之處在于對實體實例進行變更不需要呼叫 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}

變更追蹤 Proxy

EF Core 可以動態產生實 INotifyPropertyChanging 作 和 INotifyPropertyChanged 的 Proxy 類型。 這需要安裝 Microsoft.EntityFrameworkCore.Proxies NuGet 套件,並啟用 UseChangeTrackingProxies 變更追蹤 Proxy,例如:

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

建立動態 Proxy 牽涉到建立新的動態 .NET 類型(使用 Castle.Core Proxy 實作),其繼承自實體類型,然後覆寫所有屬性 setter。 因此,Proxy 的實體類型必須是可以繼承自 的型別,而且必須具有可覆寫的屬性。 此外,明確建立的集合導覽必須實 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; }
}

變更追蹤 Proxy 的一個重要缺點是 EF Core 必須一律追蹤 Proxy 的實例,絕不會追蹤基礎實體類型的實例。 這是因為基礎實體類型的實例不會產生通知,這表示會遺漏對這些實體所做的變更。

EF Core 會在查詢資料庫時自動建立 Proxy 實例,因此此缺點通常僅限於追蹤新的實體實例。 這些實例必須使用擴充方法來建立 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 事件。 如需詳細資訊,請參閱 EF Core 中的 .NET 事件

注意

StateChanged當實體第一次追蹤時,不會引發事件,即使狀態已從 Detached 變更為其他狀態的其中一個。 請務必同時接 StateChanged 聽 和 Tracked 事件,以取得所有相關通知。