详解如何获取C#类中发生数据变化的属性信息

 更新时间:2020年05月22日 10:12:47   作者:墨墨墨墨小宇  
这篇文章主要介绍了详解如何获取C#类中发生数据变化的属性信息,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一、前言#

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

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

二、Step by Step#

1、需求场景#

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

2、解决方法#

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

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

// 自动属性声明
public class Entity1
{
  public Guid Id { get; set; }
}

// 完整的属性声明
public class Entity2
{
  private Guid _id;

  public Guid Id
  {
    get => _id;
    set => _id = value;
  }
}

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

public class Sample
{
  private string _a;

  public string A
  {
    get => _a;
    set
    {
      if (_a == value)
        return;

      string old = _a;
      _a = value;
      propertyChangelogs.Add(new PropertyChangelog<Sample>(nameof(A), old, _a));
    }
  }

  private double _b;

  public double B
  {
    get => _b;
    set
    {
      if (_b == value)
        return;

      double old = _b;
      _b = value;
      propertyChangelogs.Add(new PropertyChangelog<Sample>(nameof(B), old.ToString(), _b.ToString()));
    }
  }

  private IList<PropertyChangelog<Sample>> propertyChangelogs = new List<PropertyChangelog<Sample>>();

  public IEnumerable<PropertyChangelog<Sample>> Changelogs() => propertyChangelogs;
}

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

public class PropertyChangelog<T>
{
  /// <summary>
  /// ctor
  /// </summary>
  public PropertyChangelog()
  { }

  /// <summary>
  /// ctor
  /// </summary>
  /// <param name="propertyName">属性名称</param>
  /// <param name="oldValue">旧值</param>
  /// <param name="newValue">新值</param>
  public PropertyChangelog(string propertyName, string oldValue, string newValue)
  {
    PropertyName = propertyName;
    OldValue = oldValue;
    NewValue = newValue;
  }

  /// <summary>
  /// ctor
  /// </summary>
  /// <param name="className">类名</param>
  /// <param name="propertyName">属性名称</param>
  /// <param name="oldValue">旧值</param>
  /// <param name="newValue">新值</param>
  /// <param name="changedTime">修改时间</param>
  public PropertyChangelog(string className, string propertyName, string oldValue, string newValue, DateTime changedTime)
    : this(propertyName, oldValue, newValue)
  {
    ClassName = className;
    ChangedTime = changedTime;
  }

  /// <summary>
  /// 类名称
  /// </summary>
  public string ClassName { get; set; } = typeof(T).FullName;

  /// <summary>
  /// 属性名称
  /// </summary>
  public string PropertyName { get; set; }

  /// <summary>
  /// 旧值
  /// </summary>
  public string OldValue { get; set; }

  /// <summary>
  /// 新值
  /// </summary>
  public string NewValue { get; set; }

  /// <summary>
  /// 修改时间
  /// </summary>
  public DateTime ChangedTime { get; set; } = DateTime.Now;
}

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

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

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

/// <summary>
/// 为指定的属性设定数据变更记录
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
public class PropertyChangeTrackingAttribute : Attribute
{
  /// <summary>
  /// 指定 PropertyChangeTrackingAttribute 属性的默认值
  /// </summary>
  public static readonly PropertyChangeTrackingAttribute Default = new PropertyChangeTrackingAttribute();

  /// <summary>
  /// 构造一个新的 PropertyChangeTrackingAttribute 特性实例
  /// </summary>
  public PropertyChangeTrackingAttribute()
  { }

  /// <summary>
  /// 构造一个新的 PropertyChangeTrackingAttribute 特性实例
  /// </summary>
  /// <param name="ignore">是否忽略该字段的数据变化</param>
  public PropertyChangeTrackingAttribute(bool ignore = false)
  {
    IgnoreValue = ignore;
  }

  /// <summary>
  /// 构造一个新的 PropertyChangeTrackingAttribute 特性实例
  /// </summary>
  /// <param name="displayName">属性对应页面显示名称</param>
  public PropertyChangeTrackingAttribute(string displayName)
    : this(false)
    {
      DisplayNameValue = displayName;
    }

  /// <summary>
  /// 构造一个新的 PropertyChangeTrackingAttribute 特性实例
  /// </summary>
  /// <param name="displayName">属性对应页面显示名称</param>
  /// <param name="ignore">是否忽略该字段的数据变化</param>
  public PropertyChangeTrackingAttribute(string displayName, bool ignore)
    : this(ignore)
    {
      DisplayNameValue = displayName;
    }

  /// <summary>
  /// 获取特性中的属性对应页面上显示名称参数信息
  /// </summary>
  public virtual string DisplayName => DisplayNameValue;

  /// <summary>
  /// 获取特性中的是否忽略该字段的数据变化参数信息
  /// </summary>
  public virtual bool Ignore => IgnoreValue;

  /// <summary>
  /// 修改属性对应页面显示名称参数值
  /// </summary>
  protected string DisplayNameValue { get; set; }

  /// <summary>
  /// 修改是否忽略该字段的数据变化
  /// </summary>
  protected bool IgnoreValue { get; set; }
}

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

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

/// <summary>
/// 获取类属性数据变化记录
/// </summary>
/// <typeparam name="T">监听的类类型</typeparam>
/// <param name="oldObj">包含原始值的类</param>
/// <param name="newObj">变更属性值后的类</param>
/// <param name="propertyName">指定的属性名称</param>
/// <returns></returns>
public static IEnumerable<PropertyChangelog<T>> GetPropertyLogs<T>(this T oldObj, T newObj, string propertyName = null)
{
  IList<PropertyChangelog<T>> changelogs = new List<PropertyChangelog<T>>();

  // 1、获取需要添加数据变更记录的属性信息
  //
  IList<PropertyInfo> properties = new List<PropertyInfo>();

  // PropertyChangeTracking 特性的类型
  var attributeType = typeof(PropertyChangeTrackingAttribute);

  // 对应的类中包含的属性信息
  var classProperties = typeof(T).GetProperties();

  // 获取类中需要添加变更记录的属性信息
  //
  bool flag = Attribute.IsDefined(typeof(T), attributeType);

  foreach (var i in classProperties)
  {
    // 获取当前属性添加的特性信息
    var attributeInfo = (PropertyChangeTrackingAttribute)i.GetCustomAttribute(attributeType);

    // 类未添加特性,并且该属性也未添加特性
    if (!flag && attributeInfo == null)
      continue;

    // 类添加特性,该属性未添加特性
    if (flag && attributeInfo == null)
      properties.Add(i);

    // 不管类有没有添加特性,只要类中的属性添加特性,并且 Ignore 为 false
    if (attributeInfo != null && !attributeInfo.Ignore)
      properties.Add(i);
  }

  // 2、判断指定的属性数据是否发生变更
  //
  foreach (var property in properties)
  {
    var oldValue = property.GetValue(oldObj) ?? "";
    var newValue = property.GetValue(newObj) ?? "";

    if (oldValue.Equals(newValue))
      continue;

    // 获取当前属性在页面上显示的名称
    //
    var attributeInfo = (PropertyChangeTrackingAttribute)property.GetCustomAttribute(attributeType);
    string displayName = attributeInfo == null ? property.Name
      : attributeInfo.DisplayName;

    changelogs.Add(new PropertyChangelog<T>(property.Name, displayName, oldValue.ToString(), newValue.ToString()));
  }

  return string.IsNullOrEmpty(propertyName) ? changelogs
    : changelogs.Where(i => i.PropertyName.Equals(propertyName));
}

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

[PropertyChangeTracking]
public class Entity
{
  [PropertyChangeTracking(ignore: true)]
  public Guid Id { get; set; }

  [PropertyChangeTracking(displayName: "序号")]
  public string OId { get; set; }

  [PropertyChangeTracking(displayName: "第一个字段")]
  public string A { get; set; }

  public double B { get; set; }

  public bool C { get; set; }

  public DateTime Date { get; set; } = DateTime.Now;
}

三、总结#

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

作者:墨墨墨墨小宇

出处:https://www.cnblogs.com/danvic712/p/how-to-get-the-data-changed-properties-in-csharp-class.html

到此这篇关于详解如何获取C#类中发生数据变化的属性信息的文章就介绍到这了,更多相关C#获取类属性信息内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 大白话讲解C# 中的委托

    大白话讲解C# 中的委托

    这篇文章主要介绍了C# 中的委托的相关资料,帮助初学者更好的理解和使用c#,感兴趣的朋友可以了解下
    2020-11-11
  • C#使用channel实现Plc异步任务之间的通信

    C#使用channel实现Plc异步任务之间的通信

    在C#的并发编程中,Channel是一种非常强大的数据结构,用于在生产者和消费者之间进行通信,本文将给大家介绍C#使用channel实现Plc异步任务之间的通信,文中有相关的代码示例供大家参考,感兴趣的朋友跟着小编一起来看看吧
    2024-05-05
  • C#集合之可观察集合的用法

    C#集合之可观察集合的用法

    这篇文章介绍了C#集合之可观察集合的用法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-04-04
  • C#引用类型转换的常见方式总结

    C#引用类型转换的常见方式总结

    这篇文章主要介绍了C#引用类型转换的常见方式,包括子类转换成父类,父类转换成子类,以及不是子父级关系类之间的转换,需要的朋友可以参考下
    2014-09-09
  • C#之多余控件事件及代码删除问题

    C#之多余控件事件及代码删除问题

    这篇文章主要介绍了C#之多余控件事件及代码删除问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-01-01
  • C#中进程的挂起与恢复

    C#中进程的挂起与恢复

    这篇文章主要介绍了C#中进程的挂起与恢复操作方法,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2017-03-03
  • C#遍历集合与移除元素的方法

    C#遍历集合与移除元素的方法

    这篇文章主要介绍了C#遍历集合与移除元素的方法,结合实例形式分析了C#使用for循环遍历集合以及add与Remove方法进行元素添加与移除的使用技巧,需要的朋友可以参考下
    2016-06-06
  • C#使用TensorFlow.NET训练自己的数据集的方法

    C#使用TensorFlow.NET训练自己的数据集的方法

    这篇文章主要介绍了C#使用TensorFlow.NET训练自己的数据集的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-03-03
  • 常用C#正则表达式汇总介绍

    常用C#正则表达式汇总介绍

    c#正则表达式,用于字符串处理、表单验证等场合,实用高效。现将一些常用的表达式收集于此,以备不时之需。
    2016-01-01
  • 使用GPS经纬度定位附近地点(某一点范围内查询)

    使用GPS经纬度定位附近地点(某一点范围内查询)

    目前的工作是需要手机查找附近N米以内的商户,致想法是已知一个中心点,一个半径,求圆包含于圆抛物线里所有的点,经纬度是一个点,半径是一个距离,不能直接加减,下面提供C#的解决方法
    2013-12-12

最新评论