Detección y notificaciones de cambios
Cada instancia de DbContext realiza un seguimiento de los cambios realizados en las entidades. Estas entidades de las que se realiza un seguimiento, a su vez, impulsan los cambios en la base de datos cuando se llama a SaveChanges. Esto se trata en Change Tracking en EF Corey 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 en 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 se realiza el seguimiento por primera vez de una instancia de DbContext. 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 garantizar que se detectan todos los valores modificados antes de enviar las actualizaciones a la base de datos. Sin embargo, la detección de cambios también se produce en otros momentos para garantizar que la aplicación funciona con información de seguimiento actualizada. 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 = 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);
Al examinar la vista de depuración de seguimiento de cambios antes de llamar a ChangeTracker.DetectChanges(), se muestra que no se han detectado los cambios realizados 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 Unchanged
y la nueva entrada no aparece como una entidad con seguimiento. (Los más perspicaces se darán cuenta de que las propiedades informan de sus nuevos valores, aunque estos cambios aún no hayan sido detectados por el núcleo de EF. Esto se debe a que la vista de depuración está leyendo los valores actuales directamente de 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 indicó 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 realizandirectamente 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 = 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);
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 del EF Core para realizar cambios en las entidades. Hacerlo así suele ser más complicado y funciona peor que realizar cambios en las entidades de la forma habitual. La intención de este documento es informar sobre cuándo se necesitan detectar cambios y cuándo no. La intención no es fomentar que se evite 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:
- DbContext.SaveChanges y DbContext.SaveChangesAsync, para asegurarse de que se detectan todos los cambios antes de actualizar la base de datos.
- ChangeTracker.Entries() y ChangeTracker.Entries<TEntity>(), para asegurarse de que los estados de entidad y las propiedades modificadas están actualizados.
- ChangeTracker.HasChanges(), para asegurarse de que el resultado es preciso.
- ChangeTracker.CascadeChanges(), para asegurarse de que los estados de entidad correctos para las entidades principales o primarias antes de la cascada.
- DbSet<TEntity>.Local, para asegurarse de que el gráfico con seguimiento está actualizado.
También hay algunos lugares en los que la detección de cambios se produce sólo en una 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 actualizados.
- Cuando se usan EntityEntry métodos como
Property
,Reference
Collection
oMember
para asegurarse de que las modificaciones de propiedad, los valores actuales, etc. están actualizadas. - Cuando se va a eliminar una entidad dependiente o secundaria porque se ha porque se ha interrumpido una relación necesaria. Esto detecta cuándo no se debe eliminar una entidad porque se ha vuelto a crear como elemento primario.
La detección local de cambios para una sola entidad se puede desencadenar explícitamente mediante una llamada a EntityEntry.DetectChanges().
Nota:
La detección local de cambios puede pasar por alto algunos cambios que una detección completa sí 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 que necesite 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 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 de la sección anterior, tantoChangeTracker.Entries<TEntity>() como DbContext.SaveChanges detectan los cambios automáticamente. 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 instantáneas 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 de 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 interfaces de INotifyPropertyChanging 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 recopilación deben implementar INotifyCollectionChanged
; en el ejemplo anterior, esto se cumple con el uso de una ObservableCollection<T> de publicaciones. EF Core también se incluye con una ObservableHashSet<T> implementación que tiene búsquedas más eficaces a costa de un ordenamiento 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 esto 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 realizar un seguimiento de una entidad para realizar un seguimiento de los valores originales, pero después usa las notificaciones para detectar los cambios inmediatamente, en lugar de necesitar que se llame a DetectChanges.
Los distintos ChangeTrackingStrategy valores se resumen en la tabla siguiente.
ChangeTrackingStrategy | Interfaces necesarias | Necesita DetectChanges | Instantáneas de valores originales |
---|---|---|---|
Depurador de | Ninguno | Sí | Sí |
ChangedNotifications | INotifyPropertyChanged | No | Sí |
ChangingAndChangedNotifications | INotifyPropertyChanged e INotifyPropertyChanging | No | No |
ChangingAndChangedNotificationsWithOriginalValues | INotifyPropertyChanged e INotifyPropertyChanging | No | Sí |
Uso de entidades de notificación
Las entidades de notificación se comportan como cualquier otra entidad, salvo que realizar cambios en las instancias de entidad no requieren una llamada a ChangeTracker.DetectChanges() para detectar estos cambios. Por ejemplo:
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 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 servidores proxy Castle.Core ), que hereda del tipo de entidad y, a continuación, invalida todos los establecedores de propiedades. Por lo tanto, los tipos de entidad para los proxies deben ser tipos de los que se pueda heredar y deben tener propiedades que se puedan 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 mediante los métodos de extensión CreateProxy y no de la manera normal mediante new
. Esto significa que el código de los ejemplos anteriores debe usar ahora 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);
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 entidad dan lugar a eventos ChangeTracker.StateChanged. Consulte Eventos de .NET en EF Core para obtener más información.
Nota:
El evento StateChanged
no se desencadena cuando se realiza un seguimiento por primera vez de una entidad, aunque el estado haya cambiado de Detached
a uno de los otros estados. Asegúrese de prestar atención a ambos eventos StateChanged
y Tracked
para obtener todas las notificaciones pertinentes.