Compartir a través de


Detección y notificaciones de cambios

Cada DbContext instancia realiza un seguimiento de los cambios realizados en las entidades. Estas entidades con seguimiento impulsan cambios en la base de datos cuando se llama a SaveChanges. Esto se trata en Change Tracking en EF Core y en este documento se da por supuesto que se comprenden los estados de entidad y los conceptos básicos del seguimiento de cambios de Entity Framework Core (EF Core).

El seguimiento de los cambios de propiedad y relación requiere que DbContext pueda detectar estos cambios. En este documento se explica cómo se produce esta detección, así como cómo usar notificaciones de propiedades o servidores proxy de seguimiento de cambios para forzar la detección inmediata de cambios.

Sugerencia

Puede ejecutar y depurar todo el código de este documento descargando el código de ejemplo de GitHub.

Seguimiento de cambios de instantánea

De forma predeterminada, EF Core crea una instantánea de los valores de propiedad de cada entidad cuando es seguida por una instancia de DbContext por primera vez. Los valores almacenados en esta instantánea se comparan con los valores actuales de la entidad para determinar qué valores de propiedad han cambiado.

Esta detección de cambios se produce cuando se llama a SaveChanges para asegurarse de que se detectan todos los valores modificados antes de enviar actualizaciones a la base de datos. Sin embargo, la detección de cambios también se produce en otros momentos para asegurarse de que la aplicación funciona con up-toinformación de seguimiento de fechas. La detección de cambios se puede forzar en cualquier momento llamando a ChangeTracker.DetectChanges().

Cuando se necesita la detección de cambios

Se necesita la detección de cambios cuando se ha cambiado una propiedad o navegación sin usar EF Core para realizar este cambio. Por ejemplo, considere la posibilidad de cargar blogs y publicaciones y, a continuación, realizar cambios en estas entidades:

using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(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);

Al observar la vista de depuración de seguimiento de cambios antes de llamar a ChangeTracker.DetectChanges(), se observa que los cambios realizados no han sido detectados y, por lo tanto, no se reflejan en los estados de entidad y los datos de propiedad 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}

En concreto, el estado de la entrada de blog sigue siendo Unchangedy la nueva entrada no aparece como una entidad con seguimiento. (Los astutos notarán que las propiedades informan sus nuevos valores, incluso aunque estos cambios aún no hayan sido detectados por EF Core. Esto se debe a que la vista de depuración está leyendo los valores actuales directamente desde la instancia de la entidad).

Compare esto con la vista de depuración después de llamar a 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}

Ahora el blog está marcado correctamente como Modified y se ha detectado la nueva entrada y se realiza el seguimiento como Added.

Al principio de esta sección se ha indicado que es necesario detectar cambios cuando no se usa EF Core para realizar el cambio. Esto es lo que sucede en el código anterior. Es decir, los cambios realizados en la propiedad y la navegación se realizan directamente en las instancias de entidad y no mediante ningún método de EF Core.

Compare esto con el código siguiente, que modifica las entidades de la misma manera, pero esta vez con los métodos de EF Core:

using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(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);

En este caso, la vista de depuración del rastreador de cambios muestra que se conocen todos los estados de entidad y las modificaciones de propiedad, aunque no se haya producido la detección de cambios. Esto se debe a PropertyEntry.CurrentValue que es un método de EF Core, lo que significa que EF Core conoce inmediatamente el cambio realizado por este método. Del mismo modo, la llamada DbContext.Add permite a EF Core conocer inmediatamente la nueva entidad y realizar un seguimiento adecuado.

Sugerencia

No intente evitar la detección de cambios utilizando siempre métodos de EF Core para modificar entidades. Hacerlo suele ser más complicado y funciona menos que realizar cambios en las entidades de la manera normal. La intención de este documento es informar sobre cuándo se necesitan detectar cambios y cuándo no. La intención es no fomentar la prevención de la detección de cambios.

Métodos que detectan automáticamente los cambios

DetectChanges() se llama automáticamente mediante métodos en los que es probable que esto afecte a los resultados. Estos métodos son:

También hay algunos lugares en los que la detección de cambios se produce solo en una sola instancia de entidad, en lugar de en todo el gráfico de entidades con seguimiento. Estos lugares son:

  • Al usar DbContext.Entry, para asegurarse de que el estado y las propiedades modificadas de la entidad están up-to-date.
  • Al usar EntityEntry métodos como Property, CollectionReference o Member para asegurarse de que las modificaciones de propiedad, los valores actuales, etc. son up-to-date.
  • Cuando se va a eliminar una entidad dependiente o secundaria porque se ha roto una relación necesaria. Esto detecta cuándo no se debe eliminar una entidad porque se ha reasignado de jerarquía.

La detección local de cambios para una sola entidad se puede desencadenar explícitamente mediante una llamada a EntityEntry.DetectChanges().

Nota:

Los cambios locales detectados pueden pasar por alto algunos cambios que una detección completa encontraría. Esto sucede cuando las acciones en cascada resultantes de cambios no detectados en otras entidades tienen un impacto en la entidad en cuestión. En tales situaciones, la aplicación puede necesitar forzar un examen completo de todas las entidades llamando explícitamente a ChangeTracker.DetectChanges().

Deshabilitación de la detección automática de cambios

El rendimiento de la detección de cambios no es un cuello de botella para la mayoría de las aplicaciones. Sin embargo, la detección de cambios puede convertirse en un problema de rendimiento para algunas aplicaciones que realizan un seguimiento de miles de entidades. (El número exacto dependerá de muchas cosas, como el número de propiedades de la entidad). Por este motivo, la detección automática de cambios se puede deshabilitar mediante ChangeTracker.AutoDetectChangesEnabled. Por ejemplo, considere la posibilidad de procesar entidades de combinación en una relación de varios a varios con cargas:

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    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 await base.SaveChangesAsync(cancellationToken); // Avoid automatically detecting changes again here
    }
    finally
    {
        ChangeTracker.AutoDetectChangesEnabled = true;
    }
}

Como sabemos de la sección anterior, tanto ChangeTracker.Entries<TEntity>() como DbContext.SaveChanges detectan automáticamente los cambios. Sin embargo, después de llamar a Entradas, el código no realiza ningún cambio de estado de entidad o propiedad. (Si se establecen valores de propiedad normales en entidades agregadas, no se producen cambios de estado). Por lo tanto, el código deshabilita la detección de cambios automática innecesaria al llamar al método Base SaveChanges. El código también usa un bloque try/finally para asegurarse de que la configuración predeterminada se restaura incluso si Se produce un error en SaveChanges.

Sugerencia

No suponga que el código debe deshabilitar la detección automática de cambios para que funcione bien. Esto solo es necesario cuando la generación de perfiles de una aplicación de seguimiento de muchas entidades indica que el rendimiento de la detección de cambios es un problema.

Detección de cambios y conversiones de valores

Para usar el seguimiento de cambios de capturas con un tipo de entidad, EF Core debe poder:

  • Realizar una instantánea de cada valor de propiedad cuando se realiza el seguimiento de la entidad
  • Compare este valor con el valor actual de la propiedad.
  • Generación de un código hash para el valor

Ef Core controla automáticamente esto para los tipos que se pueden asignar directamente a la base de datos. Sin embargo, cuando se usa un convertidor de valores para asignar una propiedad, ese convertidor debe especificar cómo realizar estas acciones. Esto se logra con un comparador de valores y se describe con detalle en la documentación de Comparadores de valores.

Entidades de notificación

Se recomienda el seguimiento de cambios mediante instantáneas para la mayoría de las aplicaciones. Sin embargo, las aplicaciones que realizan un seguimiento de muchas entidades o realizan muchos cambios en esas entidades pueden beneficiarse de la implementación de entidades que notifican automáticamente a EF Core cuando cambian sus valores de propiedad y navegación. Estos se conocen como "entidades de notificación".

Implementación de entidades de notificación

Las entidades de notificación usan las INotifyPropertyChanging interfaces y INotifyPropertyChanged , que forman parte de la biblioteca de clases base (BCL) de .NET. Estas interfaces definen eventos que se deben desencadenar antes y después de cambiar un valor de propiedad. Por ejemplo:

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

Además, las navegaciones de colección deben implementar INotifyCollectionChanged; en el ejemplo anterior, esto se cumple mediante un ObservableCollection<T> elemento de publicaciones. EF Core también se suministra con una ObservableHashSet<T> implementación que tiene búsquedas más eficaces a costa de un orden estable.

La mayoría de este código de notificación se mueve normalmente a una clase base no asignada. Por ejemplo:

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

Configuración de entidades de notificación

No hay forma de que EF Core valide que INotifyPropertyChanging o INotifyPropertyChanged se implementen completamente para su uso con EF Core. En concreto, algunos usos de estas interfaces lo hacen solo con notificaciones en determinadas propiedades, en lugar de en todas las propiedades (incluidas las navegaciones) según lo requiera EF Core. Por este motivo, EF Core no enlaza automáticamente estos eventos.

En su lugar, EF Core debe configurarse para usar estas entidades de notificación. Normalmente, esto se hace para todos los tipos de entidad llamando a ModelBuilder.HasChangeTrackingStrategy. Por ejemplo:

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

(La estrategia también se puede establecer de forma diferente para los distintos tipos de entidad mediante EntityTypeBuilder.HasChangeTrackingStrategy, pero suele ser contraproductivo, ya que DetectChanges sigue siendo necesario para esos tipos que no son entidades de notificación).

El seguimiento completo de cambios de notificación requiere que se implementen tanto INotifyPropertyChanging como INotifyPropertyChanged. Esto permite guardar los valores originales justo antes de cambiar el valor de la propiedad, evitando la necesidad de que EF Core cree una instantánea al realizar el seguimiento de la entidad. Los tipos de entidad que implementan solo INotifyPropertyChanged se pueden usar con EF Core. En este caso, EF sigue creando una instantánea al rastrear una entidad para registrar los valores originales, pero luego utiliza las notificaciones para detectar cambios de inmediato, en lugar de requerir que se llame a DetectChanges.

Los distintos ChangeTrackingStrategy valores se resumen en la tabla siguiente.

Estrategia de Seguimiento de Cambios Interfaces necesarias Necesita DetectChanges Capturas de los valores originales
Instantánea Ninguno
NotificacionesCambiadas INotifyPropertyChanged No
NotificacionesDeCambioYCambiadas INotifyPropertyChanged e INotifyPropertyChanging No No
NotificacionesDeCambioYCambiadoConValoresOriginales INotifyPropertyChanged e INotifyPropertyChanging No

Uso de entidades de notificación

Las entidades de notificación se comportan como cualquier otra entidad, salvo que no es necesario realizar una llamada a ChangeTracker.DetectChanges() para detectar estos cambios en las instancias de entidad. Por ejemplo:

using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(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 las entidades normales, la vista de depuración de seguimiento de cambios mostró que estos cambios no se detectaron hasta que se llamó a DetectChanges. Al examinar la vista de depuración cuando se usan entidades de notificación se muestra que estos cambios se han detectado inmediatamente:

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 seguimiento de cambios

EF Core puede generar dinámicamente tipos de proxy que implementan INotifyPropertyChanging y INotifyPropertyChanged. Esto requiere instalar el paquete NuGet Microsoft.EntityFrameworkCore.Proxies y habilitar servidores proxy de seguimiento de cambios con UseChangeTrackingProxies Por ejemplo:

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

La creación de un proxy dinámico implica crear un nuevo tipo de .NET dinámico (mediante la implementación de proxies de Castle.Core), que hereda del tipo de entidad y, a continuación, sobrescribe todos los métodos de establecimiento de propiedades. Por lo tanto, los tipos de entidad para servidores proxy deben ser tipos que se pueden heredar de y deben tener propiedades que se pueden invalidar. Además, las navegaciones de recopilación creadas explícitamente deben implementar INotifyCollectionChanged por ejemplo:

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

Una desventaja importante de los servidores proxy de seguimiento de cambios es que EF Core siempre debe realizar un seguimiento de las instancias del proxy, nunca las instancias del tipo de entidad subyacente. Esto se debe a que las instancias del tipo de entidad subyacente no generarán notificaciones, lo que significa que se perderán los cambios realizados en estas entidades.

EF Core crea instancias de proxy automáticamente al consultar la base de datos, por lo que este inconveniente suele limitarse a realizar el seguimiento de nuevas instancias de entidad. Estas instancias deben crearse usando los CreateProxy métodos de extensión y no de la manera habitual con new. Esto significa que el código de los ejemplos anteriores debe usar CreateProxyahora :

using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(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);

Eventos de seguimiento de cambios

EF Core desencadena el ChangeTracker.Tracked evento cuando se realiza un seguimiento de una entidad por primera vez. Los cambios futuros en el estado de la entidad dan lugar a eventos ChangeTracker.StateChanged. Consulte Eventos de .NET en EF Core para obtener más información.

Nota:

El StateChanged evento no se desencadena cuando una entidad es rastreada por primera vez, aunque el estado haya cambiado de Detached a uno de los otros estados. Asegúrese de escuchar eventos StateChanged y Tracked para obtener todas las notificaciones pertinentes.