重構 ASP.NET 5/EF6 專案和依賴關係注入

發表於2016-05-16

依賴關係注入 (DI) 都是關於鬆耦合的 (bit.ly/1TZWVtW)。您從其他位置(理想情況下是類建構函式)請求獲取您依賴的類,而不是將這些類硬編碼為其他類。這遵循的是顯式依賴關係原則,可以更明確地告知類使用者此類所需的協作者。這樣一來,您還可以在類物件例項有備選配置的情況下構建更靈活的軟體,同時這也對編寫這種類的自動測試真正有益。我的工作領域就是和 Entity Framework 程式碼不停地打交道。典型的例子是:在不使用鬆耦合的情況下編碼就是在建立可直接例項化 DbContext 的儲存庫或控制器。我已經上千次這樣做過。實際上,我撰寫這篇文章的目標是為了向我在專欄“EF6、EF7 與 ASP.NET 5 組合”(msdn.com/magazine/dn973011) 中編寫的程式碼應用我所學到的 DI 知識。例如,在下面的方法中,我就直接例項化了 DbContext:

由於我是在 ASP.NET 5 解決方案中使用了這種方法,而且 ASP.NET 5 也內建瞭如此多的 DI 支援,因此 EF 團隊的 Rowan Miller 就建議我利用 DI 支援來改進此示例。我一直以來都十分關注這個問題的其他方面,甚至都沒有考慮過這一點。所以,我就開始了一點一點地重構此示例,直到我能讓流按規定執行。實際上,Miller 曾指導我參考 Paweł Grudzień 在其博文“結合使用 Entity Framework 6 和 ASP.NET 5”(bit.ly/1k4Tt4Y) 中編寫的完美示例,但我明確表示會轉移我的視線,不會簡單地複製貼上這篇博文中的程式碼。相反,我是獨立重構此示例,這樣我就能更好地理解流了。最後,我很高興地發現,我的解決方案與那篇博文中的非常一致。

一直以來,我似乎都覺得控制反轉 (IoC) 和 IoC 容器是有點艱鉅的模式。請注意,自我編碼以來已有近 30 年,所以我想我並不是唯一一個在心理上從未準備向此模式過渡的有經驗開發者。Martin Fowler 在此領域是非常著名的專家。他指出 IoC 具有多重意義,而與 DI(他為了闡述 IoC 這一優點而創造了這個術語)一致的意義則在於明確應用程式的哪一部分可控制建立特定物件。如果沒有 IoC,這一直以來都是個難題。

在與 Steve Smith (deviq.com) 合著 Pluralsight 課程“域驅動設計基礎知識”(bit.ly/PS-DDD) 時,我最終接受了使用 StructureMap 庫。此庫自 2005 年建立以來就成為 .NET 開發者最常用的 IoC 容器之一。說到底,我參與這場遊戲有點晚了。在 Smith 的指導下,我能夠了解此庫的工作原理及其優勢,但仍覺得尚未相當熟練地掌握它。所以,在 Miller 提示我後,我就決定要重構我之前的示例,以便利用容器,這樣可以更加容易地將物件例項注入需要使用這些例項的邏輯中。

但首先,讓我們來談談“不要自我重複”

我的類(囊括了之前所示 GetAllNinjas 類)中最初出現的問題是,我在此類的其他方法中重複使用以下 using 程式碼:

如下所示:

不要自我重複 (DRY) 原則幫助我發現了這一潛在問題。我將把 NinjaContext 例項的建立程式碼移入建構函式中,並與各種方法共享諸如 _context 之類的變數:

不過,此類應僅以檢索資料為重點,卻仍在負責確定如何以及何時建立上下文。我想在流中將確定如何以及何時建立上下文的任務上移,只讓我的儲存庫使用注入的上下文。因此,我將再次重構,以便傳遞其他位置建立的上下文:

現在,儲存庫就可以獨立執行了。我無需通過不斷清理它來建立上下文。儲存庫並不關注上下文的配置方式、建立時間或處置時間。這還有助於此類遵循另一項物件導向的原則(即單一責任原則),因為它除了負責提出資料庫請求之外,不再負責管理 EF 上下文。在處理儲存庫類時,我可以查詢為重點。我還可以更輕鬆地進行測試,因為我的測試可以引導這些決策,不會受到以下儲存庫的掣肘:設計使用方式與我希望在自動測試使用它的方式不一致。

我原來的示例還存在一個問題,就是我將連線字串硬編碼為 DbContext。我剛才也說明了理由,就是因為這“只是個演示”,而且將連線字串從正在執行的應用(ASP.NET 5 應用程式)移入 EF6 專案過程十分複雜,而我則關注的是其他方面的問題。不過,在我重構此專案時,我將能夠利用 IoC 從正在執行的應用程式傳遞連線字串。請繼續閱讀本文,注意介紹此問題的地方。

讓 ASP.NET 5 注入 NinjaContext

不過,我該將 NinjaContext 建立程式碼移入哪裡呢? 答案就是使用儲存庫的控制器。我當然不想在控制器中引入 EF,以此來將它傳遞到儲存庫的新例項中。這會導致混亂局面的產生(如下所示):

同時,我也在強制控制器注意 EF,此程式碼不存在我剛才在儲存庫中解決的例項化依賴物件的問題。控制器可直接例項化儲存庫類。我只想讓它使用儲存庫,而不用擔心具體的建立方式和時間或處置時間。就像我在儲存庫中注入 NinjaContext 例項一樣,我想在控制器中注入隨時可用的儲存庫例項。

控制器類中更簡潔的程式碼版本更像是下面這樣:

使用 IoC 容器安排建立物件

由於我要處理的是 ASP.NET 5,而不是 StructureMap 中的請求,因此我將利用 ASP.NET 5 內建的 DI 支援。ASP.NET 5 不僅可以注入許多旨在接受物件的新 ASP.NET 類, 還提供可以協調物件去向的服務基礎結構(即 IoC 容器)。它還允許您指定將建立和注入的物件的範圍(應何時建立和處置物件)。藉助內建支援是更簡單的入門方法。

在藉助 ASP.NET 5 DI 支援以根據需要注入 NinjaContext 和 NinjaRepository 之前,讓我們來看看注入 EF7 類是什麼樣的情況,因為 EF7 內建可關聯 ASP.NET 5 DI 支援的方法。屬於標準 ASP.NET 5 專案的 startup.cs 類具有稱為“ConfigureServices”的方法。您就是在這其中告知應用程式您想如何關聯依賴關係的,以便建立適當的物件並將其注入需要使用這些物件的物件中。在下面的方法中,除 EF7 配置以外的其他所有內容均已被排除:

我的專案使用的是基於 EF6 的模型。與我的專案不同,正在執行此配置的專案依賴 EF7。接下來的幾個段落介紹了這種程式碼出現的具體情況。

由於 EntityFramework .MicrosoftSqlServer 已在其 project.json 檔案中指定,因此專案引用所有相關的 EF7 程式集。其中的一個程式集 EntityFramework.Core 向 IServiceCollection 提供了 AddEntityFramework 擴充套件方法,這樣我就能新增 Entity Framework 服務了。EntityFramework .MicrosoftSqlServer dll 提供了 AddSqlServer 擴充套件方法,此方法追加到了 AddEntityFramework 中。這就將 SqlServer 服務打包入 IoC 容器,以便 EF 知道在查詢資料庫提供程式時使用它。

AddDbContext 是 EF 的核心方法。這種核心方法可向 ASP.NET 5 內建容器新增指定的 DbContext 例項(包含指定的選項)。在其建構函式中發出 DbContext 請求(以及 ASP.NET 5 正在構造的)所有類將會在建立後獲得已配置的 DbContext。所以,這種程式碼將 NinjaContext 新增為一種已知型別,服務將會根據需要進行例項化。此外,程式碼還指定在構造 NinjaContext 時,應使用配置程式碼中的字串(在此示例中,來自 ASP.NET 5 appsettings.json 檔案,由專案模板建立)作為 SqlServer 配置選項。由於 ConfigureService 是在啟動程式碼中執行,因此當應用程式中的所有程式碼都需要 NinjaContext,但又無任何特定例項提供時,ASP.NET 5 會使用指定的連線字串例項化並傳遞新的 NinjaContext 物件。

這就是 EF7 內建的實用功能。遺憾的是,EF6 不具有其中任何一項功能。但既然您知道服務的工作原理,嚮應用程式服務新增 EF6 NinjaContext 的模式應該就是有意義的。

新增不是專為 ASP.NET 5 構建的服務

除了可以新增與 ASP.NET 5 相容的服務(包含實用的擴充套件方法,如 AddEntityFramework 和 AddMvc)之外,還可以新增其他依賴關係。IServicesCollection 介面提供了普通的 Add 方法,以及一系列用於指定所新增服務的生存期的方法: AddScoped、AddSingleton 和 AddTransient。我將針對我的解決方案主要介紹 AddScoped,因為它將請求的例項的生存期限定為 MVC 應用程式(我想在其中使用我的 EF6Model 專案)中每個 HTTP 請求的生存期。此應用程式不會嘗試跨請求共享例項。這將會在每個控制器操作中建立和處置我的 NinjaContext,從而模擬我原來要實現的目標,因為每個控制器操作均響應一個請求。

請注意,我有兩個需要注入物件的類。NinjaRepository 類需要注入 NinjaContext 物件,而 NinjaController 類則需要注入 NinjaRepository 物件。

在 startup.cs ConfigureServices 方法中,我從新增以下程式碼入手:

現在,我的應用程式已注意到這些型別,將在另一個型別的建構函式提出請求時,例項化這些型別。

當控制器建構函式在尋找要作為引數傳遞的 NinjaRepository 時:

但什麼內容都沒有傳遞,服務將快速建立 NinjaRepository。這就被稱為“建構函式注入”。如果 NinjaRepository 需要 NinjaContext 例項,但什麼內容都沒有傳遞,那麼服務也會知道進行例項化。

還記得我之前指出的 DbContext 中獲取的連線字串嗎? 現在,我可以告知構造 NinjaContext 的 AddScoped 方法這個連線字串。我將再次在 appsetting.json 檔案中新增此字串。下面就是此檔案中的相應部分:

請注意,JSON 不支援自動換行,因此以 Server= 開頭的字串無法在 JSON 檔案中自動換行。此處進行自動換行只是為了方便您閱讀。

我已將 NinjaContext 建構函式修改為接收連線字串,並在 DbContext 過載中使用此字串,而這也會接收連線字串:

現在,我可以告知 AddScoped,在發現 NinjaContext 後,應使用相應的過載構造它,同時傳遞 appsettings.json 中的 Ninja­ConnectionString:

在進行這最後一項更改之後,我重構的解決方案現在就能從頭到尾正常執行了。啟動邏輯將應用設定為注入儲存庫和上下文。在應用路由到預設控制器(所使用的儲存庫使用上下文)後,不僅會快速建立所需的物件,還會檢索資料庫中的資料。我的 ASP.NET 5 應用程式利用其內建 DI 與我在其中使用 EF6 構建模型的舊程式集進行互動。

旨在提高靈活性的介面

還可以進行最後一項改進,就是利用介面。如果可以使用不同版本的 NinjaRepository 或 NinjaContext 類,我可能會從頭到尾實現介面。由於我無法預見是否需要在 NinjaContext 上使用變體,因此我將只為儲存庫類建立介面。

圖 1 所示,NinjaRepository 現在實現 INinjaRepository 協定。

ASP.NET 5 MVC 應用程式中的控制器現在使用 INinjaRepository 介面,而不是 NinjaRepository 的具體實現:

我已修改了 NinjaRepository 的 AddScoped 方法,以告知 ASP.NET 5 無論何時需要使用介面,使用相應的實現(當前是 NinjaRepository):

如果需要使用新版本,或者我要在其他應用程式中使用介面的不同實現,那麼我可以將 AddScoped 方法修改為使用正確的實現。

在實踐中學習,而非僅僅複製貼上

我非常感激 Miller 有禮貌地為我提出了重構解決方案的挑戰。從我所撰寫的內容來看,我的重構之路自然不像看起來那麼順利。由於我並不是簡單地複製其他人的解決方案,因此我在一開始就做錯了幾個地方。瞭解所出現的問題並編寫出正確的程式碼,不僅讓我取得了成功,還在很大程度上幫助我瞭解 DI 和 IoC。我希望我撰寫的內容可以為您帶來同樣的幫助,以免您像我一樣在黑暗中摸索。


Julie Lerman 是 Microsoft MVP、.NET 導師和顧問,住在佛蒙特州的山區。您可以在全球的使用者組和會議中看到她對資料訪問和其他 .NET 主題的演示。她的部落格地址是 thedatafarm.com/blog。她是“Entity Framework 程式設計”及其 Code First 和 DbContext 版本(全都出版自 O’Reilly Media)的作者。通過 Twitter 關注她:@julielerman 並在 juliel.me/PS-Videos 上觀看其 Pluralsight 課程。

衷心感謝以下技術專家對本文的審閱: Steve Smith
Steve Smith (@ardalis) 是一位對構建優質軟體熱忱執著的企業家和軟體開發者。Steve 已釋出多門 Pluralsight 課程,涉及 DDD、SOLID、設計模式和軟體體系結構等方面。他身兼數職(Microsoft MVP、作者、導師和培訓師),經常在開發者大會上發言。請訪問 ardalis.com,看看您的團隊或專案可以從 Steve 那兒獲得哪些幫助。

相關文章