在SaveChanges中实现历史记录跟踪覆盖

时间:2017-11-08 19:43:57

标签: c# entity-framework reflection

我目前正试图通过覆盖SaveChanges方法并利用反射,以通用方式在我的应用中的所有表格上实现历史记录跟踪。作为一个简单的例子,假设我的域对象有2个类/ dbset,每个类的历史表如下:

DbSet<Cat> Cats { get; set; }
DbSet<CatHistory> CatHistories { get; set; }
DbSet<Dog> Dogs { get; set; }
DbSet<DogHistory> DogHistories { get; set; }

CatHistory类如下所示(DogHistory遵循相同的方案):

public class CatHistory : HistoricalEntity
{
  public int CatId { get; set; }

  public virtual Cat Cat{ get; set; }
}

我的目标是保存对象时,我想在相应的历史记录表中插入记录。使用反射时,我无法克服类型差异。我目前的尝试是在下面,我似乎被卡在//TODO:行:

        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)
        {

          //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);
          }
        }

2 个答案:

答案 0 :(得分:1)

您可以使用AutoMapper来映射您的历史实体。我刚刚创建了一个小测试,希望它可以复制你的情况:

iPhone10,1

答案 1 :(得分:1)

我将尝试解释我在申请中实施的方式。

我创建了名称以历史记录结尾的模型,用于在从原始表中删除记录之前需要插入的模型。

<强> BaseModel.cs

namespace ProductVersionModel.Model
{
    using System;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;

    /// <summary>
    /// all common properties of the tables are defined here
    /// </summary>
    public class BaseModel
    {
        /// <summary>
        /// id of the table
        /// </summary>
        [Key]
        public int Id { get; set; }

        /// <summary>
        /// user id of the user who modified last 
        /// </summary>
        public string LastModifiedBy { get; set; }

        /// <summary>
        /// last modified time
        /// </summary>
        public DateTime LastModifiedTime { get; set; }


        /// <summary>
        /// record created user id
        /// </summary>
        [Required]
        public string CreatedBy { get; set; }

        /// <summary>
        /// record creation time
        /// </summary>
        public DateTime CreationTime { get; set; }

        /// <summary>
        /// Not mapped to database, only for querying used
        /// </summary>
        [NotMapped]
        public int RowNumber { get; set; }
    }
}

<强> Product.cs

namespace ProductVersionModel.Model
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;

    /// <summary>
    /// store detals of the product
    /// </summary>
    public class ProductStatus : BaseModel
    {
        /// <summary>
        /// Name of the product
        /// </summary>
        [Required, MaxLength(100)]
        public string Name { get; set; }


        /// <summary>
        /// product version validity start date
        /// </summary>

        public DateTime ValidFrom { get; set; }

        /// <summary>
        /// product version valid till
        /// </summary>
        public DateTime? ValidTill { get; set; }

        /// <summary>
        /// This field used to keep track of history of a product
        /// </summary>
        public int ProductNumber { get; set; }

    }
}

<强> HistoryBaseModel.cs

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ProductVersionModel.Model.History
{
    public class HistroyBaseModel
    {

        /// <summary>
        /// id of the table
        /// </summary>
        [Key,  DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int Id { get; set; }

        public string DeletedBy { get; set; }

        public DateTime? DeletedTime { get; set; }
        /// <summary>
        /// record created user id
        /// </summary>
        [Required]
        public string CreatedBy { get; set; }

        /// <summary>
        /// record creation time
        /// </summary>
        public DateTime CreationTime { get; set; }
    }
}

<强> ProductStatusHistory.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using ProductVersionModel.Model.History;

// ReSharper disable once CheckNamespace
namespace ProductVersionModel.Model.History
{
    public class ProductStatusHistory : HistroyBaseModel
    {

        /// <summary>
        /// Name of the product
        /// </summary>
        [MaxLength(100)]
        public string Name { get; set; }

        /// <summary>
        /// product version validity start date
        /// </summary>
        public DateTime ValidFrom { get; set; }

        /// <summary>
        /// product version valid till
        /// </summary>
        public DateTime? ValidTill { get; set; }

        /// <summary>
        /// This field used to keep track of history of a product
        /// </summary>
        public int ProductNumber { get; set; }

    }
}

CrudRepository 删除方法

    public virtual int Delete(List<object> ids, string userName)
    {
        try
        {
            foreach (var id in ids)
            {
                var dbObject = _table.Find(id);
                HistroyBaseModel historyRecord = null;
                var modelAssembly = Assembly.Load(nameof(ProductVersionModel));
                var historyType =
                    modelAssembly.GetType(
                        // ReSharper disable once RedundantNameQualifier   - dont remove namespace it is required
                        $"{typeof(ProductVersionModel.Model.History.HistroyBaseModel).Namespace}.{typeof(TModel).Name}History");

                if (historyType != null)
                {
                    var historyObject = Activator.CreateInstance(historyType);

                    historyRecord = MapDeletingObjectToHistoyObject(dbObject, historyObject, userName);

                    DatabaseContext.Entry(historyRecord).State = EntityState.Added;
                }
                DatabaseContext.Entry(dbObject).State = EntityState.Deleted;
            }
            return DatabaseContext.SaveChanges();
        }
        catch (DbUpdateException ex)
        {
            throw HandleDbException(ex);
        }
    }

    protected virtual HistroyBaseModel MapDeletingObjectToHistoyObject(object inputObject, object outputObject, string userName)
    {
        var historyRecord = MapObjectToObject(inputObject, outputObject) as HistroyBaseModel;
        if (historyRecord != null)
        {
            historyRecord.DeletedBy = userName;
            historyRecord.DeletedTime = DateTime.UtcNow;
        }
        return historyRecord;
    }

    protected virtual object MapObjectToObject(object inputObject, object outputObject)
    {
        var inputProperties = inputObject.GetType().GetProperties();
        var outputProperties = outputObject.GetType().GetProperties();//.Where(x => !x.HasAttribute<IgnoreMappingAttribute>());
        outputProperties.ForEach(x =>
        {
            var prop =
                inputProperties.FirstOrDefault(y => y.Name.Equals(x.Name) && y.PropertyType == x.PropertyType);
            if (prop != null)
                x.SetValue(outputObject, prop.GetValue(inputObject));
        });

        return outputObject;
    }

TModel 是模型的类型

public class CrudRepository<TModel> : DataAccessBase, ICrudRepository<TModel> where TModel : class, new()
public class ProductStatusRepository : CrudRepository<ProductStatus>, IProductStatusRepository

如果要映射复杂权限(如子元素列表),可以覆盖相关存储库中的方法 MapDeletingObjectToHistoyObject MapObjectToObject