詳解如何獲取C#類中發(fā)生數(shù)據(jù)變化的屬性信息
一、前言#
在平時(shí)的開(kāi)發(fā)中,當(dāng)用戶修改數(shù)據(jù)時(shí),一直沒(méi)有很好的辦法來(lái)記錄具體修改了那些信息,只能暫時(shí)采用將類序列化成 json 字符串,然后全塞入到日志中的方式,此時(shí)如果我們想要知道用戶具體改變了哪幾個(gè)字段的值的話就很困難了。因此,趁著這個(gè)假期,就來(lái)解決這個(gè)一直遺留的小問(wèn)題,本篇文章記錄了我目前實(shí)現(xiàn)的方法,如果你有不同于文中所列出的方案的話,歡迎指出。
代碼倉(cāng)儲(chǔ)地址:https://github.com/Lanesra712/ingos-common/tree/master/sample/csharp/get-data-changed-properties
二、Step by Step#
1、需求場(chǎng)景#
一個(gè)經(jīng)常遇到的使用場(chǎng)景,用戶 A 修改了某個(gè)表單頁(yè)面上的數(shù)據(jù)信息,然后提交到我們的服務(wù)端完成數(shù)據(jù)的更新,對(duì)于具有某些權(quán)限的用戶來(lái)說(shuō),則是期望可以看到所有用戶對(duì)于該表單進(jìn)行操作前后的數(shù)據(jù)變更。
2、解決方法#
既然想要得知用戶操作前后的數(shù)據(jù)差異,我們肯定需要去對(duì)用戶操作前后的數(shù)據(jù)進(jìn)行比對(duì),這里就落到我們承接數(shù)據(jù)的類身上。
在我們定義類中的屬性時(shí),更多的是使用自動(dòng)屬性的方式來(lái)完成屬性的 getter、setter 聲明,而完整的屬性聲明方式則需要我們定義一個(gè)字段用來(lái)承接對(duì)于該屬性的變更。
// 自動(dòng)屬性聲明
public class Entity1
{
public Guid Id { get; set; }
}
// 完整的屬性聲明
public class Entity2
{
private Guid _id;
public Guid Id
{
get => _id;
set => _id = value;
}
}
因?yàn)樵诮o屬性進(jìn)行賦值的時(shí)候,需要調(diào)用屬性的 set 構(gòu)造器,因此,在 set 構(gòu)造器內(nèi)部我們是不是就可以直接對(duì)新賦的值進(jìn)行判斷,從而記錄下屬性的變更過(guò)程,改造后的類屬性聲明代碼如下。
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;
}
在改造后的類屬性聲明中,我們?cè)趯傩缘?set 構(gòu)造器中將新賦的值與原先的值進(jìn)行判斷,當(dāng)存在兩次值不一樣時(shí),就寫入到變更記錄的集合中,從而實(shí)現(xiàn)記錄數(shù)據(jù)變更的目的。這里對(duì)于變更記錄的實(shí)體類屬性定義如下所示。
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">修改時(shí)間</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>
/// 修改時(shí)間
/// </summary>
public DateTime ChangedTime { get; set; } = DateTime.Now;
}

可以看到,在我們對(duì) Sample 類進(jìn)行初始化賦值時(shí),記錄了兩次關(guān)于類屬性的數(shù)據(jù)變更記錄,而當(dāng)我們進(jìn)行重新賦值時(shí),只有屬性 A 發(fā)生了數(shù)據(jù)改變,因此只記錄了屬性 A 的數(shù)據(jù)變更記錄。
雖然這里已經(jīng)達(dá)到我們的目的,但是如果采用這種方式的話,相當(dāng)于原先項(xiàng)目中需要實(shí)現(xiàn)數(shù)據(jù)記錄功能的類的屬性聲明方式全部需要重寫,同時(shí),基于 C# 本身已經(jīng)提供了自動(dòng)屬性的方式來(lái)簡(jiǎn)化屬性聲明,結(jié)果現(xiàn)在我們又回到了傳統(tǒng)屬性的聲明方式,似乎顯得有些不太聰明的樣子。因此,既然通過(guò)一個(gè)個(gè)屬性進(jìn)行比較的方式過(guò)于繁瑣,這里我們通過(guò)反射的方式直接對(duì)比修改前后的兩個(gè)實(shí)體類,批量獲取發(fā)生數(shù)據(jù)變更的屬性信息。
我們最終想要實(shí)現(xiàn)的是用戶可以看到關(guān)于某個(gè)表單的字段屬性數(shù)據(jù)變化的過(guò)程,而我們定義在 C# 類中的屬性有時(shí)候需要與實(shí)際頁(yè)面上顯示的字段名稱進(jìn)行映射,以及某些屬性其實(shí)沒(méi)有必要記錄數(shù)據(jù)變化的情況,這里我通過(guò)添加自定義特性的方式,完善功能的實(shí)現(xiàn)。
/// <summary>
/// 為指定的屬性設(shè)定數(shù)據(jù)變更記錄
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
public class PropertyChangeTrackingAttribute : Attribute
{
/// <summary>
/// 指定 PropertyChangeTrackingAttribute 屬性的默認(rèn)值
/// </summary>
public static readonly PropertyChangeTrackingAttribute Default = new PropertyChangeTrackingAttribute();
/// <summary>
/// 構(gòu)造一個(gè)新的 PropertyChangeTrackingAttribute 特性實(shí)例
/// </summary>
public PropertyChangeTrackingAttribute()
{ }
/// <summary>
/// 構(gòu)造一個(gè)新的 PropertyChangeTrackingAttribute 特性實(shí)例
/// </summary>
/// <param name="ignore">是否忽略該字段的數(shù)據(jù)變化</param>
public PropertyChangeTrackingAttribute(bool ignore = false)
{
IgnoreValue = ignore;
}
/// <summary>
/// 構(gòu)造一個(gè)新的 PropertyChangeTrackingAttribute 特性實(shí)例
/// </summary>
/// <param name="displayName">屬性對(duì)應(yīng)頁(yè)面顯示名稱</param>
public PropertyChangeTrackingAttribute(string displayName)
: this(false)
{
DisplayNameValue = displayName;
}
/// <summary>
/// 構(gòu)造一個(gè)新的 PropertyChangeTrackingAttribute 特性實(shí)例
/// </summary>
/// <param name="displayName">屬性對(duì)應(yīng)頁(yè)面顯示名稱</param>
/// <param name="ignore">是否忽略該字段的數(shù)據(jù)變化</param>
public PropertyChangeTrackingAttribute(string displayName, bool ignore)
: this(ignore)
{
DisplayNameValue = displayName;
}
/// <summary>
/// 獲取特性中的屬性對(duì)應(yīng)頁(yè)面上顯示名稱參數(shù)信息
/// </summary>
public virtual string DisplayName => DisplayNameValue;
/// <summary>
/// 獲取特性中的是否忽略該字段的數(shù)據(jù)變化參數(shù)信息
/// </summary>
public virtual bool Ignore => IgnoreValue;
/// <summary>
/// 修改屬性對(duì)應(yīng)頁(yè)面顯示名稱參數(shù)值
/// </summary>
protected string DisplayNameValue { get; set; }
/// <summary>
/// 修改是否忽略該字段的數(shù)據(jù)變化
/// </summary>
protected bool IgnoreValue { get; set; }
}
考慮到我們的類中可能會(huì)包含很多的屬性信息,如果一個(gè)個(gè)的給屬性添加特性會(huì)很麻煩,因此這里可以直接針對(duì)類添加該特性。同時(shí),針對(duì)我們可能會(huì)排除類中的某些屬性,或者設(shè)定屬性在頁(yè)面中顯示的名稱,這里我們可以針對(duì)特定的類屬性進(jìn)行單獨(dú)添加特性。
完成了自定義特性之后,考慮到我們后續(xù)使用的方便,這里我采用創(chuàng)建擴(kuò)展方法的形式來(lái)聲明我們的函數(shù)方法,同時(shí)我在 PropertyChangelog 類中添加了 DisplayName 屬性用來(lái)存放屬性對(duì)應(yīng)于頁(yè)面上存放的名稱,最終完成后的代碼如下所示。
/// <summary>
/// 獲取類屬性數(shù)據(jù)變化記錄
/// </summary>
/// <typeparam name="T">監(jiān)聽(tīng)的類類型</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、獲取需要添加數(shù)據(jù)變更記錄的屬性信息
//
IList<PropertyInfo> properties = new List<PropertyInfo>();
// PropertyChangeTracking 特性的類型
var attributeType = typeof(PropertyChangeTrackingAttribute);
// 對(duì)應(yīng)的類中包含的屬性信息
var classProperties = typeof(T).GetProperties();
// 獲取類中需要添加變更記錄的屬性信息
//
bool flag = Attribute.IsDefined(typeof(T), attributeType);
foreach (var i in classProperties)
{
// 獲取當(dāng)前屬性添加的特性信息
var attributeInfo = (PropertyChangeTrackingAttribute)i.GetCustomAttribute(attributeType);
// 類未添加特性,并且該屬性也未添加特性
if (!flag && attributeInfo == null)
continue;
// 類添加特性,該屬性未添加特性
if (flag && attributeInfo == null)
properties.Add(i);
// 不管類有沒(méi)有添加特性,只要類中的屬性添加特性,并且 Ignore 為 false
if (attributeInfo != null && !attributeInfo.Ignore)
properties.Add(i);
}
// 2、判斷指定的屬性數(shù)據(jù)是否發(fā)生變更
//
foreach (var property in properties)
{
var oldValue = property.GetValue(oldObj) ?? "";
var newValue = property.GetValue(newObj) ?? "";
if (oldValue.Equals(newValue))
continue;
// 獲取當(dāng)前屬性在頁(yè)面上顯示的名稱
//
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));
}
在下面的這個(gè)測(cè)試案例中,Entity 類實(shí)際上只會(huì)記錄 5 個(gè)屬性的數(shù)據(jù)變化,我們手動(dòng)創(chuàng)建兩個(gè) Entity 類實(shí)例,同時(shí)改變兩個(gè)類實(shí)例對(duì)應(yīng)的屬性值。從我們運(yùn)行的示意圖中可以看到,雖然兩個(gè)類實(shí)例的 Id 屬性值不同,但是因?yàn)楸晃覀兪謩?dòng)忽略了,所以最終只顯示我們?cè)O(shè)定的幾個(gè)屬性的變化信息。
[PropertyChangeTracking]
public class Entity
{
[PropertyChangeTracking(ignore: true)]
public Guid Id { get; set; }
[PropertyChangeTracking(displayName: "序號(hào)")]
public string OId { get; set; }
[PropertyChangeTracking(displayName: "第一個(gè)字段")]
public string A { get; set; }
public double B { get; set; }
public bool C { get; set; }
public DateTime Date { get; set; } = DateTime.Now;
}

三、總結(jié)#
這一章是針對(duì)我之前在工作中遇到的一個(gè)問(wèn)題,趁著假期考慮的一個(gè)解決方法,雖然只是一個(gè)小問(wèn)題,但是還是挺有借鑒意義的,如果能夠給你在日常的開(kāi)發(fā)中提供些許的幫助,不勝榮幸。
作者:墨墨墨墨小宇
出處:https://www.cnblogs.com/danvic712/p/how-to-get-the-data-changed-properties-in-csharp-class.html
到此這篇關(guān)于詳解如何獲取C#類中發(fā)生數(shù)據(jù)變化的屬性信息的文章就介紹到這了,更多相關(guān)C#獲取類屬性信息內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- c#連接sqlserver數(shù)據(jù)庫(kù)、插入數(shù)據(jù)、從數(shù)據(jù)庫(kù)獲取時(shí)間示例
- C#從DataTable獲取數(shù)據(jù)的方法
- c# 獲取數(shù)據(jù)庫(kù)中所有表名稱的方法
- C#操作DataTable方法實(shí)現(xiàn)過(guò)濾、取前N條數(shù)據(jù)及獲取指定列數(shù)據(jù)列表的方法
- C#使用SqlDataAdapter對(duì)象獲取數(shù)據(jù)的方法
- C#實(shí)現(xiàn)解析百度天氣數(shù)據(jù),Rss解析百度新聞以及根據(jù)IP獲取所在城市的方法
- C#獲取所有SQL Server數(shù)據(jù)庫(kù)名稱的方法
- 在C#中如何使用正式表達(dá)式獲取匹配所需數(shù)據(jù)
- C#中獲取數(shù)據(jù)的方法實(shí)例
相關(guān)文章
C#使用channel實(shí)現(xiàn)Plc異步任務(wù)之間的通信
在C#的并發(fā)編程中,Channel是一種非常強(qiáng)大的數(shù)據(jù)結(jié)構(gòu),用于在生產(chǎn)者和消費(fèi)者之間進(jìn)行通信,本文將給大家介紹C#使用channel實(shí)現(xiàn)Plc異步任務(wù)之間的通信,文中有相關(guān)的代碼示例供大家參考,感興趣的朋友跟著小編一起來(lái)看看吧2024-05-05
C#引用類型轉(zhuǎn)換的常見(jiàn)方式總結(jié)
這篇文章主要介紹了C#引用類型轉(zhuǎn)換的常見(jiàn)方式,包括子類轉(zhuǎn)換成父類,父類轉(zhuǎn)換成子類,以及不是子父級(jí)關(guān)系類之間的轉(zhuǎn)換,需要的朋友可以參考下2014-09-09
C#使用TensorFlow.NET訓(xùn)練自己的數(shù)據(jù)集的方法
這篇文章主要介紹了C#使用TensorFlow.NET訓(xùn)練自己的數(shù)據(jù)集的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
使用GPS經(jīng)緯度定位附近地點(diǎn)(某一點(diǎn)范圍內(nèi)查詢)
目前的工作是需要手機(jī)查找附近N米以內(nèi)的商戶,致想法是已知一個(gè)中心點(diǎn),一個(gè)半徑,求圓包含于圓拋物線里所有的點(diǎn),經(jīng)緯度是一個(gè)點(diǎn),半徑是一個(gè)距離,不能直接加減,下面提供C#的解決方法2013-12-12

