Partilhar via


Detecção e notificações de alterações

Cada instância DbContext controla as alterações feitas em entidades. Essas entidades controladas, por sua vez, conduzem as alterações ao banco de dados quando SaveChanges é chamado. Isso é abordado no Controle de Alterações no EF Core, e este documento pressupõe que os estados de entidade e os conceitos básicos do controle de alterações do EF Core (Entity Framework Core) sejam compreendidos.

O acompanhamento de alterações de propriedade e relação requer que o DbContext seja capaz de detectar essas alterações. Este documento aborda como essa detecção acontece, bem como usar notificações de propriedade ou proxies de controle de alterações para forçar a detecção imediata de alterações.

Dica

Você pode executar e depurar em todo o código neste documento baixando o código de exemplo do GitHub.

Controle de alterações de instantâneo

Por padrão, o EF Core cria um instantâneo dos valores de propriedade de cada entidade quando é rastreado pela primeira vez por uma instância DbContext. Os valores armazenados nesse instantâneo são comparados com os valores atuais da entidade para determinar quais valores de propriedade foram alterados.

Essa detecção de alterações ocorre quando SaveChanges é chamado para garantir que todos os valores alterados sejam detectados antes de enviar atualizações para o banco de dados. No entanto, a detecção de alterações também ocorre em outros momentos para garantir que o aplicativo esteja trabalhando com informações de acompanhamento atualizadas. A detecção de alterações pode ser forçada a qualquer momento chamando ChangeTracker.DetectChanges().

Quando a detecção de alterações é necessária

A detecção de alterações é necessária quando uma propriedade ou navegação é alterada sem usar o EF Core para fazer essa alteração. Por exemplo, considere carregar blogs e postagens e fazer alterações nessas entidades:

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

Examinar a exibição de depuração do rastreador de alterações antes da chamada ChangeTracker.DetectChanges() mostra que as alterações feitas não foram detectadas e, portanto, não são refletidas nos estados de entidade e nos dados de propriedade modificados:

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}

Especificamente, o estado da entrada do blog ainda é Unchanged, e a nova postagem não aparece como uma entidade controlada. (O astuto observará que as propriedades relatam seus novos valores, embora essas alterações ainda não tenham sido detectadas pelo EF Core. Isso ocorre porque a exibição de depuração está lendo os valores atuais diretamente da instância da entidade.)

Contraste isso com a exibição de depuração depois de chamar 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}

Agora o blog está marcado corretamente como Modified e a nova postagem foi detectada e é rastreada como Added.

No início desta seção, afirmamos que a detecção de alterações é necessária quando não utilizamos o Entity Framework Core para fazer as alterações. Isso é o que está acontecendo no código acima. Ou seja, as alterações na propriedade e na navegação são feitas diretamente nas instâncias de entidade, e não usando nenhum método EF Core.

Contraste isso com o código a seguir que modifica as entidades da mesma maneira, mas desta vez usando métodos 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);

Nesse caso, a exibição de depuração do rastreador de alterações mostra que todos os estados de entidade e modificações de propriedade são conhecidos, mesmo que a detecção de alterações não tenha acontecido. Isso ocorre porque PropertyEntry.CurrentValue é um método EF Core, o que significa que o EF Core imediatamente sabe sobre a alteração feita por esse método. Da mesma forma, a chamada DbContext.Add permite que o EF Core saiba imediatamente sobre a nova entidade e rastreie-a adequadamente.

Dica

Não tente evitar a detecção de alterações sempre usando métodos EF Core para fazer alterações de entidade. Isso geralmente é mais complicado e tem menos desempenho do que fazer alterações em entidades da maneira normal. A intenção deste documento é informar sobre quando a detecção de alterações é necessária e quando não é. A intenção não é incentivar a prevenção da detecção de alterações.

Métodos que detectam alterações automaticamente

DetectChanges() é chamado automaticamente por métodos em que isso provavelmente afetará os resultados. Esses métodos são:

Há também alguns locais em que a detecção de alterações ocorre apenas em uma única instância de entidade, em vez de em todo o grafo de entidades controladas. Estes locais são:

  • Ao usar DbContext.Entry, para garantir que o estado da entidade e as propriedades modificadas estejam atualizados.
  • Ao usar métodos EntityEntry como Property, Collection, Reference ou Member para garantir modificações de propriedade, valores atuais etc. estão atualizados.
  • Quando uma entidade dependente/filho será excluída porque uma relação necessária foi cortada. Isso detecta quando uma entidade não deve ser excluída porque foi novamente pai.

A detecção local de alterações para uma única entidade pode ser disparada explicitamente chamando EntityEntry.DetectChanges().

Observação

As alterações de detecção local podem perder algumas alterações que uma detecção completa encontraria. Isso acontece quando ações em cascata resultantes de alterações não detectadas em outras entidades têm um impacto sobre a entidade em questão. Nessas situações, o aplicativo pode precisar forçar uma verificação completa de todas as entidades chamando explicitamente ChangeTracker.DetectChanges().

Desabilitando a detecção automática de alterações

O desempenho da detecção de alterações não é um gargalo para a maioria dos aplicativos. No entanto, detectar alterações pode se tornar um problema de desempenho para alguns aplicativos que rastreiam milhares de entidades. (O número exato dependerá de muitas coisas, como o número de propriedades na entidade.) Por esse motivo, a detecção automática de alterações pode ser desabilitada usando ChangeTracker.AutoDetectChangesEnabled. Por exemplo, considere o processamento de entidades de junção em uma relação muitos para muitos com cargas:

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

Como sabemos da seção anterior, ambos ChangeTracker.Entries<TEntity>() e DbContext.SaveChanges detectam alterações automaticamente. No entanto, depois de chamar Entradas, o código não faz nenhuma alteração de estado de entidade ou propriedade. (Definir valores de propriedade normais em entidades adicionadas não causa nenhuma alteração de estado.) O código, portanto, desabilita a detecção de alterações automáticas desnecessárias ao chamar o método SaveChanges base. O código também usa um bloco try/finally para garantir que a configuração padrão seja restaurada mesmo se SaveChanges falhar.

Dica

Não suponha que seu código desabilite a detecção automática de alterações para ter um bom desempenho. Isso só é necessário ao criar a criação de perfil de um aplicativo que rastreia muitas entidades indica que o desempenho da detecção de alterações é um problema.

Detectando alterações e conversões de valor

Para usar o controle de alterações de instantâneo com um tipo de entidade, o EF Core deve ser capaz de:

  • Fazer um instantâneo de cada valor de propriedade quando a entidade é rastreada
  • Compare esse valor com o valor atual da propriedade
  • Gerar um código hash para o valor

Isso é tratado automaticamente pelo EF Core para tipos que podem ser mapeados diretamente para o banco de dados. No entanto, quando um conversor de valor é usado para mapear uma propriedade, esse conversor deve especificar como executar essas ações. Isso é obtido com um comparador de valor e é descrito em detalhes na documentação do Value Comparers.

Entidades de notificação

O controle de alterações de instantâneo é recomendado para a maioria dos aplicativos. No entanto, os aplicativos que acompanham muitas entidades e/ou fazem muitas alterações nessas entidades podem se beneficiar da implementação de entidades que notificam automaticamente o EF Core quando seus valores de propriedade e navegação são alterados. Elas são conhecidas como "entidades de notificação".

Implementando entidades de notificação

As entidades de notificação usam as interfaces INotifyPropertyChanging e INotifyPropertyChanged, que fazem parte da biblioteca de classes básicas (BCL) do .NET. Essas interfaces definem eventos que devem ser disparados antes e depois de alterar um valor de propriedade. Por exemplo:

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

Além disso, todas as navegações de coleção devem implementar INotifyCollectionChanged; no exemplo acima, isso é satisfeito com o uso de ObservableCollection<T> de posts. O EF Core também é fornecido com uma implementação de ObservableHashSet<T> que tem pesquisas mais eficientes em detrimento da ordenação estável.

A maior parte desse código de notificação normalmente é movida para uma classe base não mapeada. Por exemplo:

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

Configurando entidades de notificação

Não há como o EF Core validar isso INotifyPropertyChanging ou INotifyPropertyChanged ser totalmente implementado para uso com o EF Core. Em particular, alguns usos dessas interfaces fazem isso com notificações apenas em determinadas propriedades, em vez de em todas as propriedades (incluindo navegações), conforme exigido pelo EF Core. Por esse motivo, o EF Core não se conecta automaticamente a esses eventos.

Em vez disso, o EF Core deve ser configurado para usar essas entidades de notificação. Isso geralmente é feito para todos os tipos de entidade chamando ModelBuilder.HasChangeTrackingStrategy. Por exemplo:

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

(A estratégia também pode ser definida de forma diferente para diferentes tipos de entidade usando EntityTypeBuilder.HasChangeTrackingStrategy, mas isso geralmente é contraproducente, pois DetectChanges ainda é necessário para os tipos que não são entidades de notificação.)

O controle de alterações de notificação completa requer que ambos INotifyPropertyChanging e INotifyPropertyChanged sejam implementados. Isso permite que os valores originais sejam salvos antes que o valor da propriedade seja alterado, evitando a necessidade de o EF Core criar um instantâneo ao acompanhar a entidade. Os tipos de entidade que implementam só INotifyPropertyChanged podem ser usados com o EF Core. Nesse caso, o EF ainda cria um instantâneo ao rastrear uma entidade para controlar os valores originais, mas usa as notificações para detectar alterações imediatamente, em vez de precisar que DetectChanges seja chamado.

Os valores diferentes ChangeTrackingStrategy são resumidos na tabela a seguir.

ChangeTrackingStrategy Interfaces necessárias Precisa de DetectChanges Valores originais de instantâneos
Instantâneo Nenhum Sim Yes
ChangedNotifications INotifyPropertyChanged Não Sim
ChangingAndChangedNotifications INotifyPropertyChanged e INotifyPropertyChanging Não Não
ChangingAndChangedNotificationsWithOriginalValues INotifyPropertyChanged e INotifyPropertyChanging Não Sim

Usando entidades de notificação

Entidades de notificação se comportam como qualquer outra entidade, exceto que fazer alterações nas instâncias de entidade não exigem uma chamada para ChangeTracker.DetectChanges() detectar essas alterações. Por exemplo:

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

Com entidades normais, a exibição de depuração do rastreador de alterações mostrou que essas alterações não foram detectadas até que DetectChanges fosse chamado. Examinar a exibição de depuração quando entidades de notificação são usadas mostra que essas alterações foram detectadas imediatamente:

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}

Proxies de controle de alterações

O EF Core pode gerar dinamicamente tipos de proxy que implementam INotifyPropertyChanging e INotifyPropertyChanged. Isso requer a instalação do pacote NuGet Microsoft.EntityFrameworkCore.Proxies e a habilitação de proxies de controle de alterações com UseChangeTrackingProxies Por exemplo:

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

A criação de um proxy dinâmico envolve a criação de um novo tipo .NET dinâmico (usando a implementação de proxies Castle.Core), que herda do tipo de entidade e, em seguida, substitui todos os setters de propriedade. Os tipos de entidade para proxies devem, portanto, ser tipos dos quais podem ser herdados e devem ter propriedades que possam ser substituídas. Além disso, as navegações de coleção criadas explicitamente devem implementar INotifyCollectionChanged Por exemplo:

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

Uma desvantagem significativa para proxies de controle de alterações é que o EF Core sempre deve acompanhar instâncias do proxy, nunca instâncias do tipo de entidade subjacente. Isso ocorre porque as instâncias do tipo de entidade subjacente não gerarão notificações, o que significa que as alterações feitas nessas entidades serão perdidas.

O EF Core cria instâncias de proxy automaticamente ao consultar o banco de dados, portanto, essa desvantagem geralmente é limitada ao acompanhamento de novas instâncias de entidade. Essas instâncias devem ser criadas usando os métodos CreateProxy de extensão e não da maneira normal usando new. Isso significa que o código dos exemplos anteriores agora deve usar 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);

Alterar eventos de controle

O EF Core aciona o evento ChangeTracker.Tracked quando uma entidade é rastreada pela primeira vez. Futuras alterações de estado de entidade resultam em eventos ChangeTracker.StateChanged. Confira Eventos .NET no EF Core para obter mais informações.

Observação

O evento StateChanged não é acionado quando uma entidade é rastreada pela primeira vez, embora o estado tenha sido alterado de Detached para um dos outros estados. Certifique-se de ouvir os eventos StateChanged e Tracked para obter todas as notificações relevantes.