前言
在平时的开发中,当用户修改数据时,一直没有很好的办法来记录具体修改了那些信息,只能暂时采用将类序列化成 json 字符串,然后全塞入到日志中的方式,此时如果我们想要知道用户具体改变了哪几个字段的值的话就很困难了。因此,趁着这个假期,就来解决这个一直遗留的小问题,本篇文章记录了我目前实现的方法,如果你有不同于文中所列出的方案的话,欢迎指出。
代码仓储地址:https://github.com/Lanesra712/ingos-common/tree/master/sample/csharp/get-data-changed-properties
Step by Step
需求场景
一个经常遇到的使用场景,用户 A 修改了某个表单页面上的数据信息,然后提交到我们的服务端完成数据的更新,对于具有某些权限的用户来说,则是期望可以看到所有用户对于该表单进行操作前后的数据变更。
解决方法
既然想要得知用户操作前后的数据差异,我们肯定需要去对用户操作前后的数据进行比对,这里就落到我们承接数据的类身上。
在我们定义类中的属性时,更多的是使用自动属性的方式来完成属性的 getter、setter 声明,而完整的属性声明方式则需要我们定义一个字段用来承接对于该属性的变更。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Entity1 { public Guid Id { get; set; } }
public class Entity2 { private Guid _id;
public Guid Id { get => _id; set => _id = value; } }
|
因为在给属性进行赋值的时候,需要调用属性的 set 构造器,因此,在 set 构造器内部我们是不是就可以直接对新赋的值进行判断,从而记录下属性的变更过程,改造后的类属性声明代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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 构造器中将新赋的值与原先的值进行判断,当存在两次值不一样时,就写入到变更记录的集合中,从而实现记录数据变更的目的。这里对于变更记录的实体类属性定义如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| public class PropertyChangelog<T> { public PropertyChangelog() { }
public PropertyChangelog(string propertyName, string oldValue, string newValue) { PropertyName = propertyName; OldValue = oldValue; NewValue = newValue; }
public PropertyChangelog(string className, string propertyName, string oldValue, string newValue, DateTime changedTime) : this(propertyName, oldValue, newValue) { ClassName = className; ChangedTime = changedTime; }
public string ClassName { get; set; } = typeof(T).FullName;
public string PropertyName { get; set; }
public string OldValue { get; set; }
public string NewValue { get; set; }
public DateTime ChangedTime { get; set; } = DateTime.Now; }
|
可以看到,在我们对 Sample 类进行初始化赋值时,记录了两次关于类属性的数据变更记录,而当我们进行重新赋值时,只有属性 A 发生了数据改变,因此只记录了属性 A 的数据变更记录。
虽然这里已经达到我们的目的,但是如果采用这种方式的话,相当于原先项目中需要实现数据记录功能的类的属性声明方式全部需要重写,同时,基于 C# 本身已经提供了自动属性的方式来简化属性声明,结果现在我们又回到了传统属性的声明方式,似乎显得有些不太聪明的样子。因此,既然通过一个个属性进行比较的方式过于繁琐,这里我们通过反射的方式直接对比修改前后的两个实体类,批量获取发生数据变更的属性信息。
我们最终想要实现的是用户可以看到关于某个表单的字段属性数据变化的过程,而我们定义在 C# 类中的属性有时候需要与实际页面上显示的字段名称进行映射,以及某些属性其实没有必要记录数据变化的情况,这里我通过添加自定义特性的方式,完善功能的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)] public class PropertyChangeTrackingAttribute : Attribute { public static readonly PropertyChangeTrackingAttribute Default = new PropertyChangeTrackingAttribute();
public PropertyChangeTrackingAttribute() { }
public PropertyChangeTrackingAttribute(bool ignore = false) { IgnoreValue = ignore; }
public PropertyChangeTrackingAttribute(string displayName) : this(false) { DisplayNameValue = displayName; }
public PropertyChangeTrackingAttribute(string displayName, bool ignore) : this(ignore) { DisplayNameValue = displayName; }
public virtual string DisplayName => DisplayNameValue;
public virtual bool Ignore => IgnoreValue;
protected string DisplayNameValue { get; set; }
protected bool IgnoreValue { get; set; } }
|
考虑到我们的类中可能会包含很多的属性信息,如果一个个的给属性添加特性会很麻烦,因此这里可以直接针对类添加该特性。同时,针对我们可能会排除类中的某些属性,或者设定属性在页面中显示的名称,这里我们可以针对特定的类属性进行单独添加特性。
完成了自定义特性之后,考虑到我们后续使用的方便,这里我采用创建扩展方法的形式来声明我们的函数方法,同时我在 PropertyChangelog 类中添加了 DisplayName 属性用来存放属性对应于页面上存放的名称,最终完成后的代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
|
public static IEnumerable<PropertyChangelog<T>> GetPropertyLogs<T>(this T oldObj, T newObj, string propertyName = null) { IList<PropertyChangelog<T>> changelogs = new List<PropertyChangelog<T>>();
IList<PropertyInfo> properties = new List<PropertyInfo>();
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);
if (attributeInfo != null && !attributeInfo.Ignore) properties.Add(i); }
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 属性值不同,但是因为被我们手动忽略了,所以最终只显示我们设定的几个属性的变化信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| [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; }
|
总结
这一章是针对我之前在工作中遇到的一个问题,趁着假期考虑的一个解决方法,虽然只是一个小问题,但是还是挺有借鉴意义的,如果能够给你在日常的开发中提供些许的帮助,不胜荣幸。