Añadiendo trazas a nuestros registros de base de datos con EF

  • net-core
  • ef-core
  • tips
  • net
  • api
  • asp-net
  • entity-framework
  • trace

Lo que pretendo con este post es mostrar como podemos dejar una pequeña traza de las operaciones realizadas sobre nuestros registros de base de datos con Entity Framework. Digo pequeña, porque no puede considerarse que, con esta implementación, tengamos un sistema auditable. No obstante, puede ser un buen complemento a ese.

Lo que vamos a conseguir es almacenar en la base de datos, asociado a cada registro, la fecha de creación, modificación y borrado, además del usuario que realiza esta operación.

Creando una entidad base

Si en nuestro proyecto no tenemos una entidad base de la que heredan todas las clases que definen las entidades de nuestra base de datos, en un buen momento para crearla.

Para este ejemplo, vamos a añadir propiedades para trazar toda la información comentada anteriormente, por lo que nuestra entidad base quedaría de la siguiente manera.

public abstract class BaseEntity
{
    public DateTime DateOfCreation { get; set; }
    public string CreationUser { get; set; }
    public DateTime? DateOfLastUpdate { get; set; }
    public string? LastUpdateUser { get; set; }
    public DateTime? DateOfDelete { get; set; }
    public string? DeleteUser { get; set; }
}

Como podemos ver en el código anterior, las trazas asociadas a la creación de un registro no admiten nulos. Sin embargo, si que se admiten para la modificación y borrado, porque, obviamente, es posible que un registro, tras ser creado, no haya sido modificado ni borrado.

Si no teníamos una clase base ya creada, en el momento de hacer que todas nuestra entidades hereden de la clase que acabamos de crear, con lo que vamos a conseguir que todas las tablas de nuestra base de datos tengan la información asociada a las trazas.

Para conseguir que nuestros datos se materialicen un nuestra base de datos, es necesario crear una migración con los cambios anteriores y aplicarla.

dotnet ef migrations add BasicTracing
dotnet ef database update

Rellenando información

Para guardar toda la información añadida en el paso anterior, sin necesidad de modificar todas nuestras consultas a base de datos que escriben en esta, vamos a añadir esta funcionalidad a nuestro contexto, de la forma que se muestra a continuación.

public class TasksDbContext : IdentityDbContext<UserEntity>
{
    private readonly IHttpContextService _httpContextService;
    public TasksDbContext(IHttpContextService httpContextService, DbContextOptions options) : base(options)
    {
        _httpContextService = httpContextService;
    }

    ...

    public override int SaveChanges()
    {
        UpdateBaseEntityInfo();
        return base.SaveChanges();
    }

    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        UpdateBaseEntityInfo();
        return base.SaveChanges(acceptAllChangesOnSuccess);
    }

    public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
    {
        UpdateBaseEntityInfo();
        return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }

    public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        UpdateBaseEntityInfo();
        return base.SaveChangesAsync(cancellationToken);
    }

    private void UpdateBaseEntityInfo()
    {
        foreach (EntityEntry entry in ChangeTracker.Entries())
        {
            BaseEntity baseEntity = entry.Entity as BaseEntity;
            if (baseEntity == null)
                return;

            switch (entry.State)
            {
                case EntityState.Deleted:
                    baseEntity.DeleteUser = _httpContextService.GetContextUser().Id;
                    baseEntity.DateOfDelete = DateTime.UtcNow;
                    break;
                case EntityState.Added:
                    baseEntity.CreationUser = _httpContextService.GetContextUser().Id;
                    baseEntity.DateOfCreation = DateTime.UtcNow;
                    break;

                case EntityState.Modified:
                    baseEntity.LastUpdateUser = _httpContextService.GetContextUser().Id;
                    baseEntity.DateOfLastUpdate = DateTime.UtcNow;
                    break;
            }
        }
    }
}

Lo primero que vemos es que en el constructor se inyecta IHttpContextService. Este servicio es un wrapper de IHttpContextAccessor que usamos para obtener la información del usuario que está realizando la petición a nuestro API. No voy a entrar en detalles de esta implementación, ya que los di en pasados posts y también lo podéis encontrar en el repositorio al que enlazo al final de este artículo.

Lo siguiente que podemos apreciar en el código es que se han sobre escrito todos los métodos SaveChanges, con la finalidad de rellenar la información de auditoría que hemos añadido a nuestras entidades. Para conseguirlo, llamamos al método UpdateBaseEntityInfo, en el que recorremos todos los cambios que se van a hacer y, dependiendo de la operación que se realice sobre las distintas entidades, se almacena la información que nos permite mantener una traza.

Talk is cheap, show me the code

En este caso, os dejo un enlace a un repo en el que implemento distintas funcionalidades de asp.net, entre ellas todas las expuestas en este post. https://github.com/jorgediegocrespo/TasksWebApi