Gerando registros de log automáticos com o Entity Framework

Uma tarefa bastante recorrente durante o desenvolvimento de sistemas é a criação de rotinas de log. E o Entity Framework facilita a nossa vida quando temos de fazer isso.

Com o Entity Framework podemos criar uma customização que encapsule os comandos que serão enviados para o banco de dados e então adicionar uma lógica que gere os registros de log necessários para cada operação.

log de dados

Esta customização é bastante simples de ser criada e basicamente se resume a sobrescrita do método SaveChanges dos nossos contextos de acesso a dados.

Logo abaixo é apresentado um exemplo de como essa customização pode ser feita.

1 – Crie a entidade de log.

A entidade de log utilizada seguirá a estrutura abaixo:

using System;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Xml.Serialization;

public class Log {

    private const string INSERT_ACTION = "Insert";
    private const string UPDATE_ACTION = "Update";
    private const string DELETE_ACTION = "Delete";

    public Log() {
        this.Date = DateTime.Now;
    }

    public int Id { get; set; }

    public string Action { get; set; }

    public string OriginalValues { get; set; }

    public string NewValues { get; set; }

    public DateTime Date { get; set; }

    public static Log CreateInsertLog(object newEntity) {
        Log log = new Log();

        log.Action = INSERT_ACTION;
        log.OriginalValues = null;
        log.NewValues = Serialize(newEntity);

        return log;
    }

    public static Log CreateDeleteLog(object newEntity) {
        Log log = new Log();

        log.Action = DELETE_ACTION;
        log.OriginalValues = Serialize(newEntity);
        log.NewValues = null;

        return log;
    }

    public static Log CreateUpdateLog(object originalEntity, object newEntity) {

        Log log = new Log();

        log.Action = UPDATE_ACTION;
        log.OriginalValues = Serialize(originalEntity);
        log.NewValues = Serialize(newEntity);

        return log;
    }

    private static string Serialize(object obj) {

        return SerializeJson(obj);
        //return SerializeXml(obj);
    }

    private static string SerializeXml(object obj) {

        XmlSerializer xs = new XmlSerializer(obj.GetType());
        using (MemoryStream buffer = new MemoryStream()) {
            xs.Serialize(buffer, obj);
            return ASCIIEncoding.ASCII.GetString(buffer.ToArray());
        }
    }

    private static string SerializeJson(object obj) {

        using (MemoryStream buffer = new MemoryStream()) {
            DataContractJsonSerializer ser = new DataContractJsonSerializer(obj.GetType());
            ser.WriteObject(buffer, obj);
            return ASCIIEncoding.ASCII.GetString(buffer.ToArray());
        }
    }
}

Note que existem dois métodos de serialização, sendo que um deles gera os dados no formato XML e o outro no formato JSON. Esses métodos de serialização são necessários neste exemplo para demonstrar como os dados de log podem ser salvos no banco de dados. Criei dois métodos, pois o formato XML é o mais usado, mas o JSON é mais econômico (devido a suas características naturais) e ocupa menos espaço no banco de dados. Vale a pena testar e identificar qual se enquadra melhor ao seu cenário.

2 – Sobrescreva o método SaveChanges do seu contexto de dados.

Para assegurar que todas as operações serão logadas no banco de dados será preciso sobrescrever o método SaveChanges e adicionar um lógica de registro destes dados na tabela de log. A lógica necessária para isso, assim como a sobrescrita do método SaveChanges, são apresentadas logo abaixo.

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;

public class DatabaseContext : DbContext {

    // Seus demais DbSets.
    // public DbSet<Entidade1> Logs { get; set; }
    // public DbSet<Entidade2> Logs { get; set; }
    // ...
    // public DbSet<EntidadeN> Logs { get; set; }
        
    public DbSet<Log> Logs { get; set; }

    public DatabaseContext() {

        Database.SetInitializer(new DropCreateDatabaseIfModelChanges<DatabaseContext>());
    }

    public override int SaveChanges() {
        // Detecta as alterações existentes na instância corrente do DbContext.
        this.ChangeTracker.DetectChanges();
        // Identifica as entidades que devem gerar registros em log.
        var entries = DetectEntries();
        // Cria lista para armazenamento temporário dos registros em log.
        List<Log> logs = new List<Log>(entries.Count());
        // Varre as entidades que devem gerar registros em log.
        foreach (var entry in entries) {
            // Cria novo registro de log.
            Log newLog = GetLog(entry);

            if (newLog != null)
                logs.Add(newLog);
        }
        // Adiciona os registros de log na fonte de dados.
        foreach (var item in logs) {
            this.Entry(item).State = EntityState.Added;
        }
        // Persiste as informações na fonte de dados.
        return base.SaveChanges();
    }

    /// <summary>
    /// Identifica quais entidades devem ser gerar registros de log.
    /// </summary>
    private IEnumerable<DbEntityEntry> DetectEntries() {
        return ChangeTracker.Entries().Where(e => (e.State == EntityState.Modified ||
                                                    e.State == EntityState.Added ||
                                                    e.State == EntityState.Deleted) &&
                                                    e.Entity.GetType() != typeof(Log));
    }

    /// <summary>
    /// Cria os registros de log.
    /// </summary>
    private Log GetLog(DbEntityEntry entry) {

        Log returnValue = null;

        if (entry.State == EntityState.Added) {
            returnValue = GetInsertLog(entry);
        } else if (entry.State == EntityState.Modified) {
            returnValue = GetUpdateLog(entry);
        } else if (entry.State == EntityState.Deleted) {
            returnValue = GetDeleteLog(entry);
        }

        return returnValue;
    }

    private Log GetInsertLog(DbEntityEntry entry) {

        return Log.CreateInsertLog(entry.Entity);
    }

    private Log GetDeleteLog(DbEntityEntry entry) {

        return Log.CreateDeleteLog(entry.Entity);
    }

    private Log GetUpdateLog(DbEntityEntry entry) {

        object originalValue = null;

        if (entry.OriginalValues != null)
            originalValue = entry.OriginalValues.ToObject();
        else
            originalValue = entry.GetDatabaseValues().ToObject();

        return Log.CreateUpdateLog(originalValue, entry.Entity);
    }
}

Espero que seja útil.

Por

MSc. Fernando Henrique Inocêncio Borba Ferreira

Microsoft Most Valuable Professional – Visual C#

Anúncios

23 Responses to Gerando registros de log automáticos com o Entity Framework

  1. Fúlvio Cezar Canducci Dias says:

    Parabéns pelo post!
    Fantástico!

  2. Parabéns, concordo com o Fúlvio, foi um excelente, post.

  3. Olá Fernando. Bem bacana a solução, já tive necessidade de gerar log e a sobrescrita do método savechanges realmente foi a melhor saida.

    Gostaria de fazer uma observação no método DetectEntries() onde no Where existe um e.GetType() != typeof(Log).

    Da uma olhada na performance jogando o typeof(Log) em uma variavel e no compare usa a variavel ao invés de typeof().

    Parabéns, muito bom o post. 🙂

    • Olá Nelson,
      Obrigado pelo comentário. Nos meus testes com poucas entidades não obtive efeito.
      Mas acredito que o próprio MSBuild já otimize o código.
      Obrigado pelo comentário, será algo a destacar no futuro.

      []s!

  4. Leandro Souza says:

    Olá Fernando, como já foi dito, ótimo post.

    Uma duvida, o entry.OriginalValues() e o entry.GetDatabaseValues() utilizado GetUpdateLog retorna o valor atual do registro no banco?

    Se sim, esta opção sempre esteve esta disponível no EF ou é alguma novidade das ultimas versões?

    • Olá Leandro,
      Tudo bem?

      Sim, esses recursos já existe desde a versão 4.1 (se já não existirem desde a versão 4.1).

      O OriginalValues retorna dados em Cache e o GetDatabaseValues vai buscar registros na base de dados.

      []s!

  5. Fantástico o post !

    Isso é uma mão na roda.

  6. Rubens Rub says:

    Repetaculê, muito bom

  7. Daniel says:

    Excelente e muito prático.
    Porém quando a entidade tem propriedade ICollection retorna um erro nesta linha
    XmlSerializer xs = new XmlSerializer(obj.GetType());
    Erro:Não é possível serializar o membro ‘App.Categoria.Produto’ do tipo ‘System.Collections.Generic.ICollection
    Alguma sugestão?
    Obrigado

  8. Daniel says:

    Blz. Fernando,

    A estrutura está assim:

    public class Categoria
    {
    public int Id { get; set; }
    public string nome { get; set; }
    public virtual ICollection produto { get; set; }
    }

    public class produto
    {
    public int Id { get; set; }
    public string nome { get; set; }
    public double valor { get; set; }
    public virtual Categoria categoria { get; set; }

    }

    []s!

    • Olá Daniel,
      Não tenho certeza se a interface ICollection é serializável, mas a interface ICollection é serializável.

      Tente fazer algo como o exemplo abaixo:
      public class Categoria
      {
      public int Id { get; set; }
      public string nome { get; set; }
      public virtual ICollection produto { get; set; }
      }

      Veja que eu especifiquei o tipo genérico da collection na propriedade ‘produto’. Acredito que vc terá de fazer isso com as demais.

      []s!

      • Daniel says:

        Consegui resolver o problema assim:

        [XmlIgnore]
        public virtual ICollection produto { get; set; }

        Agora estou tentando uma forma pra fazer isso em model first.

      • carlos says:

        Com o [XmlIgnore] ele vai ignorar as alterações nessa collection quando efetuadas. Não vai gerar as alterações referente a ela. Tem outra solução.

  9. Eric says:

    Muito bom o post, mas o log de insert vai com o id da tabela como 0(zero).
    Teria como resolver isto e pegar o id que recebe o item ao ser gravado no banco?

  10. Olá Eric,
    Existe uma maneira. Vc precisa ajustar o código para gerar novos registros após o base,SaveChanges(). Assim os IDs gerados após o base.SaveChanges() serão gravados. Provavelmente com essa alteração vc acabará por criar um método recursivo, mas é fácil contornar isso. Qualquer dúvida me mande um e-mail.
    []s!

  11. Rodrigo says:

    Está com erro no update, ele seta o valor atual e valor futuro com o mesmo valor

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: