.NET 使用 Link CS檔案的型別比較的捉鬼記
圖片取自:https://home.gamer.com.tw/creationDetail.php?sn=2461568
前言
最近擔任某產業的 Redis 導入的執行顧問,我的任務就是,協助將 Redis 導入在既有的系統裡,在這個顧問案裡面,我遇到的挑戰是:
- 現有系統使用某產品進行開發(好處是個系統共用此平台 Platform 提供的服務/壞處是某處出現問題就會全部一起死)
- 現有系統使用 .NET 4.0 的 Object Cache、所以要全部上 Redis,但是 Object Cache 是撰寫在該產品平台 Platform 內 Kernel 所提供的功能之一,我得協助修改核心 Kernel 的 Cache Provider
- 另外是,系統為 ASP.NET WebForm ,所以許多存放在 Cache 內的資料均為 DataSet(基於一些原因我無法全改為不使用 DataSet),但因為我們知道 DataSet 並無法直接序列化轉為 Redis 可接受的格式,所以我得做些手腳以協助將這些資料轉換為 Redis 可以接受的格式
- 有上千隻程式運行在上面、必須一氣呵成!
- 包含伺服器主機架構規劃 [建置 HA 高可用性架構](必須購置新主機),所以也包含硬體規格建議等等
開始
這個案子在開始之前,最麻煩之處、也比較有代表性工作在於:
- Code-Review 包括現有平台 Platform & 子系統
- 改寫現有平台原始程式碼 & 包括最主要的 Cache Provider
- 以隔離測試(Isolation Unit Test)方法來進行改寫
進行 Code-Review 的部分比須帶著團隊來進行,當然包括 IntelliSys 平台的維護者,因為我需要了解現有的架構規劃、現有的 Cache 的使用情況、與現有 Cache 策略使用規劃等等、接著就是評估修改的幅度 與 範圍,因為程式修改的部分還是得以專案形式來進行,我還是會估一個類似 WBS 的工時並提供給客戶。
實際進行時,我大約花了 1 天的時間,對!幾乎一整天的時間,看了整個 IntelliSys 幾近所有的程式碼,因為為了較精準的估花費時間,也是因為客戶要求。第二天便開始架設 Redis 環境,第三天開始修改程式碼。
以 Unit Test 方式改寫 Cache Provider
此次替此客戶導入 Redis 我用了一些新的手法,當然,這也不是多新的梗、或者多新的技術,只是我利用單元測試 Unit Test 中的隔離測試 Isolation Unit Test 的一些手法將既有的 Cache Provider 隔離開來,因為目前的 code 都早已經在 Production 環境中執行了,所以必須在現有系統中移轉。
但是為了能夠一步到位,我還是要了核心需要修改的程式碼,但因為外部參考以及呼叫它的外部 Business & Services 當然是不方便給我,但其實這沒有關係,因為全部對 Cache Provider 存取的接口才是我需要考慮的,所以所有會存取 Cache Provider 的我全部的需求的角度來細切為撰寫 Unit Test 的最小粒度,所有對 Cache Provider 的外部 Class 我全部撰寫成 Fake 假物件來支撐整個單元測試。
沒錯,也就是說,我其實只拿到 CacheHelper.cs & BaseService.cs & CodeCache.cs 這三個 C# 檔案,其他的專案都是我參照現有系統架構手刻 & 撰寫起來的 Fake 假物件,大部分都是針對介面回呼 & 滿足傳回假的空殼子而已,而這全部針對與為了滿足 Redis Cache 而設計的專案結構。
圖(一)、只有紅框部分為客戶端 Cache Provider 的程式碼
所以 Unit Test 真的非常好用,以 Unit Test 為進入點的軟體架構設計也更能夠將需求單元化 & 細粒化、更容易測試的情況下、容易控制耦合性,程式碼比較容易修改,經過分析後,整個要撐起來的 CacheHelper.cs 根本也不需要知道外部環境幾乎都是假的,而我的單元測試通過之後,CacheHelper.cs 直接 Copy 到它實際的運作環境就大功告成了!
從圖(一)中可以看出來,除了 CacheHelper.cs & BaseService.cs & CodeCache.cs 這三個 C# 檔案外,其他的『專案』、『程式檔案』都是我【造】【假】出來的! XD
圖(二)、開始撰寫單元測試
如上圖(二)、我只需要關注所有的單元測試是否成功就大功告成了,因為我基本上建造一個供 Redis Cache Provider 所需要運行的基本環境,所以完成後我只需要複製到客戶實際的環境中即可!
以上動作均在客戶的測試環境中進行。
將 DataSet 放入 Redis Cache 中
這又是另一個挑戰(但別問我為什麼要將 DataSet 置入 Redis 中),因為客戶使用 ASP.NET WebForm,在那個年代 DataSet 仍然是 .NET 離線小DB的最佳實踐,該產品在 AP 端交換的資料也都是 DataSet。
由於這也是第一次對 ASP.NET WebForm 進行 Redis 的導入,且熟悉 Redis 的朋友應該也都清楚 Redis Cache 存放的主要的內容就是字串 String,而 DataSet 雖然可以序列化為 XML,但是不是你直接標記 [Serialize] 標籤加以序列化即可,即便你將 XML 放入 Redis 中,你從 Redis 裡 Get 出來的時候型態就不是 DataSet 了,更不用說客戶還有 Typed DataSet…
那怎麼辦呢?我的解決方式是,建立一個新的型別,這個型別要能夠吃進 Typed DataSet 的 Assembly 名稱、NameSpace、Class Name 等相關資訊,當然還包括 XML Schema 資訊,所以你必須將 DataSet/Typed DataSet 拆解為這個自訂型態後再存入 Redis 中,其實存進去比較沒什麼難度,比較困難的是,取出來的時候,還得要轉換為對應型別的 Typed DataSet(這裡原生地 DataSet 反而沒什麼問題),還得透過 Assembly 的 GetType("[Namespace].[ClassName]") 來載入型別,最後還原整個 DataSet 的內容。
平行測試的插曲
這個顧問案前後我大約進行了 9 天就差不多完成了整個 (Code-Review/程式撰寫/環境安裝)等等,平行運作大致上也都沒什麼問題,雖然是 DataSet ,但是整體效能並沒有低於先前使用 .NET ObjectCache 的效能。
但是時間過了兩個禮拜,卻發現異常的情況,但是這個異常並不是隨時會發生,在某個功能會發生、某些功能又不會發生,甚至我連 Dump 檔案也建立了,透過 IntelliTrace 去看卻也看不到錯誤的堆疊,案情一整個陷入膠著狀態。
後來實在無法可想,只好利用 VS 遠端偵錯功能直接偵錯測試主機,卻讓我發現了一個驚呆了的情況:
圖(三)、表面上型別完全相同、但卻不相等的情況
如上圖(三)明明完整的 Namespace 與 Class Name 完全相同,但是程式的執行結果卻判定兩個是不同的,細看才發現兩個位於不同的組件 Assembly 中。
我突然間瞬間恍然大悟,馬上了解情況並立馬修正程式馬,馬上就修復了這個問題。
為什麼會不相等的原因呢?看我娓娓道來。
圖(四)、typeof(DataSetInRedis) 位於的組件位置
如上圖,目前的 Execute Assemblies 裡面直接 typeof(DataSetInRedis) 取的型別是位於 IntelliSys.Services 裡。
圖(五)、outputResultType 位於的組件位置
不相等的最主要原因是什麼呢?因為從 runtime 傳入到該方法中的 outputResultType 居然位於 IntelliSys.Services 中!但為什麼這兩個型別會位於不同的 Assembly 中呢?原因是因為,在 IntelliSys 的平台內使用了 Visual Studio 的 Link 程式碼的功能,這個功能是這樣的,它可以其他專案裡以 (連結) Link 的方式加入其他專案裡的程式碼檔案 (*.cs/;*.vb) 等,但對於整個方案而言,編輯的都還是同一個程式碼檔案,也就是 Shared Code 的概念,但每個專案卻以為自己獨佔這個程式碼檔案,但事實上卻是在編輯同一個檔案,且 Compile 後也是各自 Compile 到自己的 Assembly 的 Metadata 中。
下面圖(六)說明 Visual Studio 使用 Link 程式碼檔案的情況。
圖(六)、在 Visual Studio 使用 Link 程式碼檔案的情境
這種情況下,加上在原有架構下,IntelliSys.Web 與 Services 都可以同時存取 CacheProvider,如果有一個 Request 從 IntelliSys.Services 進來,但它卻因為 Entities & ViewModels 在 Application Block 區塊裡,也就是 IntelliSys.Web 層,但 IntelliSys.Services 卻使用 Assembly.Load() 動態的叫用 IntelliSys.Web 下的 Business Logic ,所以這時候的 AppDomain 其實是運作在 IntelliSys.Web 下,而當時的 Execute Assemblies 會抓到的是 IntelliSys.Web 的 DataSetInRedis 而不是 IntelliSys.Services 下面的 DataSetInRedis 物件而產生這個詭異的問題。
但因為 IntelliSys 的原始架構是如此,我只能先順應 IntelliSys 的原始架構進行修改,否則我應該得起另一個架構調整的顧問案來進行這一塊。
而修改的方式如下圖(七)
圖(七)、改以判斷 TypeName 的方式
但是判斷字串的方式並不是非常的理想,所以,我上次在軟體開發之路才對提到,我通常不太建議使用 Link 的方式來設計軟體架構,或者應該說在架構設計裡盡量不要使用 Link 檔案,因為這非常不利於軟體架構的設計,它看似只須要維護一次程式,事實上,它讓你每一個專案都跟這個 (*.CS/;*.VB) 程式檔案黏在一起,當你需要增加某(套件/專案)變成所有有 Link 到這個程式檔案的專案都要加,那是非常恐怖的!當我專案間需要建立相依性時,還會有循環參考的問題,最後會想拆,但是因為 Link 住了,又拆不開… 然而最好的做法是,你只要將需要將共用的部分獨立為一個 Assembly 並提供給需要用的專案參考不就都解了?這麼一來,我一樣只需要修改一次程式碼隨即套用在所有有參考的專案上面,這樣更容易的實作抽離、管理耦合性、也更好維護,不是嗎?
最後
有些 IDE 工具方便好用的功能不見得適合用在像今天這樣的情境,或者像是撰寫程式時,過分的使用語法糖對於是否真的會增加程式碼的可讀性?又或者反而增加以後維護時的困難。有時候多利用排版 + Coding Standard / Rule 、建立團隊撰寫程式碼的共識可能更加的重要。
關於 Gelis:
資深 .NET 技術顧問
FB 社團 (軟體開發之路):
https://www.facebook.com/groups/361804473860062/
FB 粉絲團 (Gelis 的程式設計訓練營):
https://www.facebook.com/gelis.dev.learning/
我講授過的課程 SlideShare:
https://www.slideshare.net/GelisWu
以下是我經營的項目與內容:
(1). 企業內訓課程
(2). 專業顧問
企業內訓課程:
1. .NET Core 3.1 從入門到進階
先前實體課程連結
2. 跨平台的 Web API Framework 框架設計
先前實體課程連結
3. 決戰 OOAD 系列課程 - 使用 UML
先前實體課程連結
4. 單元測試 UnitTest 與 Moq 物件實務課程
先前實體課程連結
5. 快速開發系列 - C# Project Templates 範本設計
留言
張貼留言