一、前言

在平时的开发中,当用户修改数据时,一直没有很好的办法来记录具体修改了那些信息,只能暂时采用将类序列化成 json 字符串,然后全塞入到日志中的方式,此时如果我们想要知道用户具体改变了哪几个字段的值的话就很困难了。因此,趁着这个假期,就来解决这个一直遗留的小问题,本篇文章记录了我目前实现的方法,如果你有不同于文中所列出的方案的话,欢迎指出。

代码仓储地址:https://github.com/Lanesra712/ingos-common/tree/master/sample/csharp/get-data-changed-properties

二、Step by Step

1、需求场景

一个经常遇到的使用场景,用户 A 修改了某个表单页面上的数据信息,然后提交到我们的服务端完成数据的更新,对于具有某些权限的用户来说,则是期望可以看到所有用户对于该表单进行操作前后的数据变更。

2、解决方法

既然想要得知用户操作前后的数据差异,我们肯定需要去对用户操作前后的数据进行比对,这里就落到我们承接数据的类身上。

在我们定义类中的属性时,更多的是使用自动属性的方式来完成属性的 getter、setter 声明,而完整的属性声明方式则需要我们定义一个字段用来承接对于该属性的变更。

  1. // 自动属性声明
  2. public class Entity1
  3. {
  4. public Guid Id { get; set; }
  5. }
  6.  
  7. // 完整的属性声明
  8. public class Entity2
  9. {
  10. private Guid _id;
  11.  
  12. public Guid Id
  13. {
  14. get => _id;
  15. set => _id = value;
  16. }
  17. }

因为在给属性进行赋值的时候,需要调用属性的 set 构造器,因此,在 set 构造器内部我们是不是就可以直接对新赋的值进行判断,从而记录下属性的变更过程,改造后的类属性声明代码如下。

  1. public class Sample
  2. {
  3. private string _a;
  4.  
  5. public string A
  6. {
  7. get => _a;
  8. set
  9. {
  10. if (_a == value)
  11. return;
  12.  
  13. string old = _a;
  14. _a = value;
  15. propertyChangelogs.Add(new PropertyChangelog<Sample>(nameof(A), old, _a));
  16. }
  17. }
  18.  
  19. private double _b;
  20.  
  21. public double B
  22. {
  23. get => _b;
  24. set
  25. {
  26. if (_b == value)
  27. return;
  28.  
  29. double old = _b;
  30. _b = value;
  31. propertyChangelogs.Add(new PropertyChangelog<Sample>(nameof(B), old.ToString(), _b.ToString()));
  32. }
  33. }
  34.  
  35. private IList<PropertyChangelog<Sample>> propertyChangelogs = new List<PropertyChangelog<Sample>>();
  36.  
  37. public IEnumerable<PropertyChangelog<Sample>> Changelogs() => propertyChangelogs;
  38. }

在改造后的类属性声明中,我们在属性的 set 构造器中将新赋的值与原先的值进行判断,当存在两次值不一样时,就写入到变更记录的集合中,从而实现记录数据变更的目的。这里对于变更记录的实体类属性定义如下所示。

  1. public class PropertyChangelog<T>
  2. {
  3. /// <summary>
  4. /// ctor
  5. /// </summary>
  6. public PropertyChangelog()
  7. { }
  8.  
  9. /// <summary>
  10. /// ctor
  11. /// </summary>
  12. /// <param name="propertyName">属性名称</param>
  13. /// <param name="oldValue">旧值</param>
  14. /// <param name="newValue">新值</param>
  15. public PropertyChangelog(string propertyName, string oldValue, string newValue)
  16. {
  17. PropertyName = propertyName;
  18. OldValue = oldValue;
  19. NewValue = newValue;
  20. }
  21.  
  22. /// <summary>
  23. /// ctor
  24. /// </summary>
  25. /// <param name="className">类名</param>
  26. /// <param name="propertyName">属性名称</param>
  27. /// <param name="oldValue">旧值</param>
  28. /// <param name="newValue">新值</param>
  29. /// <param name="changedTime">修改时间</param>
  30. public PropertyChangelog(string className, string propertyName, string oldValue, string newValue, DateTime changedTime)
  31. : this(propertyName, oldValue, newValue)
  32. {
  33. ClassName = className;
  34. ChangedTime = changedTime;
  35. }
  36.  
  37. /// <summary>
  38. /// 类名称
  39. /// </summary>
  40. public string ClassName { get; set; } = typeof(T).FullName;
  41.  
  42. /// <summary>
  43. /// 属性名称
  44. /// </summary>
  45. public string PropertyName { get; set; }
  46.  
  47. /// <summary>
  48. /// 旧值
  49. /// </summary>
  50. public string OldValue { get; set; }
  51.  
  52. /// <summary>
  53. /// 新值
  54. /// </summary>
  55. public string NewValue { get; set; }
  56.  
  57. /// <summary>
  58. /// 修改时间
  59. /// </summary>
  60. public DateTime ChangedTime { get; set; } = DateTime.Now;
  61. }

可以看到,在我们对 Sample 类进行初始化赋值时,记录了两次关于类属性的数据变更记录,而当我们进行重新赋值时,只有属性 A 发生了数据改变,因此只记录了属性 A 的数据变更记录。

虽然这里已经达到我们的目的,但是如果采用这种方式的话,相当于原先项目中需要实现数据记录功能的类的属性声明方式全部需要重写,同时,基于 C# 本身已经提供了自动属性的方式来简化属性声明,结果现在我们又回到了传统属性的声明方式,似乎显得有些不太聪明的样子。因此,既然通过一个个属性进行比较的方式过于繁琐,这里我们通过反射的方式直接对比修改前后的两个实体类,批量获取发生数据变更的属性信息。

我们最终想要实现的是用户可以看到关于某个表单的字段属性数据变化的过程,而我们定义在 C# 类中的属性有时候需要与实际页面上显示的字段名称进行映射,以及某些属性其实没有必要记录数据变化的情况,这里我通过添加自定义特性的方式,完善功能的实现。

  1. /// <summary>
  2. /// 为指定的属性设定数据变更记录
  3. /// </summary>
  4. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
  5. public class PropertyChangeTrackingAttribute : Attribute
  6. {
  7. /// <summary>
  8. /// 指定 PropertyChangeTrackingAttribute 属性的默认值
  9. /// </summary>
  10. public static readonly PropertyChangeTrackingAttribute Default = new PropertyChangeTrackingAttribute();
  11.  
  12. /// <summary>
  13. /// 构造一个新的 PropertyChangeTrackingAttribute 特性实例
  14. /// </summary>
  15. public PropertyChangeTrackingAttribute()
  16. { }
  17.  
  18. /// <summary>
  19. /// 构造一个新的 PropertyChangeTrackingAttribute 特性实例
  20. /// </summary>
  21. /// <param name="ignore">是否忽略该字段的数据变化</param>
  22. public PropertyChangeTrackingAttribute(bool ignore = false)
  23. {
  24. IgnoreValue = ignore;
  25. }
  26.  
  27. /// <summary>
  28. /// 构造一个新的 PropertyChangeTrackingAttribute 特性实例
  29. /// </summary>
  30. /// <param name="displayName">属性对应页面显示名称</param>
  31. public PropertyChangeTrackingAttribute(string displayName)
  32. : this(false)
  33. {
  34. DisplayNameValue = displayName;
  35. }
  36.  
  37. /// <summary>
  38. /// 构造一个新的 PropertyChangeTrackingAttribute 特性实例
  39. /// </summary>
  40. /// <param name="displayName">属性对应页面显示名称</param>
  41. /// <param name="ignore">是否忽略该字段的数据变化</param>
  42. public PropertyChangeTrackingAttribute(string displayName, bool ignore)
  43. : this(ignore)
  44. {
  45. DisplayNameValue = displayName;
  46. }
  47.  
  48. /// <summary>
  49. /// 获取特性中的属性对应页面上显示名称参数信息
  50. /// </summary>
  51. public virtual string DisplayName => DisplayNameValue;
  52.  
  53. /// <summary>
  54. /// 获取特性中的是否忽略该字段的数据变化参数信息
  55. /// </summary>
  56. public virtual bool Ignore => IgnoreValue;
  57.  
  58. /// <summary>
  59. /// 修改属性对应页面显示名称参数值
  60. /// </summary>
  61. protected string DisplayNameValue { get; set; }
  62.  
  63. /// <summary>
  64. /// 修改是否忽略该字段的数据变化
  65. /// </summary>
  66. protected bool IgnoreValue { get; set; }
  67. }

考虑到我们的类中可能会包含很多的属性信息,如果一个个的给属性添加特性会很麻烦,因此这里可以直接针对类添加该特性。同时,针对我们可能会排除类中的某些属性,或者设定属性在页面中显示的名称,这里我们可以针对特定的类属性进行单独添加特性。

完成了自定义特性之后,考虑到我们后续使用的方便,这里我采用创建扩展方法的形式来声明我们的函数方法,同时我在 PropertyChangelog 类中添加了 DisplayName 属性用来存放属性对应于页面上存放的名称,最终完成后的代码如下所示。

  1. /// <summary>
  2. /// 获取类属性数据变化记录
  3. /// </summary>
  4. /// <typeparam name="T">监听的类类型</typeparam>
  5. /// <param name="oldObj">包含原始值的类</param>
  6. /// <param name="newObj">变更属性值后的类</param>
  7. /// <param name="propertyName">指定的属性名称</param>
  8. /// <returns></returns>
  9. public static IEnumerable<PropertyChangelog<T>> GetPropertyLogs<T>(this T oldObj, T newObj, string propertyName = null)
  10. {
  11. IList<PropertyChangelog<T>> changelogs = new List<PropertyChangelog<T>>();
  12.  
  13. // 1、获取需要添加数据变更记录的属性信息
  14. //
  15. IList<PropertyInfo> properties = new List<PropertyInfo>();
  16.  
  17. // PropertyChangeTracking 特性的类型
  18. var attributeType = typeof(PropertyChangeTrackingAttribute);
  19.  
  20. // 对应的类中包含的属性信息
  21. var classProperties = typeof(T).GetProperties();
  22.  
  23. // 获取类中需要添加变更记录的属性信息
  24. //
  25. bool flag = Attribute.IsDefined(typeof(T), attributeType);
  26.  
  27. foreach (var i in classProperties)
  28. {
  29. // 获取当前属性添加的特性信息
  30. var attributeInfo = (PropertyChangeTrackingAttribute)i.GetCustomAttribute(attributeType);
  31.  
  32. // 类未添加特性,并且该属性也未添加特性
  33. if (!flag && attributeInfo == null)
  34. continue;
  35.  
  36. // 类添加特性,该属性未添加特性
  37. if (flag && attributeInfo == null)
  38. properties.Add(i);
  39.  
  40. // 不管类有没有添加特性,只要类中的属性添加特性,并且 Ignore 为 false
  41. if (attributeInfo != null && !attributeInfo.Ignore)
  42. properties.Add(i);
  43. }
  44.  
  45. // 2、判断指定的属性数据是否发生变更
  46. //
  47. foreach (var property in properties)
  48. {
  49. var oldValue = property.GetValue(oldObj) ?? "";
  50. var newValue = property.GetValue(newObj) ?? "";
  51.  
  52. if (oldValue.Equals(newValue))
  53. continue;
  54.  
  55. // 获取当前属性在页面上显示的名称
  56. //
  57. var attributeInfo = (PropertyChangeTrackingAttribute)property.GetCustomAttribute(attributeType);
  58. string displayName = attributeInfo == null ? property.Name
  59. : attributeInfo.DisplayName;
  60.  
  61. changelogs.Add(new PropertyChangelog<T>(property.Name, displayName, oldValue.ToString(), newValue.ToString()));
  62. }
  63.  
  64. return string.IsNullOrEmpty(propertyName) ? changelogs
  65. : changelogs.Where(i => i.PropertyName.Equals(propertyName));
  66. }

在下面的这个测试案例中,Entity 类实际上只会记录 5 个属性的数据变化,我们手动创建两个 Entity 类实例,同时改变两个类实例对应的属性值。从我们运行的示意图中可以看到,虽然两个类实例的 Id 属性值不同,但是因为被我们手动忽略了,所以最终只显示我们设定的几个属性的变化信息。

  1. [PropertyChangeTracking]
  2. public class Entity
  3. {
  4. [PropertyChangeTracking(ignore: true)]
  5. public Guid Id { get; set; }
  6.  
  7. [PropertyChangeTracking(displayName: "序号")]
  8. public string OId { get; set; }
  9.  
  10. [PropertyChangeTracking(displayName: "第一个字段")]
  11. public string A { get; set; }
  12.  
  13. public double B { get; set; }
  14.  
  15. public bool C { get; set; }
  16.  
  17. public DateTime Date { get; set; } = DateTime.Now;
  18. }

三、总结

这一章是针对我之前在工作中遇到的一个问题,趁着假期考虑的一个解决方法,虽然只是一个小问题,但是还是挺有借鉴意义的,如果能够给你在日常的开发中提供些许的帮助,不胜荣幸。

如何获取 C# 类中发生数据变化的属性信息的更多相关文章

  1. 【记录】mybatis中获取常量类中数据

    部分转载,已注明来源: 1.mybatis中获取常量类中数据 <update id="refuseDebt"> UPDATE dt_debt a SET         ...

  2. 项目中通过Sorlj获取索引库中的数据

    在开发项目中通过使用Solr所提供的Solrj(java客户端)获取索引库中的数据,这才是真正对项目起实质性作用的功能,提升平台的检索性能及检索结果的精确性 第一步,引入相关依赖的jar包 第二步,根 ...

  3. cxf,两个声明导致 ObjectFactory 类中发生冲突

    说明先,这里不管是client还是server端都是用java语言编写,如有写得不好,望原谅! 问题 http://localhost:8080/WEB-SMVC/cxf/userService?ws ...

  4. 获取class对象的三种方法以及通过Class对象获取某个类中变量,方法,访问成员

    public class ReflexAndClass { public static void main(String[] args) throws Exception { /** * 获取Clas ...

  5. .NET获取Html字符串中指定标签的指定属性的值

    using System.Text; using System.Text.RegularExpressions; //以上为要用到的命名空间 /// <summary> /// 获取Htm ...

  6. [爬虫]通过url获取连接地址中的数据

    1. 要想获取指定连接的数据,那么就得使用HtmlDocument对象,要想使用HtmlDocument对象就必需引用using HtmlAgilityPack; 2. 详细步骤如下:     步骤一 ...

  7. C#:实体类中做数据验证

    主要是在实体类中验证 using System; namespace Jone.Function.attribute{        /// <summary>        /// 附加 ...

  8. asp.net网页上获取其中表格中的数据(爬数据)

    下面的方法获取页面中表格数据,每个页面不相同,获取的方式(主要是正则表达式)不一样,只是提供方法参考.大神勿喷,刚使用了,就记下来了. 其中数据怎么存,主要就看着怎么使用了.只是方便记录就都放在lis ...

  9. 使用property为类中的数据添加行为

    对于面向对象编程特别重要的是,关注行为和数据的分离. 在这之前,先来讨论一些“坏”的面向对象理论,这些都告诉我们绝不要直接访问属性(如Java): class Color: def __init__( ...

随机推荐

  1. 洛谷p2149----两个终点和两个起点,最短路最大交汇长度!!!

    说实话,这题真第一次见,学到了不少有趣的东西,因吹丝汀!! 思路:因为不可能同时并行和相遇(我也不知道为啥,等我会证明了就来说说) 所以正向建边再反向建边,拓扑排序+dp求最下长路,记录下最大的就是解 ...

  2. mysql锁及四种事务隔离级别笔记

    前言 数据库是一个共享资源,为了充分利用数据库资源,发挥数据 库共享资源的特点,应该允许多个用户并行地存取数据库.但这样就会产生多个用户程序并 发存取同一数据的情况,为了避免破坏一致性,所以必须提供并 ...

  3. zabbix脚本监控mysql

    Zabbix监控mysql 1.1 客户端配置 1.1.1 安装客户端包 yum -y install unixODBC rpm -ivh zabbix-agent--.el6.x86_64.rpm ...

  4. OpenVINO 入门

    关于OpenVINO 入门,今天给大家分享一个好东西和好消息! 现如今,说人工智能(AI)正在重塑我们的各行各业绝不虚假,深度学习神经网络的研究可谓如火如荼, 但这一流程却相当复杂,但对于初学者来说也 ...

  5. Java容器知识总结

    剖析面试最常见问题之Java集合框架 说说List,Set,Map三者的区别? List(对付顺序的好帮手): List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象 Set(注重独一 ...

  6. 大数据学习之路-hdfs

    1.什么是hadoop hadoop中有3个核心组件: 分布式文件系统:HDFS —— 实现将文件分布式存储在很多的服务器上 分布式运算编程框架:MAPREDUCE —— 实现在很多机器上分布式并行运 ...

  7. 爆破linux密码 $6$3uwqC9JI$d9iPRmTDAoXs/IbsplxS3iyeErHqw7fUycacXNHyZk1UCSwFEydl515/zXN7OEwHnyUaqYcNG

    #!/usr/bin/env python # -*- coding:UTF-8 -*- import crypt import sys # 哈希密码的前两位就是盐的前两位,这里我们假设盐只有两位. ...

  8. 一个简单的spring boot程序

    搭建一个spring boot项目十分的方便,网上也有许多,可以参考 https://www.cnblogs.com/ityouknow/p/5662753.html 进行项目的搭建.在此我就不详细介 ...

  9. Project Settings之Quality翻译

    (版本是2018.4......翻译是自己的渣翻译水平) Unity allows you to set the level of graphical quality it attempts to r ...

  10. js提取JSON数据中需要的那部分数据

    var data =[ { name: "程咬金",sex:"1",age:26 }, { name: "程才",sex:"0&q ...