如何获取 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;
}

三、总结
这一章是针对我之前在工作中遇到的一个问题,趁着假期考虑的一个解决方法,虽然只是一个小问题,但是还是挺有借鉴意义的,如果能够给你在日常的开发中提供些许的帮助,不胜荣幸。
如何获取 C# 类中发生数据变化的属性信息的更多相关文章
- 【记录】mybatis中获取常量类中数据
部分转载,已注明来源: 1.mybatis中获取常量类中数据 <update id="refuseDebt"> UPDATE dt_debt a SET ...
- 项目中通过Sorlj获取索引库中的数据
在开发项目中通过使用Solr所提供的Solrj(java客户端)获取索引库中的数据,这才是真正对项目起实质性作用的功能,提升平台的检索性能及检索结果的精确性 第一步,引入相关依赖的jar包 第二步,根 ...
- cxf,两个声明导致 ObjectFactory 类中发生冲突
说明先,这里不管是client还是server端都是用java语言编写,如有写得不好,望原谅! 问题 http://localhost:8080/WEB-SMVC/cxf/userService?ws ...
- 获取class对象的三种方法以及通过Class对象获取某个类中变量,方法,访问成员
public class ReflexAndClass { public static void main(String[] args) throws Exception { /** * 获取Clas ...
- .NET获取Html字符串中指定标签的指定属性的值
using System.Text; using System.Text.RegularExpressions; //以上为要用到的命名空间 /// <summary> /// 获取Htm ...
- [爬虫]通过url获取连接地址中的数据
1. 要想获取指定连接的数据,那么就得使用HtmlDocument对象,要想使用HtmlDocument对象就必需引用using HtmlAgilityPack; 2. 详细步骤如下: 步骤一 ...
- C#:实体类中做数据验证
主要是在实体类中验证 using System; namespace Jone.Function.attribute{ /// <summary> /// 附加 ...
- asp.net网页上获取其中表格中的数据(爬数据)
下面的方法获取页面中表格数据,每个页面不相同,获取的方式(主要是正则表达式)不一样,只是提供方法参考.大神勿喷,刚使用了,就记下来了. 其中数据怎么存,主要就看着怎么使用了.只是方便记录就都放在lis ...
- 使用property为类中的数据添加行为
对于面向对象编程特别重要的是,关注行为和数据的分离. 在这之前,先来讨论一些“坏”的面向对象理论,这些都告诉我们绝不要直接访问属性(如Java): class Color: def __init__( ...
随机推荐
- CPP STL学习笔记
STL的概念 源地址 https://www.ev0l.art/index.php/archives/15/ <li> Iterator (迭代器)<li> Containe ...
- 【C++】将调用第三方库的代码封装成动态库供上层调用
需求分析 Java应用中需要调用C++的程序,而这个C++的程序中需要引入一个第三方静态库.所以需要将该程序编译成一个动态库文件(.so)供Java调用. 步骤 使用CLion创建一个动态库的项目,会 ...
- Windows服务器管理--批量管理工具
iis7批量远程控制: 一款电脑远程监控的工具,IIS7远程桌面管理是一款专业的远程桌面连接软件,无需安装.操作简单方便.完美的界面设计.强大的监控功能.稳定的系统平台,满足了用户实现远程桌面连接的需 ...
- VS Code 解决 因为在此系统上禁止运行脚本
vscode执行命令的 主要是由于没有权限执行脚本.开通权限就可以解决啦 在搜索框中输入:powerShell 选择管理员身份运行 输入命令行:set-ExecutionPolicy RemoteSi ...
- 大数据学习之路-Centos6安装python3.5
Centos 6.8安装python3.5.2 因为学习所需,需要用到python3.x的环境,目前Linux系统默认的版本都是python2.x的,还有一些自带的工具需要用到python2.6版本, ...
- Docker+Nginx使用流程(笔记)
Docker+Nginx使用流程 本教程依据个人理解并经过实际验证为正确,特此记录下来,权当笔记. 注:基于linux操作系统 # uname -r 查看你当前的内核版本 # yum -y insta ...
- 利用SSH桥接访问服务器的Docker运行的MySql服务
前情提要 docker的运用越来广泛,许多IT公司都已经将自己的服务跑在Docker上面.在安全性方面又做了一层防护.比如:跑在Docker上的Mysql不做外网端口映射,只能在linux服务器上或进 ...
- bash的默认组合键
组合键 组合按键 执行结果 Ctrl+C 终止目前的命令 Ctrl+D 输入结束(EOF),例如邮件结束的时候 Ctrl+M 就是Enter啦! Ctrl+S 暂停屏幕输出 Ctrl+Q 恢复屏幕输出 ...
- Collections 工具类
针对 List 集合的方法 排序 sort 如果集合元素为基本数据类型,采用快排:对于集合元素为引用类型,采用归并排序. //对指定 List 集合的元素按照自然排序 void sort(List&l ...
- Java面向对象之异常详解
目录 Java面向对象之异常[一] Java面向对象之异常[二] 捕获异常的规则 访问异常信息 异常对方法重写的影响 finally详解 Java面向对象之异常[一] Java面向对象之异常[二] 往 ...