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.