C#監控類屬性的更改(大花貓動了哪些小玩具)

来源:http://www.cnblogs.com/longtushen/archive/2017/07/23/7225288.html
-Advertisement-
Play Games

實體類創建後在方法中對哪些屬性賦值了,傳遞到底層方法時在底層如何得知哪些屬性被賦值過。如何監控屬性的更改,請看腦洞大開之《大花貓動了哪些小玩具》——記屬性監控之曲線救國。 ...


  C#監控類屬性的更改(大花貓動了哪些小玩具)

  實體類創建後在方法中對哪些屬性賦值了,傳遞到底層方法時在底層如何得知哪些屬性被賦值過。如何監控屬性的更改,請看腦洞大開之《大花貓動了哪些小玩具》——記屬性監控之曲線救國。

  在使用EF更新資料庫實體時。很多時候我們想要的只是更新表中的某一個或部分欄位。雖然可以通過設置來告訴上下文我們要更新的欄位。但是一般我們都會把數據持久層封裝起來。通過泛型操作。而這時我們就無法得知應用層面修改了哪些欄位了。

  最近也在學習EF,就正好遇到了這個問題。當然,如果直接在應用層面使用,通過設置欄位的IsModified狀態就可以了。如下
  db.Entry(model).Property(x => x.Token).IsModified = false;
  可是,這僅限於學習和demo。正式開發中一般是不會把這種底層操作公開給應用層面的。都會把資料庫持久層進行封裝。然後通過實體工廠(倉庫)加實體泛型的方式提供增刪改查。
  具體的可以參考《基於Entity Framework的Repository模式設計》之類的文章。
  這類方式都有一個共同點,更新和刪除的時候都有如下類似代碼:

    public virtual void Update(TEntity TObject)
        {
            try
            {
                var entry = Context.Entry(TObject);
                Context.Set<TEntity>().Attach(TObject);
                entry.State = EntityState.Modified;
            }
            catch (OptimisticConcurrencyException ex)
            {
                throw ex;
            }
        }

  個人理解:Update(TEntity TObject)通過傳遞一個實體到方法,然後附加到資料庫上下文,並將數據標記為修改狀態。然後進行的更新。
  這種情況會對實體的所有欄位進行更新。那麼我們則需要保證這個實體是從資料庫查出來的,或者與資料庫的記錄是對應的上的。這在C/S結構中是沒有問題的,可問題是在B/S結構中呢?我們不可能把實體所有的欄位都打包,發送到客戶端,然後客戶端修改在返回到服務端,然後在調用倉庫方法更新吧。說個最簡單的,修改用戶密碼,我們只需要一個用戶ID,一個新密碼就可以了。或者鎖定用戶賬號,只需要一個用戶ID,一個鎖定狀態,一個鎖定時間。這樣,我們不可能把整個用戶實體打包傳來傳去吧。有人說可以在保存的時候先根據ID查一遍資料庫,然後再將修改的屬性值附加上去後再更新就可以了。這就回到問題上了:在倉庫方法中只有泛型類型,而你在調用倉庫更新方法時傳遞的是一個實體類型。倉庫並不知道你是那個實體,並且更新了哪些欄位。
當然,通過觸發器我們知道資料庫的更新都是先刪後插,所以更新幾個欄位與全列更新底層操作是沒有多少區別的。

  現在拋開倉庫更新等實體泛型等信息。就單看一下當一個實體發生改變時,我們怎麼能知道他修改了哪些屬性。
  正常情況下一個實體長這樣

 1     /// <summary>
 2     /// 一個具體的實體
 3     /// </summary>
 4     public class AccountEntity : MainEntity
 5     {
 6         /// <summary>
 7         /// 文本類型
 8         /// </summary>
 9         public virtual string Account { get; set; }
10         /// <summary>
11         /// 又一個文本屬性
12         /// </summary>
13         public virtual string Password { get; set; }
14         /// <summary>
15         /// 數字類型
16         /// </summary>
17         public virtual int Sex { get; set; }
18         /// <summary>
19         /// 事件類型
20         /// </summary>
21         public virtual DateTime Birthday { get; set; }
22         /// <summary>
23         /// 雙精度浮點數
24         /// </summary>
25         public virtual double Height { get; set; }
26         /// <summary>
27         /// 十進位數
28         /// </summary>
29         public virtual decimal Monery { get; set; }
30         /// <summary>
31         /// 二進位
32         /// </summary>
33         public virtual byte[] PublicKey { get; set; }
34         /// <summary>
35         /// Guid類型
36         /// </summary>
37         public virtual Guid AreaId { get; set; }
38     }
View Code

  當我們要修改這個實體的屬性時:

var entity = new accountEntity();
entity.Id=1;
entity.Account = "給屬性賦值';

  然後將這個實體傳遞到底層進行操作。

db.Update(entity);

  完全沒有問題,可是我的問題在底層怎麼知道我應用層修改了那幾個屬性呢?再加一個方法,告訴底層,我修改了這幾個屬性。

db.Update(entity,"Account");

  好像也沒有什麼不可哈。

  可是這樣,如果我修改了Account,參數中卻傳遞了Password怎麼辦?所以,應該在實體上就應該有一個集合對整個屬性是否有修改的狀態進行存儲。然後到底層Update方法在取出更新過的欄位進行下一步操作。
  通過這一思路,我想到在實體中加一個字典:

protected Dictionary<string, dynamic> FieldTracking = new Dictionary<string, dynamic>();

  當屬性賦值時,則添加到字典中來。(當然,這種操作是會增加程式的開銷的)

FieldTracking["Account"]="給屬性賦值";

  然後在底層在取出裡面的集合,來區分哪些欄位被修改(大花貓動了哪些小玩具)。

  改造下實體屬性

        public virtual string Account
        {
            get
            { return _Account; }
            set {
                _Account = value;
                FieldTracking["Account"] = value;
            }
        }

  看過編譯後的IL代碼的都知道,class中的屬性最終會編譯成兩個方法 setvalue和getvalue,那麼通過修改set方法添加FieldTracking["Account"] = value;就可以讓屬性在賦值的時候添加到字典中。

  很簡單吧。


  你以為這樣就完了。如果拿房間來比喻實體、拿玩具來比作屬性。我家那大花貓就是修改實體屬性的方法。你知道我家有多少玩具嗎?你每天回家的時候你知道大花貓動了哪個小玩具嗎?給每個玩具裝個GPS?哈哈哈哈,別鬧,花這心思還不如再買點回來。什麼?買回來的還得裝,算了。研究下怎麼裝吧。

  一個程式可能有上百個實體類,修改現有的實體類,給每個set加一行?作為一個程式員是不可能容忍做這樣的操作的。寫一個工具,讀取所有的實體代碼,加上這一行,保存。這是個好辦法。那每次添加一個實體類就得調用工具重寫來一遍,每次修改屬性再調用一遍,恩。沒問題。能用就行。這不是一個真心養貓的人的人能容忍的。

  那怎麼辦?把貓打死?那玩具的存在將會沒有任何意義。想到一個辦法,在我離開房子的時候(程式初始化),給房子里的所有房間(實體類)創建一個同樣的房間(繼承),包含了與原房間所有需要監控(標記為virtual)的玩具的複製,在複製過程中加上GPS(-_~)。然後給貓玩。貓通過我給的門進到這個繼承的房間中玩所有玩具的時候,GPS就能將貓的動作全部記錄下來。我一回家,這貓玩了哪些玩具一看GPS記錄就全知道了。喲,這小崽子,在王元鵝呢。
  

  看不懂,沒關係,上馬:
  1、在程式集初始化的時候,通過反射,查找所有繼承自BaseEntity的實體類。遍歷其中的屬性。找到標記為virtual進行複製。

    剛開始對於如果找到virtual屬性花了不少時間。我總只想著在屬性上找,卻沒想到去set_value方法上去找(其實get_value方法也是)。還是太菜啊。

    註:NoMapAttribute特性是一個自定義的標記,表示不參與映射。因為不參與映射就不需要監控。與本文章代碼沒有太大的關係。僅供參考。

//獲取實體所在的程式集(ClassLibraryDemo)
var assemblyArray = AppDomain.CurrentDomain.GetAssemblies()
        .Where(w => w.GetName().Name == "ClassLibraryDemo")
        .ToList();
//實體的基類
var baseEntityType = typeof(BaseEntity);
//迴圈程式集
foreach (Assembly item in assemblyArray)
{
    //找到這個程式集中繼承自基類的實體
    var types = item.GetTypes().Where(t => t.IsAbstract == false
        && baseEntityType.IsAssignableFrom(t) 
        && t != baseEntityType);
    foreach (Type btItem in types){
        //遍歷這個實體類中的屬性
var properties = btItem.GetProperties(BindingFlags.Public | BindingFlags.Instance)
                        .Where(w => w.CanRead && w.CanWrite
                            && w.GetCustomAttributes(typeof(NoMapAttribute), false).Any() == false
                            //TODO:要不要檢查get方法?
                            && w.GetSetMethod().IsVirtual);
    }
}

  2、根據1的結果,複製一個新的房間(動態代碼生成一個類,這個類繼承1中的實體,並且重寫了屬性的set方法)

  這個過程就設計到動態代碼的生成了。

//首先創建一個與實體類對應的動態類
CodeTypeDeclaration ct = new CodeTypeDeclaration(btItem.Name + "_Dynamic");
//迴圈實體中的所有標記為virtual的屬性
foreach (PropertyInfo fiItem in properties)
{
	//創建一個屬性
	var p = new CodeMemberProperty();
	//設置屬性為公共、重寫
	p.Attributes = MemberAttributes.Public | MemberAttributes.Override;//override
	//設置屬性的類型為繼承的屬性的數據類型
	p.Type = new CodeTypeReference(fiItem.PropertyType);
	//屬性名稱與繼承的一致
	p.Name = fiItem.Name;
	//包含set代碼
	p.HasSet = true;
	//包含get代碼
	p.HasGet = true;
	//設置get代碼
	//return base.Account
	p.GetStatements.Add(new CodeMethodReturnStatement(
                new CodeFieldReferenceExpression(
                        new CodeBaseReferenceExpression(), fiItem.Name)));
	//設置set代碼
	//base.Account=value;
	p.SetStatements.Add(
	new CodeAssignStatement(
                new CodeFieldReferenceExpression(
                        new CodeBaseReferenceExpression(), fiItem.Name),
	new CodePropertySetValueReferenceExpression()));
	//FieldTracking["Account"]=value;
	p.SetStatements.Add(new CodeSnippetExpression("FieldTracking[\"" + fiItem.Name + "\"] = value"));
	//將屬性添加到類中
	ct.Members.Add(p);
}

  3、將剛纔生成的類加到原類所在的命名空間+".Dynamic"(加尾碼以示區分)

//聲明一個命名空間(與當前實體類同名+尾碼)
CodeNamespace ns = new CodeNamespace(btItem.Namespace + ".Dynamic");
ns.Types.Add(ct);

  4、編輯生成代碼所在的程式集

    //要動態生成代碼的程式集
    CodeCompileUnit program = new CodeCompileUnit();
    //添加引用
    program.ReferencedAssemblies.Add("mscorlib.dll");
    program.ReferencedAssemblies.Add("System.dll");
    program.ReferencedAssemblies.Add("System.Core.dll");

    //定義代碼工廠
    CSharpCodeProvider provider = new CSharpCodeProvider();
    //編譯程式集
    var cr = provider.CompileAssemblyFromDom(new System.CodeDom.Compiler.CompilerParameters();
    //看編譯是否通過
    var error = cr.Errors;
    if (error.HasErrors)
    {
        Console.WriteLine("錯誤列表:");
        //編譯不通過
        foreach (dynamic item in error)
        {
            Console.WriteLine("ErrorNumber:{0};Line:{1};ErrorText{2}",
                item.ErrorNumber,
                item.Line, 
                item.ErrorText);
        }
        return;
    }
    else
    {
        Console.WriteLine("編譯成功。");
    }

 

   查看生成的代碼

//查看生成的代碼
var codeText = new StringBuilder();
using (var codeWriter = new StringWriter(codeText))
{
    CodeDomProvider.CreateProvider("CSharp").GenerateCodeFromNamespace(ns,
        codeWriter,
        new CodeGeneratorOptions()
        {
            BlankLinesBetweenMembers = true
        });
}
Console.WriteLine(codeText);

 

  5、將複製的新類與原類建立映射關係。

foreach (Type item in ts)
{
    //註冊(模擬實現,通過字典實現的,也可以通過IOC註入方式處理)
    Mapping.Map(item.BaseType, item);
}

  6、獲得這個複製的實體對象

//創建一個指定的實體對象
AccountEntity ae = Mapping.GetMap<AccountEntity>();

  7、對這個實體對象的屬性進行賦值

//主鍵賦值不會修改屬性更新
ae.BaseEntity_Id = 1;//不會變(未標記為virtual)
ae.MainEntity_Name = "大花貓";
ae.MainEntity_UpdateTime = DateTime.Now;
//修改某個屬性
ae.Account = "admin";
ae.Account = "以最後一次的修改為準";

  8、調用底層方法,底層根據這個實體屬性獲得被修改的屬性名稱

//調用基類中的方法 獲取變動的屬性
var up = ae.GetFieldTracking();
Console.WriteLine("有修改的欄位:");
up.ForEach(fe =>
{
    Console.WriteLine(fe + ":" + ae[fe]);
});

  9、完美

  

 

  就這樣,在底層就能知道哪些實體被賦值過了。

  當然,有些實體我們只是需要用來計算,則可以調用方法將賦值過的屬性進行刪除

//刪除變更欄位
ae.RemoveChanges("Account");

 

  這隻是一個簡單的實現,還有一種比較複雜的情況,在第6步,獲得這個複製的實體對象時,怎麼用一個現有的new出來的實體對象去創建建並監控呢。就像,別人送我一房間現成的玩具,給我的時候貓就在裡面玩了。嗷,把貓打死吧。

 

  總結:

再次認識到反射的強大。
也第一次實現了代碼生成代碼並使用的經歷。
對欄位和屬性的區別有了更深的認識。
對訪問修飾符和虛virtual方法有了更好的認識。

 

  本文僅供參考,如果你能通過閱讀本文解決你的問題或能學到點什麼那就更好了。

 

   源代碼被貓吃了,被貓吃了……


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、redis配置 1、啟動redis 這裡使用的是windows版本的redis,直接解壓使用就可以了。 註意1: 當運行redis-server時需要註意的是不同的電腦因為配置問題可能無法雙擊啟動。所以需要通過命令提示符的方式進行啟動 這個界面表示redis已經啟動,預設配置 redis.hos ...
  • 本位出處:http://www.cnblogs.com/wy123/p/7211742.html (保留出處並非什麼原創作品權利,本人拙作還遠遠達不到,僅僅是為了鏈接到原文,因為後續對可能存在的一些錯誤進行修正或補充,無他) MySQL中的InnoDB引擎表索引類型有一下幾種(以下所說的索引,沒有特 ...
  • 基本知識方便操作 //創建資料庫 create database databasename; //進入資料庫 use databasename; //顯示表名(可以通過這個語句查看表數量,從而判斷是否導入錯誤) show tables; //在進入資料庫之前顯示所有表 show tables fro ...
  • LVS服務原理以及搭建(理論+乾貨) 版權聲明:本文為yunshuxueyuan原創文章 如需轉載請標明出處: https://my.oschina.net/yunshuxueyuan/blog QQ技術交流群:299142667 一、 LVS簡介 LVS是Linux Virtual Server的 ...
  • HADOOP背景介紹 1.1Hadoop產生背景 ——分散式文件系統(GFS),可用於處理海量網頁的存儲 ——分散式計算框架MAPREDUCE,可用於處理海量網頁的索引計算問題。 1.2 什麼是HADOOP 1.3 HADOOP在大數據、雲計算中的位置和關係 1.4Hadoop生態系統 HDFS:分 ...
  • 字元串參數:一定要將單引號替換成2個單引號,這點非常重要 正常方式:SELECT * FROM 客戶信息 WHERE 客戶編號='001' 註入方式:SELECT * FROM客戶信息WHERE客戶編號='001'; UPDATE 客戶信息 SET 客戶編號 = NULL--' 結果:你的客戶信息將 ...
  • NoSql:全名【not only sql 】是一種非關係型資料庫 High performance 高併發讀寫Huge storage 海量數據的高效存儲與訪問產品:mongodb redis hbase 等 。 各個產品存儲類型描述: 鍵值對來存儲資料庫 【Redis】 優勢:快速查詢。 缺點: ...
  • 剛開始學習mongodb,對筆記做了一個整理。是基於nodejs來學習的。 1.mongodb介紹 mongodb 是C++語言編寫的,是一個基於分散式文件存儲的開源資料庫系統。 在高負載的情況下,添加更多的節點,可以保證伺服器性能。 mongodb 旨在為WEB應用提供可擴展的高性能數據存儲解決方 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...