I am currently trying to implement history tracking on all of my tables in my app in a generic way by overriding the SaveChanges
method and making use of reflection. As a simple case, let's say I have 2 classes/dbsets for my domain objects and a history table for each like the following:
DbSet<Cat> Cats { get; set; }
DbSet<CatHistory> CatHistories { get; set; }
DbSet<Dog> Dogs { get; set; }
DbSet<DogHistory> DogHistories { get; set; }
The CatHistory
class looks like the following (DogHistory
follows the same scheme):
public class CatHistory : HistoricalEntity
{
public int CatId { get; set; }
public virtual Cat Cat{ get; set; }
}
with HistoricalEntity
defined as the following:
public class HistoricalEntity : Entity
{
public string Field { get; set; }
public string OldValue { get; set; }
public string NewValue { get; set; }
public DateTime ModDate { get; set; }
public string ModUser { get; set; }
}
My Goal is when an object is saved, I would like to insert a record in the appropriate history table. I am having trouble overcoming type difference when using reflection. My current attempt is below and I seem to be stuck on the //TODO:
line:
public override int SaveChanges()
{
foreach (var entry in ChangeTracker.Entries().Where(x => x.State == EntityState.Modified || x.State == EntityState.Added || x.State == EntityState.Deleted).ToList())
{
//leverage this to get type of object being saved, now we know what table to query to get original object and check props for changes
var type = ObjectContext.GetObjectType(entry.Entity.GetType());
var typeName = type.Name;
var historyTypeName = typeName + "History";
var properties = entry.CurrentValues.PropertyNames.Where(x => entry.Property(x).IsModified).ToList();
//get the history entry type from our calculated typeName
var historyType = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(x => x.Name == historyTypeName);
if(historyType != null)
{
//get the dbset for the given entry type
DbSet dbSet = this.Set(type);
DbSet historyDbSet = this.Set(historyType);
//modified entries
if (dbSet != null && historyDbSet != null && entry.State == EntityState.Modified)
{
var existingEntry = dbSet.Find(entry.Property("Id").CurrentValue);
//create history record and add entry to table
var newHistories = GetHistoricalEntities(existingEntry, type, entry);
var listType = typeof(List<>).MakeGenericType(new[] { historyType });
var typedHistories = (IList)Activator.CreateInstance(listType);
//TODO: turn newHistories (type = List<HistoricalEntity>) into specific list type (List<MyObjectHistory>) so I can addrange on appropriate DbSet (MDbSet<MyObjectHistory>)
historyDbSet.AddRange(newHistories);
}
}
}
return base.SaveChanges();
}
private List<HistoricalEntity> GetHistoricalEntities(object existingObject, Type existingObjectType, object updatedObject)
{
var histories = new List<HistoricalEntity>();
var simpleProperties = existingObjectType.GetProperties().Where(x => !x.PropertyType.IsClass);
foreach (var property in simpleProperties)
{
var oldValue = property.GetValue(existingObject, null);
var newValue = property.GetValue(updatedObject, null);
if (!object.Equals(oldValue, newValue))
{
var history = new HistoricalEntity();
history.ModDate = DateTime.UtcNow;
history.ModUser = HttpContext.Current.User.Identity.GetUserId();
history.Field = property.Name;
history.OldValue = oldValue.ToString();
history.NewValue = newValue.ToString();
histories.Add(history);
//TODO: remove
System.Diagnostics.Debug.WriteLine("Property " + property.Name + " was: " + oldValue + "; is: " + newValue);
}
}
return histories;
}
Aucun commentaire:
Enregistrer un commentaire