Trazar cambios en entidades de EF Core

  • net-core
  • ef-core

Hace unos días, me surgió la necesidad de crear una traza que contuviese todos los cambios que se iban realizando en una base de datos de EF. Tras mirar cómo se hacía en distintas aplicaciones, me centré en conseguir una aproximación similar a la que se hace en VSTS (Ahora conocido como Azure DevOps), el cual registra todos los cambios a nivel de propiedad, permitiendote ver el valor anterior y el valor nuevo.

Conseguir esta traza no es muy complicado, aunque aviso de que la implementación que aquí voy a mostrar no traza inserciones ni borrados y traza los cambios de entidades que tienen un único campo como clave primaria.

Una vez aclarado el alcance de la solución que voy a mostrar, vamos entrar en materia. A continuación detallo los pasos que he seguido.

Crear modelo

Para el ejemplo, se va a partir de un modelo con una única clase, ya que con eso es suficiente para mostrar la funcionalidad que nos ocupa. La única clase del modelo, es la clase personas, con las propiedades que a continuación se detallan.

public class PersonEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string PhoneNumber { get; set; }
    public string Email { get; set; }
}

Además de nuestro modelo, es necesario añadir una nueva clase que almacenará el log, tal y como se muestra a continuación

public class LogEntity
{
    public int Id { get; set; }
    public string EntityName { get; set; }
    public string PropertyName { get; set; }
    public string PrimaryKeyValue { get; set; }
    public string OldValue { get; set; }
    public string NewValue { get; set; }
    public DateTime DateChanged { get; set; }
}

Modificar DbContext

Al añadir la clase LogEntity, es necesario añadirla al contexto. Bastará con incluir la colección Logs

public DbSet<LogEntity> Logs { get; set; }

Loguear los cambios

Una vez que se tiene toda la infraestructura necesaria, lo único que falta es trazar todos los cambios que se produzcan y guardarlos en la colección de Logs. Para ello, es necesario sobrescribir el/los métodos SaveChanges que se utilicen del DbContext. En este caso, se ha sobrescrito sólo SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)), que es el que se va a utilizar. No se detalla la sobrescritura de otros métodos SaveChanges ya que se haría de una manera totalmente análoga.

public override async Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
    ProcessLog();
    return await base.SaveChangesAsync(cancellationToken);
}

private void ProcessLog()
{
    var modifiedEntities = ChangeTracker.Entries().Where(p => p.State == EntityState.Modified).ToList();
    var now = DateTime.UtcNow;

    foreach (var change in modifiedEntities)
    {
        var entityName = change.Entity.GetType().Name;
        var primaryKey = GetPrimaryKeyValue(change);

        foreach (var prop in change.OriginalValues.Properties)
        {
            var originalValue = change.OriginalValues[prop.Name].ToString();
            var currentValue = change.CurrentValues[prop.Name].ToString();
            if (originalValue != currentValue)
            {
                LogEntity log = new LogEntity()
                {
                    EntityName = entityName,
                    PrimaryKeyValue = primaryKey.ToString(),
                    PropertyName = prop.Name,
                    OldValue = originalValue,
                    NewValue = currentValue,
                    DateChanged = now
                };
                Logs.Add(log);
            }
        }
    }
}

private object GetPrimaryKeyValue(EntityEntry entry)
{
    var keyName = this.Model.FindEntityType(entry.Entity.GetType()).FindPrimaryKey().Properties.Select(x => x.Name).Single();
    return entry.CurrentValues[keyName];
}

El método ProcessLog es el encargado de buscar todos los cambios que hay en el contexto e ir añadiéndolos al log. Éste método se apoya en GetPrimaryKeyValue para obtener el valor de la clave primaria de una entidad modificada. Tal y como se indico al principio del post, este ejemplo no funcionaría si los objetos modificados no tienen clave primaria.

 

Aquí os dejo un enlace en el que encontrar un proyecto de ejemplo con una implementación completa del ejemplo.

https://github.com/jorgediegocrespo/TraceEfChanges

Un saludo!!