Entity Framework / SQL2008 – ¿Cómo actualizar automáticamente los campos LastModified para entidades?

Si tengo la siguiente entidad:

public class PocoWithDates { public string PocoName { get; set; } public DateTime CreatedOn { get; set; } public DateTime LastModified { get; set; } } 

Lo que corresponde a una tabla de SQL Server 2008 con el mismo nombre / atributos …

¿Cómo puedo hacerlo automáticamente ?

  1. Establezca el campo CreatedOn / LastModified para el registro hasta ahora (al hacer INSERT)
  2. Establezca el campo LastModified para el registro hasta ahora (cuando se realiza UPDATE)

Cuando digo automáticamente , quiero decir que quiero ser capaz de hacer esto:

 poco.Name = "Changing the name"; repository.Save(); 

No esta:

 poco.Name = "Changing the name"; poco.LastModified = DateTime.Now; repository.Save(); 

Detrás de escena, “algo” debería actualizar automáticamente los campos de fecha y hora. ¿Qué es ese “algo”?

Estoy usando Entity Framework 4.0. ¿Hay alguna forma en que EF pueda hacer eso automáticamente? (una configuración especial en el EDMX tal vez?)

Desde el lado de SQL Server, puedo usar DefaultValue , pero eso solo funcionará para INSERT (no para UPDATE).

De manera similar, puedo establecer un valor predeterminado usando un constructor en el POCO, pero nuevamente esto solo funcionará al crear instancias del objeto.

Y, por supuesto, podría usar Triggers, pero no es ideal.

Como utilizo Entity Framework, puedo engancharme al evento SavingChanges y actualizar los campos de fecha aquí, pero el problema es que necesito estar “al tanto” de los POCO (en este momento, mi repository se implementa con generics). Tendría que hacer algún tipo de truco OO (como hacer que mi POCO implemente una interfaz, y llamar a un método sobre eso). No estoy en contra de eso, pero si tengo que hacer eso, prefiero configurar manualmente los campos.

Básicamente estoy buscando una solución SQL Server 2008 o Entity Framework 4.0. (o una forma inteligente de .NET)

¿Algunas ideas?

EDITAR

Gracias a @marc_s por su respuesta, pero fui con una solución que es mejor para mi escenario.

Sé que llegué un poco tarde a la fiesta, pero resolví esto para un proyecto en el que estoy trabajando y pensé en compartir mi solución.

En primer lugar, para hacer que la solución sea más reutilizable, creé una clase base con las propiedades de la marca de tiempo:

 public class EntityBase { public DateTime? CreatedDate { get; set; } public DateTime? LastModifiedDate { get; set; } } 

Luego anulé el método SaveChanges en mi DbContext:

 public class MyContext : DbContext { public override int SaveChanges() { ObjectContext context = ((IObjectContextAdapter)this).ObjectContext; //Find all Entities that are Added/Modified that inherit from my EntityBase IEnumerable objectStateEntries = from e in context.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Modified) where e.IsRelationship == false && e.Entity != null && typeof(EntityBase).IsAssignableFrom(e.Entity.GetType()) select e; var currentTime = DateTime.Now; foreach (var entry in objectStateEntries) { var entityBase = entry.Entity as EntityBase; if (entry.State == EntityState.Added) { entityBase.CreatedDate = currentTime; } entityBase.LastModifiedDate = currentTime; } return base.SaveChanges(); } } 

Como tengo una capa de servicio que media entre mis controladores (estoy usando ASP.NET MVC) y mi repository, he decidido establecer automáticamente los campos aquí.

Además, mis POCO no tienen relaciones / abstracciones, son completamente independientes. Me gustaría mantenerlo de esta manera, y no marcar ninguna propiedad virtual, ni crear clases base.

Así que creé una interfaz, IAutoGenerateDateFields:

 public interface IAutoGenerateDateFields { DateTime LastModified { get;set; } DateTime CreatedOn { get;set; } } 

Para cualquier POCO deseo autogenerar estos campos, implemento esta interfaz.

Usando el ejemplo en mi pregunta:

 public class PocoWithDates : IAutoGenerateDateFields { public string PocoName { get; set; } public DateTime CreatedOn { get; set; } public DateTime LastModified { get; set; } } 

En mi capa de servicio, ahora compruebo si el objeto concreto implementa la interfaz:

 public void Add(SomePoco poco) { var autoDateFieldsPoco = poco as IAutoGenerateDateFields; // returns null if it's not. if (autoDateFieldsPoco != null) // if it implements interface { autoDateFieldsPoco.LastModified = DateTime.Now; autoDateFieldsPoco.CreatedOn = DateTime.Now; } // ..go on about other persistence work. } 

Probablemente romperé ese código en el método Agregar a un ayudante / extensión más adelante.

Pero creo que esta es una solución decente para mi escenario, ya que no quiero usar virtuales en Save (ya que estoy usando Unit of Work, Repository, y Pure POCO’s), y no quiero usar triggers.

Si tiene algún pensamiento / sugerencia, hágamelo saber.

También me gustaría presentar una solución tardía. Este solo es aplicable a .NET Framework 4 pero hace que este tipo de tarea sea trivial.

 var vs = oceContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified); foreach (var v in vs) { dynamic dv = v.Entity; dv.DateLastEdit = DateTime.Now; } 

aquí está la versión editada de la respuesta anterior. El anterior no funcionó para las actualizaciones para mí.

 public override int SaveChanges() { var objectStateEntries = ChangeTracker.Entries() .Where(e => e.Entity is TrackedEntityBase && (e.State == EntityState.Modified || e.State == EntityState.Added)).ToList(); var currentTime = DateTime.UtcNow; foreach (var entry in objectStateEntries) { var entityBase = entry.Entity as TrackedEntityBase; if (entityBase == null) continue; if (entry.State == EntityState.Added) { entityBase.CreatedDate = currentTime; } entityBase.LastModifiedDate = currentTime; } return base.SaveChanges(); } 

Tienes dos opciones:

  • tener una clase base para todas las clases de entidades comerciales que hace

     poco.LastModified = DateTime.Now; 

    en un .Save() virtual .Save() que todos los demás tendrían que llamar

  • usa un disparador en la base de datos

No creo que haya otro método razonablemente seguro y fácil de lograr esto.

Podemos usar una clase parcial y anular el método SaveChanges para lograr esto.

 using System; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.Core.Objects; using System.Data.Entity.Infrastructure; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web; namespace TestDatamodel { public partial class DiagnosisPrescriptionManagementEntities { public override int SaveChanges() { ObjectContext context = ((IObjectContextAdapter)this).ObjectContext; foreach (ObjectStateEntry entry in (context.ObjectStateManager .GetObjectStateEntries(EntityState.Added | EntityState.Modified))) { if (!entry.IsRelationship) { CurrentValueRecord entryValues = entry.CurrentValues; if (entryValues.GetOrdinal("ModifiedBy") > 0) { HttpContext currentContext = HttpContext.Current; string userId = "nazrul"; DateTime now = DateTime.Now; if (currContext.User.Identity.IsAuthenticated) { if (currentContext .Session["userId"] != null) { userId = (string)currentContext .Session["userId"]; } else { userId = UserAuthentication.GetUserId(currentContext .User.Identity.UserCode); } } if (entry.State == EntityState.Modified) { entryValues.SetString(entryValues.GetOrdinal("ModifiedBy"), userId); entryValues.SetDateTime(entryValues.GetOrdinal("ModifiedDate"), now); } if (entry.State == EntityState.Added) { entryValues.SetString(entryValues.GetOrdinal("CreatedBy"), userId); entryValues.SetDateTime(entryValues.GetOrdinal("CreatedDate"), now); } } } } return base.SaveChanges(); } } } 

Tuve que agregar la entidad base extenderla desde mi base de datos primera clase parcial (que no es ideal)

  public override int SaveChanges() { AddTimestamps(); return base.SaveChanges(); } public override async Task SaveChangesAsync() { AddTimestamps(); return await base.SaveChangesAsync(); } private void AddTimestamps() { //var entities = ChangeTracker.Entries().Where(x => x.Entity is BaseEntity && (x.State == EntityState.Added || x.State == EntityState.Modified)); //ObjectiveContext context = ((IObjectContextAdapter)this).ObjectContext; var entities = ChangeTracker.Entries().Where(e => e.Entity is BaseEntity && (e.State == EntityState.Modified || e.State == EntityState.Added)).ToList(); var currentUsername = !string.IsNullOrEmpty(System.Web.HttpContext.Current?.User?.Identity?.Name) ? HttpContext.Current.User.Identity.Name : "Anonymous"; foreach (var entity in entities) { if (entity.State == EntityState.Added) { ((BaseEntity)entity.Entity).CREATEDON = DateTime.UtcNow; ((BaseEntity)entity.Entity).CREATEDBY = currentUsername; } ((BaseEntity)entity.Entity).MODIFIEDON = DateTime.UtcNow; ((BaseEntity)entity.Entity).MODIFIEDBY = currentUsername; } }