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.
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#
Parabéns pelo post!
Fantástico!
Olá Fúlvio.
Obrigado pelo comentário.
[]s!
Parabéns, concordo com o Fúlvio, foi um excelente, post.
Obrigado por postar, Leandro.
[]s!
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!
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!
Fantástico o post !
Isso é uma mão na roda.
Obrigado por comentar, Leandro.
[]s!
Repetaculê, muito bom
Obrigado, Rubens.
[]s!
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
Olá Daniel,
Tudo beleza?
Isso me parece uma limitação da sua classe Produto. Parece que ela não pode ser serializada. Qual a estrutura dessa classe?
[]s!
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!
Consegui resolver o problema assim:
[XmlIgnore]
public virtual ICollection produto { get; set; }
Agora estou tentando uma forma pra fazer isso em model first.
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.
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?
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!
Poderia passar como resolver essa questão.
Está com erro no update, ele seta o valor atual e valor futuro com o mesmo valor
Olá, envie seu exemplo para ferhenriquef@live.com.
Irei verificar.
Obrigado.