C# 利用反射+特性实现数据实体-跟踪对比

需求描述:在工作中遇到一个比较难搞的需求,在数据上传时,需要与上一次上传的数据进行对比,并将对象属性值发生变更的属性描述出来(例如XX记录的名称由‘张三’变为‘李四’),实现一个数据对比的功能。

功能实现:在网上百度了下,参考了部分大佬的实现方式,结合自己的业务需求,最后决定使用C#反射和特性方式去实现之一功能。话不多说,直接上代码。

  1. 首先准备一个实体变更日志记录的类,用来保存数据跟踪对比后的结果。
public class EntityChangeLog  
{
        public string Operator { get; set; }

        public string ClassName { get; set; }

        public string DisplayName { get; set; }

        public List<EntityChangeLog> Logs { get; set; }

        public List<EntityChangeLogItem> Items { get; set; }

        public EntityChangeLog()
        {
            this.Logs = new List<EntityChangeLog>();
            this.Items = new List<EntityChangeLogItem>();
        }
 }

public class EntityChangeLogItem 
{
        public string DisplayName { get; set; }

        public string OldValue { get; set; }

        public string NewValue { get; set; }

        public EntityChangeLogItem(string displayName, string oldValue, string newValue)
        {
            this.DisplayName = displayName;
            this.OldValue = oldValue;
            this.NewValue = newValue;
        }
}

2.声明一个C#特性,用于标识在对比类上。

/// <summary>
/// 定义实体属性的类型
/// </summary>
public enum EnumSubItemSort
{
     /// <summary>
     /// 普通属性
     /// </summary>
     GeneralProperty,
     /// <summary>
     /// 子类
     /// </summary>
     Subclass,
     /// <summary>
     /// 子类集合
     /// </summary>
     SubclassSet
}

/// <summary>
/// 实体变更追踪特性
/// </summary> 
[AttributeUsage(AttributeTargets.Class)]
public class EntityChangeTrackingAttribute : Attribute
{
        public string DisplayName { get; }

        public Type EntityType { get; }

        public  EntityChangeTrackingAttribute(string displayName, Type type)
        {
            this.DisplayName = displayName;
            this.EntityType = type;
        }
}

[AttributeUsage(AttributeTargets.Property)]
public class EntityChangeTrackingItemAttribute : Attribute
{
        public string DisplayName { get; }

        public bool Ignore { get; }

        public EnumSubItemSort SubItemSort { get; }

        public Type SubItemType { get; }

        public EntityChangeTrackingItemAttribute(string displayName, bool ignore = false)
        {
            this.DisplayName = displayName;
            this.Ignore = ignore;
            this.SubItemSort = EnumSubItemSort.GeneralProperty; // 调用此构造函数默认为一般属性
        }

        public EntityChangeTrackingItemAttribute(string displayName, EnumSubItemSort sort, Type type, bool ignore = false)
        {
            this.DisplayName = displayName;
            this.Ignore = ignore;
            this.SubItemSort = sort;
            this.SubItemType = type;
        }
}

3.声明一个静态类,用于扩展实体的方法。

public static class EntityChangeTrackingExtend
    {
        public static EntityChangeLog EntityChangeLogExtend<T>(this T oldT, T newT)
        {
            if (oldT == null || newT == null) throw new Exception("参与对比实体都不能为NULL!");

            var echangeLog = new EntityChangeLog();
            var attribute = typeof(EntityChangeTrackingAttribute);
            var isDefined = Attribute.IsDefined(typeof(T), attribute); // 该类是否添加了变更特性,true标识添加
            if (isDefined)
            {
                var calssAttr = (EntityChangeTrackingAttribute)typeof(T).GetCustomAttribute(attribute);
                echangeLog.Operator = "更新";
                echangeLog.DisplayName = calssAttr.DisplayName;
                echangeLog.ClassName = calssAttr.EntityType.Name;
                GenerateChangeLog(oldT, newT, calssAttr.EntityType, echangeLog);
            }

            return echangeLog;
        }

        private static void GenerateChangeLog(object oldT, object newT, Type entityType, EntityChangeLog echangeLog)
        {
            var attribute = typeof(EntityChangeTrackingItemAttribute);
            var properties = entityType.GetProperties();
            foreach (var prop in properties)
            {
                var propAttr = (EntityChangeTrackingItemAttribute)prop.GetCustomAttribute(attribute);
                if (propAttr == null || propAttr.Ignore) continue; // 判断属性是否不进行跟踪记录

                switch (propAttr.SubItemSort)
                {
                    case EnumSubItemSort.GeneralProperty:
                        {
                            var oldValue = prop.GetValue(oldT) ?? null;
                            var newValue = prop.GetValue(newT) ?? null;
                            if (oldValue == null && newValue == null) continue;
                            if (oldValue != null && newValue != null && oldValue.Equals(newValue)) continue;

                            var displayName = string.IsNullOrWhiteSpace(propAttr.DisplayName) ? prop.Name : propAttr.DisplayName;
                            var echangeItem = new EntityChangeLogItem(displayName, oldValue?.ToString(), newValue?.ToString());
                            echangeLog.Items.Add(echangeItem);
                            break;
                        }
                    case EnumSubItemSort.Subclass:
                        {
                            var T1 = prop.GetValue(oldT) ?? null;
                            var T2 = prop.GetValue(newT) ?? null;
                            if (T1 == null && T2 == null) continue;

                            var log = new EntityChangeLog();
                            log.Operator = GetOperator(T1, T2);
                            log.DisplayName = propAttr.DisplayName;
                            log.ClassName = propAttr.SubItemType.Name;
                            // 注意:这里需要限定每个传递的对象必须要有一个无参数的构造函数
                            T1 ??= Assembly.GetExecutingAssembly().CreateInstance(propAttr.SubItemType.FullName);
                            T2 ??= Assembly.GetExecutingAssembly().CreateInstance(propAttr.SubItemType.FullName);
                            GenerateChangeLog(T1, T2, propAttr.SubItemType, log); 
                            echangeLog.Logs.Add(log);
                            break;
                        }
                    case EnumSubItemSort.SubclassSet:
                        {
                            var listO = (prop.GetValue(oldT) as IEnumerable<IEntityChange>) ?? new List<IEntityChange>();
                            var listN = (prop.GetValue(newT) as IEnumerable<IEntityChange>) ?? new List<IEntityChange>();
                            if (listO.Count() == 0 && listN.Count() == 0) continue; // 子集合没有任何变更

                            var log = new EntityChangeLog();
                            log.Operator = "被更改";
                            log.DisplayName = propAttr.DisplayName;
                            log.ClassName = propAttr.SubItemType.Name;

                            // 1. 生成更新日志
                            var updateTuples = (from o in listO 
                                                from n in listN
                                                where o.Identifies == n.Identifies
                                                select new Tuple<IEntityChange, IEntityChange>(o, n)).ToList();
                            foreach (var item in updateTuples)
                            {
                                var logItem = new EntityChangeLog();
                                logItem.Operator = "更新";
                                logItem.DisplayName = $"{propAttr.DisplayName}-数据项[{item.Item1.Identifies}]";
                                logItem.ClassName = propAttr.SubItemType.Name;

                                GenerateChangeLog(item.Item1, item.Item2, propAttr.SubItemType, logItem);
                                log.Logs.Add(logItem);
                            }

                            // 2. 生成更新日志
                            var delEntities = listO.Except(listN, new EntityChangeComparer()).ToList();
                            foreach (var item1 in delEntities)
                            {
                                var logItem = new EntityChangeLog();
                                logItem.Operator = "删除";
                                logItem.DisplayName =  $"{propAttr.DisplayName}-数据项[{item1.Identifies}]";
                                logItem.ClassName = propAttr.SubItemType.Name;

                                var item2 = Assembly.GetExecutingAssembly().CreateInstance(propAttr.SubItemType.FullName);
                                GenerateChangeLog(item1, item2, propAttr.SubItemType, logItem);
                                log.Logs.Add(logItem);
                            }

                            // 3. 生成添加日志
                            var addEntities = listN.Except(listO, new EntityChangeComparer()).ToList();
                            foreach (var item2 in addEntities)
                            {
                                var logItem = new EntityChangeLog();
                                logItem.Operator = "添加";
                                logItem.DisplayName = $"{propAttr.DisplayName}-数据项[{item2.Identifies}]";
                                logItem.ClassName = propAttr.SubItemType.Name;

                                var item1 = Assembly.GetExecutingAssembly().CreateInstance(propAttr.SubItemType.FullName);
                                GenerateChangeLog(item1, item2, propAttr.SubItemType, logItem);
                                log.Logs.Add(logItem);
                            }
                            echangeLog.Logs.Add(log);
                            break;
                        }
                }
            }
        }

        private static string GetOperator(object oldT, object newT)
        {
            if (oldT != null && newT == null) return "删除";
            if (oldT == null && newT != null) return "添加";
            return "更新";
        }
    }

4.处理一般的属性,例如string、int、double等类型的属性时,都比较好对比,但是遇到包含类以及包含类集合属性时处理比较麻烦,尤其是集合属性,会涉及到集合对比,所以在上面第2点我声明了一个枚举,包含普通属性、对象属性以及集合属性三个枚举值,需要分别处理。在使用使需要在特性使用的地方指定好传入对象的类型,这一点在下面会说。这里先说说我是如何处理集合属性的,首先定义了一个接口,接口中具有一个属性,用来标识两个集合对比时的数据项,也可以理解为主键,在对比时取出主键,用来判断是否增加、删除还是更新了集合中的数据。接口如下:

public interface IEntityChange
{
        public string Identifies { get; }
}

public class EntityChangeComparer : IEqualityComparer<IEntityChange>
{
        public bool Equals(IEntityChange x, IEntityChange y)
        {
            return x.Identifies == y.Identifies;
        }

        public int GetHashCode(IEntityChange obj)
        {
            return obj.Identifies.GetHashCode();
        }
}
  1. 接下来定义我们的数据对象,把特性标识在类名和属性名上,注意子类和子类集合的用法。
/// <summary>
/// 噪声校准记录-就是我们需要对比的数据主表
/// </summary> 
[EntityChangeTracking("噪声校准记录", typeof(NoiseCalibration))]
public class NoiseCalibration
{ 
        [EntityChangeTrackingItem("项目ID")]
        public Guid ProjectId { get; set; }

        [EntityChangeTrackingItem("采样日期")]
        public DateTime SamplingDate { get; set; }

        [EntityChangeTrackingItem("测试设备")]
        public string TestInstrument { get; set; }

        [EntityChangeTrackingItem("日间数据", EnumSubItemSort.Subclass, typeof(NoiseCalibrationItem))]
        public NoiseCalibrationItem Daytime { get; set; }

        [EntityChangeTrackingItem("夜间数据", EnumSubItemSort.Subclass, typeof(NoiseCalibrationItem))]
        public NoiseCalibrationItem Nighttime { get; set; }

        [EntityChangeTrackingItem("采样照片", EnumSubItemSort.SubclassSet, typeof(PhotoInfo))]
        public List<PhotoInfo> PhotoList  { get; set; }

       [EntityChangeTrackingItem("采样记录", EnumSubItemSort.SubclassSet, typeof(NoiseRecord))]
        public List<NoiseRecord> Records { get; set; }
}

/// <summary>
/// 包含类对象 
/// </summary>
public class NoiseCalibrationItem
{
        [EntityChangeTrackingItem("采样前时间")]
        public DateTime? BeforeTestTime { get; set; }

        [EntityChangeTrackingItem("采样前数据")]
        public string BeforeTestValue { get; set; }

        [EntityChangeTrackingItem("采样前结果")]
        public bool BeforeValueIsNormal { get; set; }

        [EntityChangeTrackingItem("采样后时间")]
        public DateTime? AfterTestTime { get; set; }

        [EntityChangeTrackingItem("采样后数据")]
        public string AfterTestValue { get; set; }

        [EntityChangeTrackingItem("采样前结果")]
        public bool AfterValueIsNormal { get; set; }

        [EntityChangeTrackingItem("校准值")]
        public double? CalibrationValue { get; set; }
}

/// <summary>
/// 包含类对象-集合属性用,这里需要继承IEntityChange,将主键字段进行绑定
/// </summary>
public class PhotoInfo : IEntityChange
 {
        [EntityChangeTrackingItem("照片名称")]
        public string Name { get; set; }

        [EntityChangeTrackingItem("照片描述")]
        public string Desc { get; set; }

        [EntityChangeTrackingItem("上传人员")]
        public string UploadUser { get; set; }

        public string Date { get; set; }

        public string Identifies => this.Date;
}

/// <summary>
/// 包含类对象-集合属性,集合中嵌套集合,这里需要继承IEntityChange,将主键字段进行绑定
/// </summary>
public class NoiseRecord : IEntityChange
{
        public string Date { get; set; }

        [EntityChangeTrackingItem("开始时间")]
        public DateTime Start { get; set; }

        [EntityChangeTrackingItem("结束时间")]
        public DateTime End { get; set; }

        [EntityChangeTrackingItem("时长")]
        public int Length { get; set; }

        [EntityChangeTrackingItem("参考值")]
        public float Value { get; set; }

        public string Identifies => this.Date;

        [EntityChangeTrackingItem("采样明细", EnumSubItemSort.SubclassSet, typeof(NoiseRecordItem))]
        public List<NoiseRecordItem> RecordItems { get; set; }
}

/// <summary>
/// 包含类对象-集合属性-嵌套的子集合对象,这里需要继承IEntityChange,将主键字段进行绑定
/// </summary>
public class NoiseRecordItem : IEntityChange
{
        public string Parameter { get; set; }

        [EntityChangeTrackingItem("标准值")]
        public double StandardValue { get; set; }

        [EntityChangeTrackingItem("测试值")]
        public double TestValue { get; set; }

        [EntityChangeTrackingItem("测试结果")]
        public bool TestResult { get; set; }

        [EntityChangeTrackingItem("测试备注")]
        public string TestRemark { get; set; }

        public string Identifies => this.Parameter;
}
  1. 模拟创建实体数据,调用扩展方法,生成跟踪记录。
public static void Execute()
{
            var tmodel1 = new NoiseCalibration()
            {
                ProjectId = new Guid(),
                SamplingDate = new DateTime(2021, 10, 1, 10, 8, 9),
                TestInstrument = "TAX09832",
                //Daytime = new NoiseCalibrationItem()
                //{
                //    AfterTestTime = new DateTime(2021, 10, 1),
                //    AfterTestValue = "500.00",
                //    AfterValueIsNormal = true,
                //    BeforeTestTime = new DateTime(2021, 10, 2),
                //    BeforeTestValue = "501.00",
                //    BeforeValueIsNormal = false,
                //    CalibrationValue = 785.11
                //},
                Nighttime = new NoiseCalibrationItem()
                {
                    AfterTestTime = new DateTime(2021, 10, 1),
                    AfterTestValue = "600.00",
                    AfterValueIsNormal = true,
                    BeforeTestTime = new DateTime(2021, 10, 2),
                    BeforeTestValue = "601.00",
                    BeforeValueIsNormal = false,
                    CalibrationValue = 456.5
                },
                PhotoList = new List<PhotoInfo>()
                {
                    new PhotoInfo() { Date = "2021-11-18", Name = "0.jpg", Desc = "小朋友", UploadUser = "朱上" },
                    new PhotoInfo() { Date = "2021-11-19", Name = "1.jpg", Desc = "风景美如画", UploadUser = "秦岚" }
                },
                Records = new List<NoiseRecord>()
                {
                    new NoiseRecord()
                    {
                        Date = "2021-11-19",
                        Start = new DateTime(2021,11,19,07,05,06),
                        End = new DateTime(2021,11,19,08,05,06),
                        Length = 60,
                        Value = 95.5f,
                        RecordItems = new List<NoiseRecordItem>(){
                            new NoiseRecordItem(){ Parameter = "二氧化碳", StandardValue = 9.9, TestValue = 8.9, TestResult = true, TestRemark = "棒棒的1"},
                            new NoiseRecordItem(){ Parameter = "一氧化碳", StandardValue = 8.9, TestValue = 8.9, TestResult = true, TestRemark = "棒棒的2"}
                        }
                    },
                    new NoiseRecord()
                    {
                        Date = "2021-11-20",
                        Start = new DateTime(2021,11,20,07,05,06),
                        End = new DateTime(2021,11,20,08,05,06),
                        Length = 60,
                        Value = 85.6f,
                        RecordItems = new List<NoiseRecordItem>(){
                            new NoiseRecordItem(){ Parameter = "二氧化碳", StandardValue = 9.9, TestValue = 8.9, TestResult = true, TestRemark = "棒棒的3"},
                            new NoiseRecordItem(){ Parameter = "一氧化碳", StandardValue = 8.9, TestValue = 8.9, TestResult = true, TestRemark = "棒棒的4"}
                        }
                    }
                }
            };

            var tmodel2 = new NoiseCalibration()
            {
                ProjectId = new Guid(),
                SamplingDate = new DateTime(2021, 10, 2, 10, 8, 9),
                TestInstrument = "TAX034333",
                Daytime = new NoiseCalibrationItem()
                {
                    AfterTestTime = new DateTime(2021, 10, 2),
                    AfterTestValue = "499.00",
                    AfterValueIsNormal = true,
                    BeforeTestTime = new DateTime(2021, 10, 3),
                    BeforeTestValue = "502.00",
                    BeforeValueIsNormal = false,
                    CalibrationValue = 500.00
                },
                Nighttime = new NoiseCalibrationItem()
                {
                    AfterTestTime = new DateTime(2021, 10, 2),
                    AfterTestValue = "595.00",
                    AfterValueIsNormal = true,
                    BeforeTestTime = new DateTime(2021, 10, 3),
                    BeforeTestValue = "603.00",
                    BeforeValueIsNormal = false,
                    CalibrationValue = 600.00
                },
                PhotoList = new List<PhotoInfo>()
                {
                    new PhotoInfo(){ Date = "2021-11-19", Name = "2.jpg", Desc = "桂林山水甲天下", UploadUser = "李云龙" },
                    new PhotoInfo(){ Date = "2021-11-20", Name = "4.jpg", Desc = "天下无双好风光", UploadUser = "猪八戒"}
                },
                Records = new List<NoiseRecord>()
                {
                    new NoiseRecord()
                    {
                        Date = "2021-11-19",
                        Start = new DateTime(2021,11,19,10,05,06),
                        End = new DateTime(2021,11,19,11,00,06),
                        Length = 55,
                        Value = 95.6f,
                        RecordItems = new List<NoiseRecordItem>(){
                            new NoiseRecordItem(){ Parameter = "二氧化碳", StandardValue = 85.5, TestValue = 88.9, TestResult = false, TestRemark = "差差的1"},
                            new NoiseRecordItem(){ Parameter = "一氧化碳", StandardValue = 88.9, TestValue = 88.9, TestResult = false, TestRemark = "差差的2"}
                        }
                    },
                    new NoiseRecord()
                    {
                        Date = "2021-11-20",
                        Start = new DateTime(2021,11,20,11,05,06),
                        End = new DateTime(2021,11,20,12,00,06),
                        Length = 55,
                        Value = 185.6f,
                        RecordItems = new List<NoiseRecordItem>(){
                            new NoiseRecordItem(){ Parameter = "二氧化碳", StandardValue = 19.9, TestValue = 18.9, TestResult = false, TestRemark = "差差的3"},
                            new NoiseRecordItem(){ Parameter = "一氧化碳", StandardValue = 18.9, TestValue = 18.9, TestResult = false, TestRemark = "差差的4"}
                        }
                    },
                    new NoiseRecord()
                    {
                        Date = "2021-11-21",
                        Start = new DateTime(2021,11,21,11,05,06),
                        End = new DateTime(2021,11,21,12,00,06),
                        Length = 55,
                        Value = 185.6f,
                        RecordItems = new List<NoiseRecordItem>(){
                            new NoiseRecordItem(){ Parameter = "二氧化碳", StandardValue = 119.9, TestValue = 118.9, TestResult = false, TestRemark = "差差的5"},
                            new NoiseRecordItem(){ Parameter = "一氧化碳", StandardValue = 118.9, TestValue = 118.9, TestResult = false, TestRemark = "差差的6"}
                        }
                    }
                }
            };

            var log = tmodel1.EntityChangeLogExtend(tmodel2);
            // 使用Newtonsoft.Json将生成的记录转成JSON字符串输出到屏幕
            Console.WriteLine(JsonConvert.SerializeObject(log));
}

输出结果:


image.png

看着有点乱,可以格式化下:

{
    "Operator":"更新",
    "ClassName":"NoiseCalibration",
    "DisplayName":"噪声校准记录",
    "Logs":[
        {
            "Operator":"添加",
            "ClassName":"NoiseCalibrationItem",
            "DisplayName":"日间数据",
            "Logs":[

            ],
            "Items":[
                {
                    "DisplayName":"采样前时间",
                    "OldValue":null,
                    "NewValue":"2021/10/3 0:00:00"
                },
                {
                    "DisplayName":"采样前数据",
                    "OldValue":null,
                    "NewValue":"502.00"
                },
                {
                    "DisplayName":"采样后时间",
                    "OldValue":null,
                    "NewValue":"2021/10/2 0:00:00"
                },
                {
                    "DisplayName":"采样后数据",
                    "OldValue":null,
                    "NewValue":"499.00"
                },
                {
                    "DisplayName":"采样前结果",
                    "OldValue":"False",
                    "NewValue":"True"
                },
                {
                    "DisplayName":"校准值",
                    "OldValue":null,
                    "NewValue":"500"
                }
            ]
        },
        {
            "Operator":"更新",
            "ClassName":"NoiseCalibrationItem",
            "DisplayName":"夜间数据",
            "Logs":[

            ],
            "Items":[
                {
                    "DisplayName":"采样前时间",
                    "OldValue":"2021/10/2 0:00:00",
                    "NewValue":"2021/10/3 0:00:00"
                },
                {
                    "DisplayName":"采样前数据",
                    "OldValue":"601.00",
                    "NewValue":"603.00"
                },
                {
                    "DisplayName":"采样后时间",
                    "OldValue":"2021/10/1 0:00:00",
                    "NewValue":"2021/10/2 0:00:00"
                },
                {
                    "DisplayName":"采样后数据",
                    "OldValue":"600.00",
                    "NewValue":"595.00"
                },
                {
                    "DisplayName":"校准值",
                    "OldValue":"456.5",
                    "NewValue":"600"
                }
            ]
        },
        {
            "Operator":"被更改",
            "ClassName":"PhotoInfo",
            "DisplayName":"采样照片",
            "Logs":[
                {
                    "Operator":"更新",
                    "ClassName":"PhotoInfo",
                    "DisplayName":"采样照片-数据项[2021-11-19]",
                    "Logs":[

                    ],
                    "Items":[
                        {
                            "DisplayName":"照片名 称",
                            "OldValue":"1.jpg",
                            "NewValue":"2.jpg"
                        },
                        {
                            "DisplayName":"照片描述",
                            "OldValue":"风景美如画",
                            "NewValue":"桂林山水甲天下"
                        },
                        {
                            "DisplayName":"上传人员",
                            "OldValue":"秦岚",
                            "NewValue":"李云 龙"
                        }
                    ]
                },
                {
                    "Operator":"删除",
                    "ClassName":"PhotoInfo",
                    "DisplayName":"采样照片-数据项[2021-11-18]",
                    "Logs":[

                    ],
                    "Items":[
                        {
                            "DisplayName":"照片名称",
                            "OldValue":"0.jpg",
                            "NewValue":null
                        },
                        {
                            "DisplayName":"照片描述",
                            "OldValue":"小朋友",
                            "NewValue":null
                        },
                        {
                            "DisplayName":"上传人员",
                            "OldValue":"朱上",
                            "NewValue":null
                        }
                    ]
                },
                {
                    "Operator":"添加",
                    "ClassName":"PhotoInfo",
                    "DisplayName":"采样照 片-数据项[2021-11-20]",
                    "Logs":[

                    ],
                    "Items":[
                        {
                            "DisplayName":"照片名称",
                            "OldValue":null,
                            "NewValue":"4.jpg"
                        },
                        {
                            "DisplayName":"照片描述",
                            "OldValue":null,
                            "NewValue":"天下无双好风光"
                        },
                        {
                            "DisplayName":"上传人员",
                            "OldValue":null,
                            "NewValue":"猪八戒"
                        }
                    ]
                }
            ],
            "Items":[

            ]
        },
        {
            "Operator":"被更改",
            "ClassName":"NoiseRecord",
            "DisplayName":"采样记录",
            "Logs":[
                {
                    "Operator":"更新",
                    "ClassName":"NoiseRecord",
                    "DisplayName":"采样记录-数据项[2021-11-19]",
                    "Logs":[
                        {
                            "Operator":"被更改",
                            "ClassName":"NoiseRecordItem",
                            "DisplayName":"采样明细",
                            "Logs":[
                                {
                                    "Operator":"更新",
                                    "ClassName":"NoiseRecordItem",
                                    "DisplayName":"采样明细-数据项[二氧化碳]",
                                    "Logs":[

                                    ],
                                    "Items":[
                                        {
                                            "DisplayName":"标准值",
                                            "OldValue":"9.9",
                                            "NewValue":"85.5"
                                        },
                                        {
                                            "DisplayName":"测试值",
                                            "OldValue":"8.9",
                                            "NewValue":"88.9"
                                        },
                                        {
                                            "DisplayName":"测试结果",
                                            "OldValue":"True",
                                            "NewValue":"False"
                                        },
                                        {
                                            "DisplayName":"测试备注",
                                            "OldValue":"棒棒的1",
                                            "NewValue":"差差的1"
                                        }
                                    ]
                                },
                                {
                                    "Operator":"更新",
                                    "ClassName":"NoiseRecordItem",
                                    "DisplayName":"采样明细-数据项[一氧化碳]",
                                    "Logs":[

                                    ],
                                    "Items":[
                                        {
                                            "DisplayName":"标准值",
                                            "OldValue":"8.9",
                                            "NewValue":"88.9"
                                        },
                                        {
                                            "DisplayName":"测试值",
                                            "OldValue":"8.9",
                                            "NewValue":"88.9"
                                        },
                                        {
                                            "DisplayName":"测试结果",
                                            "OldValue":"True",
                                            "NewValue":"False"
                                        },
                                        {
                                            "DisplayName":"测试备注",
                                            "OldValue":"棒棒的2",
                                            "NewValue":"差差的2"
                                        }
                                    ]
                                }
                            ],
                            "Items":[

                            ]
                        }
                    ],
                    "Items":[
                        {
                            "DisplayName":"开始时间",
                            "OldValue":"2021/11/19 7:05:06",
                            "NewValue":"2021/11/19 10:05:06"
                        },
                        {
                            "DisplayName":"结束时间",
                            "OldValue":"2021/11/19 8:05:06",
                            "NewValue":"2021/11/19 11:00:06"
                        },
                        {
                            "DisplayName":"时长",
                            "OldValue":"60",
                            "NewValue":"55"
                        },
                        {
                            "DisplayName":"参考值",
                            "OldValue":"95.5",
                            "NewValue":"95.6"
                        }
                    ]
                },
                {
                    "Operator":"更新",
                    "ClassName":"NoiseRecord",
                    "DisplayName":"采样记录-数据项[2021-11-20]",
                    "Logs":[
                        {
                            "Operator":"被更改",
                            "ClassName":"NoiseRecordItem",
                            "DisplayName":"采样明细",
                            "Logs":[
                                {
                                    "Operator":"更新",
                                    "ClassName":"NoiseRecordItem",
                                    "DisplayName":"采样明细-数据项[二氧化碳]",
                                    "Logs":[

                                    ],
                                    "Items":[
                                        {
                                            "DisplayName":"标准值",
                                            "OldValue":"9.9",
                                            "NewValue":"19.9"
                                        },
                                        {
                                            "DisplayName":"测试值",
                                            "OldValue":"8.9",
                                            "NewValue":"18.9"
                                        },
                                        {
                                            "DisplayName":"测试结果",
                                            "OldValue":"True",
                                            "NewValue":"False"
                                        },
                                        {
                                            "DisplayName":"测试备注",
                                            "OldValue":"棒棒的3",
                                            "NewValue":"差差的3"
                                        }
                                    ]
                                },
                                {
                                    "Operator":"更新",
                                    "ClassName":"NoiseRecordItem",
                                    "DisplayName":"采样明细-数据项[ 一氧化碳]",
                                    "Logs":[

                                    ],
                                    "Items":[
                                        {
                                            "DisplayName":"标准值",
                                            "OldValue":"8.9",
                                            "NewValue":"18.9"
                                        },
                                        {
                                            "DisplayName":"测试值",
                                            "OldValue":"8.9",
                                            "NewValue":"18.9"
                                        },
                                        {
                                            "DisplayName":"测试结果",
                                            "OldValue":"True",
                                            "NewValue":"False"
                                        },
                                        {
                                            "DisplayName":"测试备注",
                                            "OldValue":"棒棒的4",
                                            "NewValue":"差差的4"
                                        }
                                    ]
                                }
                            ],
                            "Items":[

                            ]
                        }
                    ],
                    "Items":[
                        {
                            "DisplayName":"开始时间",
                            "OldValue":"2021/11/20 7:05:06",
                            "NewValue":"2021/11/20 11:05:06"
                        },
                        {
                            "DisplayName":"结束时间",
                            "OldValue":"2021/11/20 8:05:06",
                            "NewValue":"2021/11/20 12:00:06"
                        },
                        {
                            "DisplayName":"时长",
                            "OldValue":"60",
                            "NewValue":"55"
                        },
                        {
                            "DisplayName":"参考值",
                            "OldValue":"85.6",
                            "NewValue":"185.6"
                        }
                    ]
                },
                {
                    "Operator":"添加",
                    "ClassName":"NoiseRecord",
                    "DisplayName":"采样记录-数据项[2021-11-21]",
                    "Logs":[
                        {
                            "Operator":"被更改",
                            "ClassName":"NoiseRecordItem",
                            "DisplayName":"采样明细",
                            "Logs":[
                                {
                                    "Operator":"添加",
                                    "ClassName":"NoiseRecordItem",
                                    "DisplayName":"采样明细-数据项[二氧化碳]",
                                    "Logs":[

                                    ],
                                    "Items":[
                                        {
                                            "DisplayName":"标准值",
                                            "OldValue":"0",
                                            "NewValue":"119.9"
                                        },
                                        {
                                            "DisplayName":"测试值",
                                            "OldValue":"0",
                                            "NewValue":"118.9"
                                        },
                                        {
                                            "DisplayName":"测试备注",
                                            "OldValue":null,
                                            "NewValue":"差差的5"
                                        }
                                    ]
                                },
                                {
                                    "Operator":"添加",
                                    "ClassName":"NoiseRecordItem",
                                    "DisplayName":"采样明细-数据项[一氧化碳]",
                                    "Logs":[

                                    ],
                                    "Items":[
                                        {
                                            "DisplayName":"标准值",
                                            "OldValue":"0",
                                            "NewValue":"118.9"
                                        },
                                        {
                                            "DisplayName":"测试值",
                                            "OldValue":"0",
                                            "NewValue":"118.9"
                                        },
                                        {
                                            "DisplayName":"测试备注",
                                            "OldValue":null,
                                            "NewValue":"差差的6"
                                        }
                                    ]
                                }
                            ],
                            "Items":[

                            ]
                        }
                    ],
                    "Items":[
                        {
                            "DisplayName":"开始时间",
                            "OldValue":"0001/1/1 0:00:00",
                            "NewValue":"2021/11/21 11:05:06"
                        },
                        {
                            "DisplayName":"结束时间",
                            "OldValue":"0001/1/1 0:00:00",
                            "NewValue":"2021/11/21 12:00:06"
                        },
                        {
                            "DisplayName":"时长",
                            "OldValue":"0",
                            "NewValue":"55"
                        },
                        {
                            "DisplayName":"参考值",
                            "OldValue":"0",
                            "NewValue":"185.6"
                        }
                    ]
                }
            ],
            "Items":[

            ]
        }
    ],
    "Items":[
        {
            "DisplayName":"采样日期",
            "OldValue":"2021/10/1 10:08:09",
            "NewValue":"2021/10/2 10:08:09"
        },
        {
            "DisplayName":"测试设备",
            "OldValue":"TAX09832",
            "NewValue":"TAX034333"
        }
    ]
}
  1. 至此就可以拿到我们的变更记录了。
    8.将变更记录转成PDF格式的文件,是另一个需求,使用Itext7操作。
using System;
using System.IO;
using iText.Kernel.Events;
using iText.Kernel.Geom;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas;
using iText.Layout;
using iText.Layout.Element;
using iText.Kernel.Colors;
using iText.Layout.Properties;
using iText.Layout.Borders;
using iText.Kernel.Font;
using iText.IO.Font;
using static iText.Kernel.Font.PdfFontFactory;
using iText.IO.Image;
using Favor.Example.RecordDiff;
using System.Collections.Generic;

namespace Favor.Example.Itext
{
    public class PdfHelper
    {
        public static void GenerateEntityChangePdf(EntityChangeLog changeLog)
        {
            var filePath = $@"...\DateTime.Now.ToFileTime()}.pdf"; // 随便声明路径
            var fontPath = $@"...\SIMHEI_0.TTF";// 声明字体路径
            var imageLogo = $@"...\SGS_RGB_12mm.bmp"; // 声明页眉图片

            using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
            {
                using (var pdfdocument = new PdfDocument(new PdfWriter(fileStream)))
                {
                    var pdfFont = PdfFontFactory.CreateFont(fontPath, PdfEncodings.IDENTITY_H, EmbeddingStrategy.PREFER_EMBEDDED);
                    var document = new Document(pdfdocument, PageSize.A4).SetFont(pdfFont);
                    var docwidth = pdfdocument.GetDefaultPageSize().GetWidth();
                    var tableWidth = docwidth - document.GetRightMargin() - document.GetLeftMargin();
                    pdfdocument.AddEventHandler(PdfDocumentEvent.END_PAGE, new HeaderFooterHandler(imageLogo, pdfFont));

                    document.Add(CreatePara($"{changeLog.DisplayName}-{changeLog.Operator}明细"));
                    var table = new Table(new float[3]).UseAllAvailableWidth();
                    GenEntityLogItem(table, changeLog.Items, changeLog.Operator);
                    GenEntityLog(table, changeLog.Logs);
                    document.Add(table);
                    document.Close();
                }
            }

        }

        private static void GenEntityLogItem(Table table, List<EntityChangeLogItem> logItems, string oper)  
        {
            if (logItems.Count == 0) return;
            AddTCell(table, "字段", 100);
            AddTCell(table, "旧值", null);
            AddTCell(table, "新值", null);
            foreach (var item in logItems)
            {
                AddTCell(table, item.DisplayName, 100);
                AddTCell(table, CellOldValue(item.OldValue, oper), TextAlignment.LEFT);
                AddTCell(table, CellNewValue(item.NewValue, oper), TextAlignment.LEFT);
            }
        }

        private static string CellOldValue(string value,string oper)
        {
            if (string.IsNullOrWhiteSpace(value)) return string.Empty;
            if (oper == "添加") return string.Empty;
            return value;
        }

        private static string CellNewValue(string value, string oper)
        {
            if (string.IsNullOrWhiteSpace(value)) return string.Empty;
            if (oper == "删除") return string.Empty;
            return value;
        }

        public static void AddTCell(Table table, string content, float? width)
        {
            var cell = new Cell();
            if (width.HasValue)
            {
                cell.SetWidth(width.Value);
            }
            cell.SetBold();
            cell.SetBackgroundColor(ColorConstants.LIGHT_GRAY, 0.5f);
            cell.Add(CreatePara(content, 10));
            table.AddCell(cell);
        }

        public static void AddTCell(Table table, string content, TextAlignment alignment)
        {
            var cell = new Cell().SetMinWidth(200).Add(CreatePara(content, 10, alignment));
            table.AddCell(cell);
        }

        private static void GenEntityLog(Table table, List<EntityChangeLog> logs)
        {
            if (logs.Count == 0) return;

            foreach (var log in logs)
            {
                if (log.Operator == "被更改")
                {
                    var cell0 = new Cell(1,3);
                    cell0.SetHeight(5).SetBackgroundColor(ColorConstants.BLACK, 0.5f);
                    table.AddCell(cell0);
                }

                var cell1 = new Cell(1,3);
                cell1.Add(CreatePara($"{log.DisplayName}-{log.Operator}"));
                table.AddCell(cell1);

                GenEntityLogItem(table, log.Items, log.Operator);
                GenEntityLog(table, log.Logs);
            }
        }

        private static Paragraph CreatePara(string text,float fontSize = 12f, TextAlignment alignment = TextAlignment.CENTER)
        {
            return new Paragraph(text).SetMargin(1f)
               .SetFontSize(fontSize) 
               .SetTextAlignment(alignment);
        }

 
    public class HeaderFooterHandler : IEventHandler
    {
        private readonly PdfFont Font;
        private readonly string ImageLogo;

        public HeaderFooterHandler(string imageLogo,PdfFont font)
        {
            this.ImageLogo = imageLogo;
            this.Font = font; 
        }


        public void HandleEvent(Event @event)
        {
            try
            {
                PdfDocumentEvent docEvent = (PdfDocumentEvent)@event;
                PdfDocument pdfDoc = docEvent.GetDocument();
                Document doc = new Document(pdfDoc);
                PdfPage page = docEvent.GetPage();
                int pageNumber = pdfDoc.GetPageNumber(page);
                Rectangle pageSize = page.GetPageSize();
                var pdfWidth = pageSize.GetWidth();
                var pdfHeight = pageSize.GetHeight();

                PdfCanvas pdfCanvas = new PdfCanvas(page.NewContentStreamBefore(), page.GetResources(), pdfDoc);
                Color lineColor = new DeviceRgb(133, 135, 134);
                pdfCanvas.SetLineWidth(1.5f).SetStrokeColor(lineColor);
                var tableWidth = pdfWidth - doc.GetRightMargin() - doc.GetLeftMargin();

                // 1.生成页眉
                var x0 = doc.GetRightMargin();
                var y0 = pdfHeight - doc.GetTopMargin();
                pdfCanvas.MoveTo(x0, y0).LineTo(pdfWidth - doc.GetRightMargin(), y0).Stroke();
                var headerTable = new Table(2)
                    .SetHeight(30).SetFixedLayout()
                    .SetWidth(tableWidth).SetHorizontalAlignment(HorizontalAlignment.CENTER);


                var image = new Image(ImageDataFactory.Create(this.ImageLogo));
                var cellhead1 = new Cell().SetBorder(Border.NO_BORDER).Add(image)
                    .SetVerticalAlignment(VerticalAlignment.BOTTOM);
                headerTable.AddCell(cellhead1);

                var paraHead2 = new Paragraph()
                    .SetVerticalAlignment(VerticalAlignment.BOTTOM)
                    .Add(new Tab()).AddTabStops(new TabStop(1000, TabAlignment.RIGHT))
                    .Add("页眉内容").SetFontSize(10f).SetFontColor(lineColor);
                var cellHead2 = new Cell().SetBorder(Border.NO_BORDER).Add(paraHead2).SetFont(this.Font);
                headerTable.AddCell(cellHead2);

                // 设置页眉位置
                headerTable.SetFixedPosition(doc.GetLeftMargin(), pdfHeight - doc.GetTopMargin(), tableWidth);
                doc.Add(headerTable);

                // 2.生成页脚
                pdfCanvas.MoveTo(x0, doc.GetBottomMargin()).LineTo(pdfWidth - doc.GetRightMargin(), doc.GetBottomMargin()).Stroke();
                var footerTable = new Table(2)
                    .SetHeight(40).SetFixedLayout()
                    .SetWidth(tableWidth).SetHorizontalAlignment(HorizontalAlignment.CENTER);

                var paraFoot1 = new Paragraph()
                    .SetVerticalAlignment(VerticalAlignment.BOTTOM)
                    .Add($"第{pageNumber}页/共{pdfDoc.GetNumberOfPages()}页")
                    .SetFontSize(8f).SetFontColor(lineColor);
                var cellFoot1 = new Cell().SetBorder(Border.NO_BORDER).Add(paraFoot1).SetFont(this.Font);
                footerTable.AddCell(cellFoot1);

                var paraFoot2 = new Paragraph()
                    .SetVerticalAlignment(VerticalAlignment.BOTTOM)
                    .Add(new Tab()).AddTabStops(new TabStop(1000, TabAlignment.RIGHT))
                    .Add($"生成日期:{DateTime.Now.ToLongDateString()}")
                    .SetFontSize(8f).SetFontColor(lineColor);
                var cellFoot2 = new Cell().SetBorder(Border.NO_BORDER).Add(paraFoot2).SetFont(this.Font);
                footerTable.AddCell(cellFoot2);

                 // 设置页脚位置
                footerTable.SetFixedPosition(doc.GetLeftMargin(), doc.GetBottomMargin() - footerTable.GetHeight().GetValue(), tableWidth);
                doc.Add(footerTable);
            }
            catch (Exception ex)
            {
                throw;
            }
        }
    }
}

  1. 调用GenerateEntityChangePdf()方法,生成PDF文件。如下:


    image.png
image.png

基本上实现了想要的需求,欢迎各位大佬提意见哈!!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容