mercredi 8 novembre 2017

ASP.NET MVC Implementing history tracking in SaveChanges override

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