详解如何获取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#获取类属性信息内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C#关于类的只读只写属性实例分析

    C#中属性的目的是对字段的封装,是为了程序数据的安全性考虑的.本文即以实例形式对C#中只读只写属性进行剖析. 对于只读或只写的属性定义: 1.不写入其中一个get\set方法即可只读或只写 比如: private int a; public int A{ get { return a; } } 2.用private进行保护,类外同样意味着只读或只写 比如: private int a; public int A{ private get { return a; } set { a = value

  • C#类中的属性使用总结(详解类的属性)

    复制代码 代码如下: private int dd;  public int dd  {      get{ return xx*3;}      set{ xx = value/3;}  } 没有set的属性是一种只读属性,没有get的访问器是一种只写属性.(1) get访问器用来返回字段或者计算 并返回字段,它必须以return或者throw终结. 复制代码 代码如下: private string name;  public string Name  {      get      { 

  • C#使用Directoryinfo类获得目录信息和属性的方法

    本文实例讲述了C#使用Directoryinfo类获得目录信息和属性的方法.分享给大家供大家参考.具体如下: using System; using System.IO; class MainClass { static void Main(string[] args) { FileInfo file = new FileInfo("c:\\a.txt"); // Display directory information. DirectoryInfo dir = file.Direc

  • C#反射(Reflection)对类的属性get或set值实现思路

    近段时间,有朋友叫Insus了解一下反射(Reflection)方面的知识,反射提供了封装程序集.模块和类型的对象(Type类型).可以使用反射动态创建类型的实例,将类型绑定到现有对象,或从现有对象获取类型并调用其方法或访问其字段和属性.如果代码中使用了属性,可以利用反射对它们进行访问. 下面的例子,是Insus练习对一个类别的属性进行set和get值. 首先写一个类,再写一个可读写的属性: 复制代码 代码如下: using System; using System.Collections.Ge

  • C#两个相同属性的类赋值方法

    最近有遇到两个类之间的赋值问题,两个类的属性几乎都一样的,所以写了个通过反射获取属性的然后赋值的方法,把一个类的属性的值赋值给另一个类. 框架是.net 4.5 public static D Mapper<D, S>(S s) { D d = Activator.CreateInstance<D>(); try { var sType = s.GetType(); var dType = typeof(D); foreach (PropertyInfo sP in sType.G

  • C# 抽象类,抽象属性,抽象方法(实例讲解)

    抽象类往往用来表征对问题领域进行分析.设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象. 下面我们以水果为例,首先定义抽象类Fruit,抽象类中有公共属性vendor,抽象属性Price和抽象方法GrowInArea, public abstract class Fruit { public string vendor { get; set; } //默认为private public abstract float Price { get; } //抽象属性必须是公有的

  • C#类中属性与成员变量的使用小结

    属性实际上和成员变量没什么区别,属性代表类的某种特征, 让人更好理解而已. 使用中注意问题:1.属性名和变量名不能相同, 2.一般变量都是private,属性都是public的,属性用于给类外调用,变量限于类内使用,感觉封装性体现得要好些 3.属性必须和一个变量相联系,而这个变量必须要在类中定义.如果不定义,用成如下方法: 复制代码 代码如下: public int b //定义一个属性b  {      get   {    return b;   }   set   {    b = val

  • C#编程获取实体类属性名和值的方法示例

    本文实例讲述了C#编程获取实体类属性名和值的方法.分享给大家供大家参考,具体如下: 遍历获得一个实体类的所有属性名,以及该类的所有属性的值 //先定义一个类: public class User { public string name { get; set; } public string gender { get; set; } public string age { get; set; } } //实例化类,并给实列化对像的属性赋值: User u = new User(); u.name

  • C#反射技术的简单操作(读取和设置类的属性)

    要想对一个类型实例的属性或字段进行动态赋值或取值,首先得得到这个实例或类型的Type,微软已经为我们提供了足够多的方法. 首先建立一个测试的类 复制代码 代码如下: public class MyClass { public int one { set; get; } public int two { set; get; } public int five { set; get; } public int three { set; get; } public int four { set; ge

  • C#读取静态类常量属性和值的实例讲解

    1.背景 最近项目中有一个需求需要从用户输入的值找到该值随对应的名字,由于其它模块已经定义了一份名字到值的一组常量,所以想借用该定义. 2.实现 实现的思路是采用C#支持的反射. 首先,给出静态类中的常量属性定义示例如下. public static class FruitCode { public const int Apple = 0x00080020; public const int Banana = 0x00080021; public const int Orange = 0x000

随机推荐