Entity Framework – Atribuir valores antes dos dados serem salvos

Olá,

Um cenário bastante comum, que encontro nos fóruns, é a necessidade de registrar alguma informação na entidade antes dela ser salva na base de dados, via Entity Framework.

Dados como: “data de inclusão”, “data da última atualização”, “registro de log”, “usuário que realizou a alteração” e outros, são dados pertinentes para o funcionamento da aplicação e que estão atrelados ao evento de inclusão e atualização dos dados na base de dados.

duck9ph

Vamos propor um exemplo: supondo que temos de gravar documentos em nossa base de dados e temos de registrar informações sobre a data de inclusão e data da última alteração do documento.

Propomos então que a estrutura de nossa classe de documentos tem a seguinte estrutura:

public class Document
{
    public int Id { get; set; }

    public string Title { get; set; }

    public DateTime CreationTime { get; set; }

    public DateTime LastUpdate { get; set; }

    public byte[] Content { get; set; }
}

E nossa classe de contexto com o banco de dados contém o seguinte código:

public class DataContext : DbContext
{
    public DbSet<Document> Documents { get; set; }

    public DataContext()
    {
        Database.SetInitializer<DataContext>(new DropCreateDatabaseIfModelChanges<DataContext>());
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add<Document>(new DocumentConfiguration());

        base.OnModelCreating(modelBuilder);
    }

    public override int SaveChanges()
    {   
        // Detecta as alterações efetuadas sobre as entidades
        this.ChangeTracker.DetectChanges();

        // Identifica as instâncias de 'Document' existentes 
        // dentro do ChangeTracker
        var documents = ChangeTracker.Entries<Document>();

        if (documents != null)
        {
            // Varre os 'Documents' existentes no ChangeTracker
            foreach (DbEntityEntry<Document> item in documents)
            {
                // Verifica o estado do item
                switch (item.State)
                {
                    case EntityState.Added:                           
                        item.Entity.LastUpdate = item.Entity.CreationTime = DateTime.Now;                             
                        break;
                    case EntityState.Modified:
                        item.Entity.LastUpdate = DateTime.Now;
                        break;
                }
            }
        }

        // Realiza a gravação dos itens na base de dados
        return base.SaveChanges();
    }
}

Notemos então que nossa classe de contexto com o banco de dados sobrescreve o método “SaveChanges()” e adiciona uma lógica própria para identificação de possíveis instâncias de documentos existentes no ChangeTracker. O ChangeTracker é um pool que acumula todas as instâncias que sofreram alguma alteração, seja ela uma inclusão, modificação ou exclusão.

Note que dentro desta lógica identificamos qual o estado de nossa instância (Added ou Modified) e atribuímos a data corrente as propriedades LastUpdate e CreationTime conforme necessário.

Para testar o funcionamento de nossa lógica, montei o bloco de código abaixo:

class Program
{
    static void Main(string[] args)
    {
        using (DataContext context = new DataContext())
        {
            Document newDocument = new Document();

            newDocument.Title = "Document 1";

            context.Documents.Add(newDocument);

            context.SaveChanges();

            newDocument.Title = "Document 1*";

            context.SaveChanges();

        }
    }
}

Este post foi bastante simples, mas responde a muitas dúvidas que surgem nos fóruns.

Espero que seja útil

Por

Msc. Fernando Henrique Inocêncio Borba Ferreira

Microsoft Most Valuable Professional – Data Platform Development

8 comentários

  1. Muito bom. Ficaria melhor ainda se utilizar uma interface nas entidades, assim o SaveChanges verifica se a classe implementa a interface e entao aplica a atualizacao de data/hora, ao inves de escrever codigo para cada entidade.

      1. /* Exemplo:
        O método SaveChanges foi sobrescrito para verificar se irá persistir instâncias de entidades que implementam
        a interface IAuditableEntity.
        Para facilitar, utilizo uma class base chamada EntityBase que implementa essa interface,
        e implementa também IValidatableObject.
        */
        public interface IAuditableEntity
        {
        DateTime DataCadastro { get; set; }
        DateTime? DataAlteracao { get; set; }
        string Modifier { get; set; }
        }
        public abstract class EntityBase : IAuditableEntity, IValidatableObject
        {
        [Key]
        public long Id { get; set; }
        public DateTime DataCadastro { get; set; }
        public DateTime? DataAlteracao { get; set; }
        public string Modifier { get; set; }
        public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
        yield return ValidationResult.Success;
        }
        }
        public class Usuario : EntityBase
        {
        [Required, StringLength(50)]
        public string Login { get; set; }
        [Required, StringLength(100)]
        public string Senha { get; set; }
        [Required, StringLength(50)]
        public string Salt { get; set; }
        public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
        if (Login == Senha)
        yield return new ValidationResult("Login/Senha devem ser diferentes", new[] "Login", "Senha");
        else yield return ValidationResult.Success;
        }
        }
        public class MyContext : DbContext
        {
        private readonly string _modifier; //informações customizadas
        public MyContext(string modifier)
        {
        _modifier = modifier;
        }
        public MyContext()
        : this("") /* sem modificador*/
        {
        }
        public override int SaveChanges()
        {
        var auditables = ChangeTracker.Entries().Where(x => x.Entity is IAuditableEntity);
        foreach (var entity in auditables)
        {
        var auditableEntity = (IAuditableEntity)entity.Entity;
        switch (entity.State)
        {
        case EntityState.Added:
        if (auditableEntity.DataCadastro == default(DateTime))
        auditableEntity.DataCadastro = DateTime.Now;
        break;
        case EntityState.Modified:
        auditableEntity.DataAlteracao = DateTime.Now;
        break;
        }
        auditableEntity.Modifier = _modifier;
        }
        return base.SaveChanges();
        }
        /* Código omitido …*/
        }

        view raw
        AudtibleContext.cs
        hosted with ❤ by GitHub

  2. Parabéns pelo post, Fernando! Eu particularmente eu tenho uma entidade base onde fica minha chave primária e minhas propriedades de data de atualização e criação

    1. Grande Yan!
      Sim! Usar classes base com os atributos básicos para cada entidade são uma vantagem dos ORMs.

      Outro ponto vantajoso é o uso de rotinas de log das entidades. Este é o ponto perfeito para a inclusão de lógicas deste tipo.

      []s e obrigado por postar!

  3. Dúvida que anda me perseguindo……
    Quando damos o override no SaveChanges nós já temos o ID da entidade que esta sendo incluída ou esse ID somente depois do SaveChanges??

    1. Olá, Waldemir.

      O padrão é o ID ser gerado apenas depois do SaveChanges.
      Quem gera esse valor é o banco de dados se você adotar o comportamento padrão de IDs com autonumeração.

      Se você desligar a autonumeração da sua entidade, você será o responsável por manter esse ID e então estará livre para fazer do seu modo e ter o valor antes do SaveChanges.

      1. Pois é.. Verifiquei isso hoje, apesar de não entender o porque isso ocorre e por vezes vamos salvar algo e no meio do caminho cancelamos e quando vemos o próximo registro criado ele pula o ID seguinte da mesma forma, como se tivesse registrado e depois cancelado…..

        De qualquer forma muito obrigado pela resposta e tempo.

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 )

Foto do Google

Você está comentando utilizando sua conta Google. 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 )

Conectando a %s

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.