Condividi tramite


Rilevamento modifiche e notifiche

Ogni istanza di DbContext tiene traccia delle modifiche apportate alle entità. Queste entità rilevate a loro volta determinano le modifiche apportate al database quando viene chiamato SaveChanges. Questo argomento è trattato in Rilevamento modifiche in EF Core e questo documento presuppone che gli stati dell'entità e le nozioni di base del rilevamento delle modifiche di Entity Framework Core (EF Core) siano riconosciuti.

Per rilevare le modifiche alle proprietà e alle relazioni è necessario che DbContext sia in grado di rilevare queste modifiche. Questo documento illustra come avviene questo rilevamento, nonché come usare le notifiche delle proprietà o i proxy di rilevamento delle modifiche per forzare il rilevamento immediato delle modifiche.

Suggerimento

È possibile eseguire ed eseguire il debug in tutto il codice di questo documento scaricando il codice di esempio da GitHub.

Rilevamento modifiche basato su snapshot

Per impostazione predefinita, EF Core crea uno snapshot dei valori delle proprietà di ogni entità quando viene rilevata per la prima volta da un'istanza DbContext. I valori archiviati in questo snapshot vengono quindi confrontati con i valori correnti dell'entità per determinare quali valori delle proprietà sono stati modificati.

Questo rilevamento delle modifiche si verifica quando viene chiamato SaveChanges per assicurarsi che tutti i valori modificati vengano rilevati prima di inviare aggiornamenti al database. Tuttavia, il rilevamento delle modifiche avviene anche in altri momenti per garantire che l'applicazione funzioni con informazioni di rilevamento aggiornate. Il rilevamento delle modifiche può essere forzato in qualsiasi momento chiamando ChangeTracker.DetectChanges().

Quando è necessario il rilevamento delle modifiche

Il rilevamento delle modifiche è necessario quando una proprietà o una struttura di spostamento è stata modificata senza usare EF Core per apportare questa modifica. Si consideri ad esempio il caricamento di blog e post e quindi le modifiche apportate a queste entità:

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

Esaminando la visualizzazione debug di Rilevamento modifiche prima di chiamare ChangeTracker.DetectChanges() viene mostrato che le modifiche apportate non sono state rilevate e quindi non vengono riflesse negli stati dell'entità e nei dati delle proprietà modificate:

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}

In particolare, lo stato della voce di blog è ancora Unchangede il nuovo post non viene visualizzato come entità rilevata. L'astuto noterà che le proprietà segnalano i nuovi valori, anche se queste modifiche non sono ancora state rilevate da EF Core. Ciò è dovuto al fatto che la visualizzazione di debug legge i valori correnti direttamente dall'istanza dell'entità.

Contrasto con la visualizzazione di debug dopo aver chiamato 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}

Ora il blog è contrassegnato correttamente come Modified e il nuovo post è stato rilevato e viene monitorato come Added.

All'inizio di questa sezione è stato dichiarato che è necessario rilevare le modifiche quando non si usa EF Core per apportare la modifica. Questo è ciò che accade nel codice precedente. Vale a dire, le modifiche apportate alla proprietà e alla navigazione vengono apportate direttamente nelle istanze di entità e non usando metodi EF Core.

A differenza del codice seguente, che modifica le entità nello stesso modo, ma questa volta usando i metodi di 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);

In questo caso la visualizzazione di debug di Rilevamento modifiche mostra che tutti gli stati dell'entità e le modifiche alle proprietà sono noti, anche se il rilevamento delle modifiche non è stato eseguito. Questo perché PropertyEntry.CurrentValue è un metodo EF Core, il che significa che EF Core conosce immediatamente la modifica apportata da questo metodo. Analogamente, la chiamata DbContext.Add consente a EF Core di conoscere immediatamente la nuova entità e di monitorarla in modo appropriato.

Suggerimento

Non tentare di evitare di rilevare le modifiche usando sempre i metodi di EF Core per apportare modifiche alle entità. Questa operazione è spesso più complessa e offre prestazioni meno elevate rispetto a apportare modifiche alle entità nel modo normale. L'intenzione di questo documento è informare su quando è necessario rilevare le modifiche e quando non lo è. L'intento non è quello di incoraggiare l'evitare il rilevamento delle modifiche.

Metodi che rilevano automaticamente le modifiche

DetectChanges() viene chiamato automaticamente dai metodi in cui è probabile che ciò influisca sui risultati. Questi metodi sono:

Esistono anche alcune posizioni in cui il rilevamento delle modifiche avviene solo in una singola istanza di entità, anziché nell'intero grafico delle entità rilevate. Questi luoghi sono:

  • Quando si usa DbContext.Entry, per assicurarsi che lo stato dell'entità e le proprietà modificate siano aggiornati.
  • Quando si usano EntityEntry metodi come Property, CollectionReference o Member per assicurarsi che le modifiche alle proprietà, i valori correnti e così via siano aggiornati.
  • Quando un'entità dipendente/figlio verrà eliminata perché è stata interrotta una relazione obbligatoria. Questo rileva quando un'entità non deve essere eliminata perché è stata ri-padre.

Il rilevamento locale delle modifiche per una singola entità può essere attivato in modo esplicito chiamando EntityEntry.DetectChanges().

Nota

Le modifiche rilevate localmente possono perdere alcune modifiche rilevate da un rilevamento completo. Ciò si verifica quando le azioni a catena risultanti da modifiche non rilevate ad altre entità hanno un impatto sull'entità in questione. In tali situazioni l'applicazione potrebbe dover forzare un'analisi completa di tutte le entità chiamando ChangeTracker.DetectChanges()in modo esplicito .

Disabilitazione del rilevamento automatico delle modifiche

Le prestazioni del rilevamento delle modifiche non sono un collo di bottiglia per la maggior parte delle applicazioni. Tuttavia, il rilevamento delle modifiche può diventare un problema di prestazioni per alcune applicazioni che tengono traccia di migliaia di entità. Il numero esatto dipenderà da molti elementi, ad esempio il numero di proprietà nell'entità. Per questo motivo, il rilevamento automatico delle modifiche può essere disabilitato tramite ChangeTracker.AutoDetectChangesEnabled. Si consideri ad esempio l'elaborazione di entità join in una relazione molti-a-molti con i payload:

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

Come sappiamo dalla sezione precedente, sia che ChangeTracker.Entries<TEntity>()DbContext.SaveChanges rilevano automaticamente le modifiche. Tuttavia, dopo aver chiamato le voci, il codice non apporta modifiche allo stato di entità o proprietà. L'impostazione dei valori normali delle proprietà nelle entità aggiunte non comporta modifiche di stato. Il codice disabilita quindi il rilevamento automatico non necessario quando si chiama il metodo SaveChanges di base. Il codice usa anche un blocco try/finally per assicurarsi che l'impostazione predefinita venga ripristinata anche se SaveChanges ha esito negativo.

Suggerimento

Non presupporre che il codice debba disabilitare il rilevamento automatico delle modifiche per ottenere prestazioni buone. Questa operazione è necessaria solo quando si esegue la profilatura di un'applicazione che rileva molte entità indica che le prestazioni del rilevamento delle modifiche sono un problema.

Rilevamento di modifiche e conversioni di valori

Per usare il rilevamento delle modifiche dello snapshot con un tipo di entità, EF Core deve essere in grado di:

  • Creare uno snapshot di ogni valore della proprietà quando viene rilevata l'entità
  • Confrontare questo valore con il valore corrente della proprietà
  • Generare un codice hash per il valore

Questa operazione viene gestita automaticamente da EF Core per i tipi che possono essere mappati direttamente al database. Tuttavia, quando viene usato un convertitore di valori per eseguire il mapping di una proprietà, tale convertitore deve specificare come eseguire queste azioni. Questo risultato viene ottenuto con un operatore di confronto dei valori ed è descritto in dettaglio nella documentazione degli strumenti di confronto dei valori.

Entità di notifica

Il rilevamento delle modifiche dello snapshot è consigliato per la maggior parte delle applicazioni. Tuttavia, le applicazioni che tengono traccia di molte entità e/o apportano molte modifiche a tali entità possono trarre vantaggio dall'implementazione di entità che notificano automaticamente a EF Core quando cambiano le proprietà e i valori di navigazione. Queste entità sono note come "entità di notifica".

Implementazione di entità di notifica

Le entità di notifica usano le INotifyPropertyChanging interfacce e INotifyPropertyChanged , che fanno parte della libreria di classi di base .NET ( BCL). Queste interfacce definiscono eventi che devono essere attivati prima e dopo la modifica di un valore della proprietà. Ad esempio:

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

Inoltre, tutti gli spostamenti di raccolta devono implementare INotifyCollectionChanged; nell'esempio precedente questo è soddisfatto usando un ObservableCollection<T> di post. EF Core include anche un'implementazione ObservableHashSet<T> che offre ricerche più efficienti a scapito dell'ordinamento stabile.

La maggior parte di questo codice di notifica viene in genere spostata in una classe base non mappata. Ad esempio:

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

Configurazione delle entità di notifica

Ef Core non è in grado di verificare che INotifyPropertyChanging o INotifyPropertyChanged siano completamente implementati per l'uso con EF Core. In particolare, alcuni usi di queste interfacce lo fanno con le notifiche solo su determinate proprietà, anziché su tutte le proprietà (incluse le operazioni di spostamento) come richiesto da EF Core. Per questo motivo, EF Core non si collega automaticamente a questi eventi.

Ef Core deve invece essere configurato per usare queste entità di notifica. Questa operazione viene in genere eseguita per tutti i tipi di entità chiamando ModelBuilder.HasChangeTrackingStrategy. Ad esempio:

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

La strategia può anche essere impostata in modo diverso per i diversi tipi di entità usando EntityTypeBuilder.HasChangeTrackingStrategy, ma questo è in genere controproducente perché DetectChanges è ancora necessario per quei tipi che non sono entità di notifica.

Il rilevamento completo delle modifiche delle notifiche richiede l'implementazione di INotifyPropertyChanging e INotifyPropertyChanged . In questo modo è possibile salvare i valori originali subito prima della modifica del valore della proprietà, evitando la necessità di EF Core per creare uno snapshot durante il rilevamento dell'entità. I tipi di entità che implementano solo INotifyPropertyChanged possono essere usati anche con EF Core. In questo caso, Entity Framework crea ancora uno snapshot quando si tiene traccia di un'entità per tenere traccia dei valori originali, ma quindi usa le notifiche per rilevare immediatamente le modifiche, invece di dover chiamare DetectChanges.

I diversi ChangeTrackingStrategy valori sono riepilogati nella tabella seguente.

ChangeTrackingStrategy Interfacce necessarie Needs DetectChanges Snapshot dei valori originali
Snapshot None
ChangedNotifications INotifyPropertyChanged No
ChangingAndChangedNotifications INotifyPropertyChanged e INotifyPropertyChanging No No
ChangingAndChangedNotificationsWithOriginalValues INotifyPropertyChanged e INotifyPropertyChanging No

Uso delle entità di notifica

Le entità di notifica si comportano come qualsiasi altra entità, ad eccezione del fatto che le modifiche apportate alle istanze di entità non richiedono una chiamata a per ChangeTracker.DetectChanges() rilevare queste modifiche. Ad esempio:

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

Con le normali entità, la visualizzazione di debug di Rilevamento modifiche ha mostrato che queste modifiche non sono state rilevate fino a quando non è stato chiamato DetectChanges. Esaminando la visualizzazione di debug quando vengono usate entità di notifica, queste modifiche sono state rilevate immediatamente:

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 di rilevamento delle modifiche

EF Core può generare dinamicamente tipi proxy che implementano INotifyPropertyChanging e INotifyPropertyChanged. Ciò richiede l'installazione del pacchetto NuGet Microsoft.EntityFrameworkCore.Proxies e l'abilitazione dei proxy di rilevamento delle modifiche con UseChangeTrackingProxies Ad esempio:

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

La creazione di un proxy dinamico comporta la creazione di un nuovo tipo .NET dinamico (usando l'implementazione dei proxy Castle.Core ), che eredita dal tipo di entità e quindi esegue l'override di tutti i setter di proprietà. I tipi di entità per i proxy devono pertanto essere tipi che possono essere ereditati da e devono avere proprietà che possono essere sottoposte a override. Inoltre, gli spostamenti di raccolta creati in modo esplicito devono implementare INotifyCollectionChanged , ad esempio:

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

Uno svantaggio significativo dei proxy di rilevamento delle modifiche è che EF Core deve tenere sempre traccia delle istanze del proxy, mai istanze del tipo di entità sottostante. Ciò è dovuto al fatto che le istanze del tipo di entità sottostante non genereranno notifiche, il che significa che le modifiche apportate a queste entità non verranno perse.

EF Core crea automaticamente istanze proxy durante l'esecuzione di query sul database, pertanto questo svantaggio è generalmente limitato al rilevamento di nuove istanze di entità. Queste istanze devono essere create usando i metodi di CreateProxy estensione e non nel modo normale usando new. Ciò significa che il codice degli esempi precedenti deve ora usare 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);

Eventi di rilevamento delle modifiche

EF Core genera l'evento ChangeTracker.Tracked quando un'entità viene rilevata per la prima volta. Le modifiche future dello stato dell'entità generano ChangeTracker.StateChanged eventi. Per altre informazioni, vedere Eventi .NET in EF Core.

Nota

L'evento StateChanged non viene generato quando un'entità viene rilevata per la prima volta, anche se lo stato è passato da Detached a uno degli altri stati. Assicurarsi di restare in ascolto di entrambi StateChanged gli eventi e Tracked per ottenere tutte le notifiche pertinenti.