倉儲(chǔ)模式是否依然適用于EF Core?
昨天『.NET 大牛之路』群里的小伙伴們談?wù)摿耸褂?EF Core 實(shí)現(xiàn)倉儲(chǔ)模式的話題,我想起以前看過一篇一名國(guó)外大佬寫的文章,覺得非常有參考價(jià)值,今天進(jìn)行了翻譯,供大家賞鑒。
原文:bit.ly/3cGDqZ0
作者:Jon P Smith
翻譯:精致碼農(nóng)-王亮
說明:原文首次發(fā)布于 2018 年 2 月,最后更新于 2020 年 7 月。
正文:
我在 2014 年寫了第一篇關(guān)于倉儲(chǔ)模式的文章,它仍然是一篇很受歡迎的文章。而這一篇文章是那篇文章的更新版,基于這幾年 EF Core 新的發(fā)布和對(duì) EF Core 數(shù)據(jù)庫訪問模式的進(jìn)一步研究。
1. Original: Analysing whether Repository pattern useful with Entity Framework (2014年5月).
https://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework/
2. First solution: Four months on – my solution to replacing the Repository pattern (2014年10月).
https://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework-part-2/
3. THIS ARTICLE: Is the repository pattern useful with Entity Framework Core?1概要
答案是“否”,倉儲(chǔ)/工作單元模式(簡(jiǎn)稱 Rep/UoW)對(duì) EF Core 沒有用。EF Core 已經(jīng)實(shí)現(xiàn)了 Rep/UoW 模式,所以在 EF Core 上再加一個(gè) Rep/UoW 模式是沒有用的。
更好的解決方案是直接使用 EF Core,它允許你使用 EF Core 的所有功能來構(gòu)建高性能的數(shù)據(jù)庫訪問。
2本文目的
這篇文章關(guān)注的是:
人們對(duì) EF 的 Rep/UoW 模式有什么看法。
在 EF 中使用 Rep/UoW 模式的優(yōu)點(diǎn)和缺點(diǎn)。
用 EF Core 代碼取代 Rep/UoW 模式的三種方法。
如何使你的 EF Core 數(shù)據(jù)庫訪問代碼易于發(fā)現(xiàn)和重構(gòu)。
關(guān)于 EF Core 單元測(cè)試的討論。
我將假設(shè)你熟悉 C# 和 EF 6.x 或 EF Core 庫。文中我特別談到了 EF Core,但大部分內(nèi)容也與 EF6.x 有關(guān)。
3背景
在 2013 年,我參與了一個(gè)專門用于醫(yī)療保健建模的大型網(wǎng)絡(luò)應(yīng)用程序的開發(fā)工作。我使用了 ASP.NET MVC4 和 EF 5,后者當(dāng)時(shí)剛剛問世,支持處理地理數(shù)據(jù)的 SQL Spatial 類型。當(dāng)時(shí)流行的數(shù)據(jù)庫訪問模式是 Rep/UoW 模式--參見微軟在 2013 年寫的關(guān)于使用 EF Core 和 Rep/UoW 模式訪問數(shù)據(jù)庫的文章。
Implementing the Repository and Unit of Work Patterns in an ASP.NET MVC Application
https://docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application我使用 Rep/UoW 構(gòu)建了我的應(yīng)用程序,但在開發(fā)過程中發(fā)現(xiàn)這確實(shí)是一個(gè)痛點(diǎn)。我不得不不斷地“調(diào)整”版本庫的代碼來修復(fù)一些小問題,而每次“調(diào)整”都會(huì)破壞一些其他的東西。正是這一點(diǎn)讓我研究如何更好地實(shí)現(xiàn)我的數(shù)據(jù)庫訪問代碼。
說到這里,我在 2017 年底與一家新成立的公司簽約,幫助他們解決 EF6.x 應(yīng)用程序的性能問題。性能問題的主要部分被證明是由于懶加載,這是需要的,因?yàn)閼?yīng)用程序使用了 Rep/UoW 模式。
事實(shí)證明,一個(gè)參與啟動(dòng)項(xiàng)目的程序員曾經(jīng)使用過 Rep/UoW 模式,在與該公司的創(chuàng)始人交談時(shí),他說他發(fā)現(xiàn)應(yīng)用程序的 Rep/UoW 部分是相當(dāng)不透明的,很難操作。
4人們?nèi)绾慰磦}儲(chǔ)模式
在研究 Spatial Modeller? 設(shè)計(jì)的過程中,我發(fā)現(xiàn)了一些博客文章,為拋棄倉儲(chǔ)模式提供了有力的證據(jù)。這類文章中最有說服力、考慮最周全的是“Repositories On Top UnitOfWork Are Not a Good Idea”。Rob Conery 的主要觀點(diǎn)是,Rep/UoW 只是重復(fù)了 Entity Framework (EF) DbContext 給你的東西,所以為什么要把一個(gè)完美的框架隱藏在一個(gè)沒有任何價(jià)值的外表下呢。Rob 稱之為“過度抽象的愚蠢行為”。
另一篇博客是“Why Entity Framework renders the Repository pattern obsolete”。在這篇文章中,Isaac Abraham 補(bǔ)充說,倉儲(chǔ)模式并沒有使測(cè)試變得更容易,這是它本應(yīng)該做的一件事。這一點(diǎn)在 EF Core 中更加實(shí)際,你將在后面看到。
那么,他們是對(duì)的嗎?
5我對(duì) Rep/UoW 模式的看法
讓我試著以盡可能公平的方式回顧一下 Rep/UoW 模式的優(yōu)點(diǎn)和缺點(diǎn)。以下是我的觀點(diǎn)。
Rep/UoW 模式的優(yōu)點(diǎn)
隔離你的數(shù)據(jù)庫訪問代碼。倉庫模式的最大優(yōu)點(diǎn)是你知道你所有的數(shù)據(jù)庫訪問代碼在哪里。另外,你通常會(huì)把你的倉儲(chǔ)庫分成幾個(gè)部分,如目錄庫、訂單處理庫等,這使得你很容易找到有錯(cuò)誤或需要性能調(diào)整的特定查詢的代碼。這無疑是一個(gè)很大的優(yōu)點(diǎn)。
聚合(Aggregation)。領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)是一種設(shè)計(jì)系統(tǒng)的方法,它建議你有一個(gè)根實(shí)體,其他相關(guān)的實(shí)體被歸入它。我在《Entity Framework Core in Action》一書中使用的例子是一個(gè)帶有 Review 實(shí)體集合的 Book 實(shí)體。這些 Review 只有在與 Book 相聯(lián)系時(shí)才有意義,所以 DDD 說你應(yīng)該只通過 Book 實(shí)體來改變 Review。Rep/UoW 模式通過提供一種方法在 Book Repository 中將評(píng)論添加/刪除中來實(shí)現(xiàn)這一點(diǎn)。
隱藏復(fù)雜的 T-SQL 命令。有時(shí)你需要繞過 EF Core,使用 T-SQL。這種類型的訪問應(yīng)該從高層隱藏起來,但又容易找到,以幫助維護(hù)或重構(gòu)。我應(yīng)該指出,Rob Conery 的帖子 Command/Query Objects 也可以處理這個(gè)問題。
易于模擬/測(cè)試。很容易模擬一個(gè)單獨(dú)的資源庫,這使得訪問數(shù)據(jù)庫的單元測(cè)試代碼更容易。這在若干年前是真的,但現(xiàn)在這有其他的方法來解決這個(gè)問題,我將在后面介紹。
你會(huì)注意到,我沒有提出“用另一個(gè)數(shù)據(jù)庫訪問庫替換 EF Core”。這是 Rep/UoW 背后的想法之一,但我認(rèn)為這是一個(gè)誤解,因?yàn)?a)很難替換一個(gè)數(shù)據(jù)庫訪問庫,b)你真的會(huì)在你的應(yīng)用程序中交換這樣一個(gè)關(guān)鍵庫嗎?
Rep/UoW 模式的缺點(diǎn)
前三個(gè)項(xiàng)目都是圍繞著性能。我并不是說你不能寫一個(gè)高效的 Rep/UoW,但它是一項(xiàng)艱難的工作,而且我看到許多實(shí)現(xiàn)都有內(nèi)在的性能問題(包括微軟舊的 Rep/UoW 實(shí)現(xiàn))。以下是我在 Rep/UoW 模式中發(fā)現(xiàn)的缺點(diǎn)清單。
性能--處理實(shí)體關(guān)系。一個(gè)資源庫通常會(huì)返回一種類型的
IEnumerable/IQueryable結(jié)果,例如在微軟的例子中的一個(gè) Student 實(shí)體類。假設(shè)你想從 Student 的關(guān)系中顯示信息,比如他們的地址?在這種情況下,倉儲(chǔ)庫中最簡(jiǎn)單的方法是使用懶加載來讀取學(xué)生的地址實(shí)體,我看到人們經(jīng)常這樣做。問題是懶加載會(huì)導(dǎo)致每一個(gè)關(guān)系都要單獨(dú)往返于數(shù)據(jù)庫,這比把所有的數(shù)據(jù)庫訪問合并成一次數(shù)據(jù)庫往返要慢。(另一種方法是有多個(gè)不同返回類型的查詢方法,但這將使你的資源庫變得非常大和麻煩--見第 4 點(diǎn))。數(shù)據(jù)不符合要求的格式。因?yàn)閭}儲(chǔ)組件通常是根據(jù)數(shù)據(jù)庫創(chuàng)建的,返回的數(shù)據(jù)可能不是服務(wù)或用戶需要的確切格式。你也許可以調(diào)整倉儲(chǔ)庫的輸出,但這是你必須要寫的第二個(gè)階段。我認(rèn)為更好的做法是在靠近前端的地方形成你的查詢,并包括你需要的數(shù)據(jù)的任何調(diào)整。
性能--更新:許多 Rep/UoW 的實(shí)現(xiàn)試圖隱藏 EF Core,但這樣做并沒有利用它的所有功能。例如,Rep/UoW 會(huì)使用 EF Core 的 Update 方法更新一個(gè)實(shí)體,該方法會(huì)保存實(shí)體中的每個(gè)屬性。而使用 EF Core 內(nèi)置的變化跟蹤功能,它將只更新已經(jīng)改變的屬性。
太通用了。Rep/UoW 的誘惑力來自于你可以寫一個(gè)通用的倉儲(chǔ)庫(Repository),然后用它來建立所有的子倉儲(chǔ)庫,例如目錄庫、訂單處理庫等。這應(yīng)該可以最大限度地減少你需要寫的代碼,但我的經(jīng)驗(yàn)是,一個(gè)通用的倉儲(chǔ)庫在開始時(shí)是有效的,但隨著事情變得越來越復(fù)雜,你最終不得不為每個(gè)單獨(dú)的倉儲(chǔ)庫添加越來越多的代碼。“The more reusable the code is, the less usable it is.” --Neil Ford
總結(jié)一下不好的地方--Rep/UoW 隱藏了 EF Core,這意味著你不能使用 EF Core 的功能來編寫簡(jiǎn)單但高效的數(shù)據(jù)庫訪問代碼。
6如何保留 Rep/UoW 的優(yōu)點(diǎn)使用 EF Core
在前面的優(yōu)點(diǎn)部分中,我列出了隔離、聚合、隱藏和單元測(cè)試,Rep/UoW 做得很好。在這一節(jié)中,我將談?wù)撘恍┎煌能浖J胶蛯?shí)踐,當(dāng)你直接使用 EF Core 時(shí),這些模式和實(shí)踐與良好的架構(gòu)設(shè)計(jì)相結(jié)合,提供同樣的隔離、聚合等功能。
我將解釋每一個(gè)優(yōu)點(diǎn)的實(shí)現(xiàn),然后把它們放到一個(gè)分層的軟件架構(gòu)中。
1. 查詢對(duì)象:一種隔離和隱藏?cái)?shù)據(jù)庫讀取的方法
數(shù)據(jù)庫訪問可分為四種類型。新增、讀取、更新和刪除--被稱為 CRUD。對(duì)我來說,讀的部分,在 EF Core 中被稱為查詢,往往是最難建立和性能調(diào)整的。許多應(yīng)用程序都依賴于良好的、快速的查詢,例如,要購(gòu)買的產(chǎn)品列表,要做的事情列表,等等。人們想出的方案是查詢對(duì)象。
我第一次接觸到它們是在 2013 年 Rob Conery 的文章中(前面提到),他提到了命令/查詢對(duì)象。另外,Jimmy Bogard 在 2012 年發(fā)表了一篇名為“Favor query objects over repositories”的文章。使用 .NET 的 IQueryable 類型和擴(kuò)展方法,我們可以在 Rob 和 Jimmy 的例子中改進(jìn)查詢對(duì)象模式。
下面的列表給出了一個(gè)查詢對(duì)象的簡(jiǎn)單例子,它可以選擇一個(gè)整數(shù)列表的排序方式。
public?static?class?MyLinqExtension
{
public?static IQueryable<int> MyOrder
(this IQueryable<int> queryable, bool?ascending)
{
return?ascending
? queryable.OrderBy(num => num)
: queryable.OrderByDescending(num => num);
}
}下面這是這個(gè) MyOrder 查詢對(duì)象使用示例:
var numsQ = new[] { 1, 5, 4, 2, 3 }.AsQueryable();
var result = numsQ
.MyOrder(true)
.Where(x => x > 3)
.ToArray();MyOrder 查詢對(duì)象的工作原理是,IQueryable 類型持有一個(gè)命令列表,當(dāng)我應(yīng)用 ToArray 方法時(shí),這些命令會(huì)被執(zhí)行。在我的簡(jiǎn)單例子中,我沒有使用數(shù)據(jù)庫,但如果我們用應(yīng)用程序的 DbContext 的 DbSet 屬性替換 numsQ 變量,那么 IQueryable 類型中的命令將被轉(zhuǎn)換為數(shù)據(jù)庫命令。
因?yàn)?IQueryable 類型直到最后才被執(zhí)行,所以你可以將多個(gè)查詢對(duì)象連鎖起來。讓我從我的書《Entity Framework Core in Action》中給你一個(gè)更復(fù)雜的數(shù)據(jù)庫查詢的例子。在下面的代碼中,使用了四個(gè)查詢對(duì)象鏈在一起,對(duì)一些圖書的數(shù)據(jù)進(jìn)行選擇、排序、過濾和分頁。你可以在實(shí)時(shí)網(wǎng)站 efcoreinaction.com 看到這些。
public IQueryable SortFilterPage
(SortFilterPageOptions options)
{
var booksQuery = _context.Books
.AsNoTracking()
.MapBookToDto()
.OrderBooksBy(options.OrderByOptions)
.FilterBooksBy(options.FilterBy,
options.FilterValue);
options.SetupRestOfDto(booksQuery);
return booksQuery.Page(options.PageNum-1,
options.PageSize);
} 查詢對(duì)象提供了比 Rep/UoW 模式更好的隔離性,因?yàn)槟憧梢园褟?fù)雜的查詢分割成一系列的查詢對(duì)象,并把它們連在一起。這使得它更容易編寫和理解、重構(gòu)和測(cè)試。另外,如果你有一個(gè)需要原始 SQL 的查詢,你可以使用 EF Core 的 FromSql 方法,它也可以返回 IQueryable。
2. 新增、更新和刪除數(shù)據(jù)庫訪問方法
查詢對(duì)象處理了 CRUD 的讀取部分,但是新增、更新和刪除部分呢,也就是你向數(shù)據(jù)庫寫入的部分?我將向你展示運(yùn)行 CUD 操作的兩種方法:直接使用 EF Core 命令,以及使用實(shí)體類中的 DDD 方法。讓我們來看看非常簡(jiǎn)單的更新例子:在我的圖書應(yīng)用中添加一個(gè)評(píng)論(見efcoreinaction.com)。
注:如果你想嘗試添加評(píng)論,有一個(gè)與我的書配套的 GitHub repo(
github.com/JonPSmith/EfCoreInAction),選擇分支 Chapter05(每章都有一個(gè)分支)并在本地運(yùn)行該應(yīng)用程序。你會(huì)看到每本書旁邊都有一個(gè)管理按鈕,有幾個(gè) CUD 命令。
方式一:直接使用 EF Core 命令
最明顯的方法是使用 EF Core 方法來完成數(shù)據(jù)庫的更新。下面是一個(gè)方法,它將為一本書添加一個(gè)新的評(píng)論,其中包括用戶提供的評(píng)論信息。注意:ReviewDto 是一個(gè)持有用戶填寫完評(píng)論信息后返回的信息類。
public Book AddReviewToBook(ReviewDto dto)
{
var book = _context.Books
.Include(r => r.Reviews)
.Single(k => k.BookId == dto.BookId);
var newReview = new Review(dto.numStars, dto.comment, dto.voterName);
book.Reviews.Add(newReview);
_context.SaveChanges();
return book;
}注:
AddReviewToBook方法是在一個(gè)叫做AddReviewService的類中,這個(gè)類在我的ServiceLayer中。這個(gè)類被注冊(cè)為一個(gè)服務(wù),并且有一個(gè)構(gòu)造函數(shù),它接收應(yīng)用程序的DbContext,這個(gè)DbContext是通過 DI 注入的。注入的值被存儲(chǔ)在私有字段_context中,AddReviewToBook方法可以使用它來訪問數(shù)據(jù)庫。
這將把新的評(píng)論添加到數(shù)據(jù)庫中,這很有效,但還有另一種方法可以使用更多的 DDD 方法來構(gòu)建。
方式二:DDD 風(fēng)格的實(shí)體類
EF Core 為我們提供了一個(gè)新的地方來編寫你的更新代碼--實(shí)體類內(nèi)部。EF Core 有一個(gè)叫做后援字段(backing fields)的功能,它使構(gòu)建 DDD 實(shí)體成為可能。后援字段允許你控制對(duì)任何關(guān)系結(jié)構(gòu)的訪問。這在 EF6.x 中其實(shí)是不可能的。
DDD 談到了聚合(前面提到過),所有的聚合只能通過根實(shí)體中的方法來改變,我把它稱為訪問方法。在 DDD 術(shù)語中,評(píng)論是圖書實(shí)體的聚合,所以我們應(yīng)該通過圖書實(shí)體類中的一個(gè)名為 AddReview 的訪問方法來添加一個(gè)評(píng)論。這樣一來,上面的代碼就變成了 Book 實(shí)體中的一個(gè)方法,在這里:
public Book AddReviewToBook(ReviewDto dto)
{
var book = _context.Find(dto.BookId);
book.AddReview(dto.numStars, dto.comment,
dto.voterName, _context);
_context.SaveChanges();
return book;
} Book 實(shí)體類中的 AddReview 訪問方法看起來像這樣:
public?class?Book
{
private HashSet _reviews;
public IEnumerable Reviews => _reviews?.ToList();
//...other properties left out
//...constructors left out
public?void?AddReview(int numStars, string comment,
string voterName, DbContext context = null)
{
if (_reviews != null)
{
_reviews.Add(new Review(numStars, comment, voterName));
}
else?if (context == null)
{
throw?new ArgumentNullException(nameof(context),
"You must provide a context if the Reviews collection isn't valid.");
}
else?if (context.Entry(this).IsKeySet)
{
context.Add(new Review(numStars, comment, voterName, BookId));
}
else
{
throw?new InvalidOperationException("Could not add a new review.");
}
}
//...
} 這個(gè)方法更復(fù)雜,因?yàn)樗梢蕴幚韮煞N不同的情況:一種是已經(jīng)加載了 Review,另一種是還沒有。但如果評(píng)論還沒有被加載,它比原來的情況要快,因?yàn)樗褂昧恕巴ㄟ^外鍵創(chuàng)建關(guān)系”的方法。
因?yàn)樵L問方法的代碼在實(shí)體類里面,如果需要的話可以更復(fù)雜,因?yàn)樗鼘⑹悄阈枰獙懙奈ㄒ话姹镜拇a。在方式一中,你可以在不同的地方重復(fù)相同的代碼,只要你需要更新 Book 的 Review 集合。
注:我寫了一篇名為“Creating Domain-Driven Design entity classes with Entity Framework Core”的文章,全部都是關(guān)于 DDD 風(fēng)格的實(shí)體類。這篇文章對(duì)這個(gè)話題有更詳細(xì)的介紹。我還更新了關(guān)于如何用 EF Core 編寫業(yè)務(wù)邏輯的文章,以使用同樣的 DDD 風(fēng)格的實(shí)體類。
為什么實(shí)體類中的方法不調(diào)用 SaveChanges?在方式一中,一個(gè)方法包含了所有的部分:a)加載實(shí)體,b)更新實(shí)體,c)調(diào)用 SaveChanges 來更新數(shù)據(jù)庫。我可以這樣做,因?yàn)槲抑浪怯梢粋€(gè)網(wǎng)絡(luò)請(qǐng)求調(diào)用的,而這就是我想做的全部。對(duì)于 DDD 實(shí)體方法,你不能在實(shí)體方法中調(diào)用 SaveChanges,因?yàn)槟悴荒艽_定操作已經(jīng)完成。例如,如果你從備份中加載一本書,你可能想創(chuàng)建這本書,添加作者,添加任何評(píng)論,然后調(diào)用 SaveChanges,這樣所有的東西都在一起被提交到數(shù)據(jù)庫。
方式三:GenericServices 庫
還有第三種方式。我注意到在我構(gòu)建的 ASP.NET 應(yīng)用程序中使用 CRUD 命令時(shí)有一個(gè)標(biāo)準(zhǔn)模式,早在 2014 年我就建立了一個(gè)名為 GenericServices 的庫,它可以與 EF6.x 一起使用。2018 年我為 EF Core 建立了一個(gè)更全面的版本,名為 EfCore.GenericServices,見這篇關(guān)于 EfCore.GenericServices 的文章:
GenericServices: A library to provide CRUD front-end services from a EF Core database
https://www.thereformedprogrammer.net/genericservices-a-library-to-provide-crud-front-end-services-from-a-ef-core-database/這些庫并沒有真正實(shí)現(xiàn)倉儲(chǔ)模式,而是在實(shí)體類和前端需要實(shí)際數(shù)據(jù)之間充當(dāng)適配器模式。我曾使用過原始的 EF6.x,GenericServices 為我節(jié)省了幾個(gè)月的枯燥的前端代碼編寫。新的 EfCore.GenericServices 甚至更好,因?yàn)樗梢耘c標(biāo)準(zhǔn)風(fēng)格的實(shí)體類和 DDD 風(fēng)格的實(shí)體類一起工作。
哪一個(gè)方式更好
方式一(直接使用 EF Core 代碼)要寫的代碼最少,但有可能出現(xiàn)重復(fù),因?yàn)閼?yīng)用程序的不同部分可能要對(duì)一個(gè)實(shí)體應(yīng)用 CUD 命令。例如,當(dāng)用戶通過改變事物時(shí),你可能會(huì)通過 ServiceLayer 進(jìn)行更新,但外部 API 可能不會(huì)通過 ServiceLayer,所以你必須重復(fù) CUD 代碼。
方式二(DDD 風(fēng)格的實(shí)體類)將關(guān)鍵的更新部分放在實(shí)體類內(nèi),所以代碼對(duì)任何能得到實(shí)體實(shí)例的人都是可用的。事實(shí)上,由于 DDD 風(fēng)格的實(shí)體類“鎖定”了對(duì)屬性和集合的訪問,每個(gè)人都必須使用 Book 實(shí)體的 AddReview 訪問方法,如果他們想更新 Review 集合的話。出于許多原因,這是我想在未來的應(yīng)用中使用的方法(見我的文章中關(guān)于利弊的討論)。其(輕微的)缺點(diǎn)是它需要一個(gè)單獨(dú)的加載/保存部分,這意味著更多的代碼。
方式三(GenericServices 庫)是我的首選方法,尤其是現(xiàn)在我已經(jīng)建立了 EfCore.GenericServices 版本,可以處理 DDD 風(fēng)格的實(shí)體類。正如你在關(guān)于 EfCore.GenericServices 的文章中所看到的,這個(gè)庫極大地減少了你在 Web/移動(dòng)/桌面應(yīng)用程序中需要編寫的代碼。當(dāng)然,你仍然需要在你的業(yè)務(wù)邏輯中訪問數(shù)據(jù)庫,但這是另一回事。
7組織你的 CRUD 代碼
Rep/UoW 模式的一個(gè)好處是,它將你所有的數(shù)據(jù)訪問代碼放在一個(gè)地方。當(dāng)換成直接使用 EF Core 時(shí),你可以把你的數(shù)據(jù)訪問代碼放在任何地方,但這使得你或其他團(tuán)隊(duì)成員很難找到它。因此,我建議對(duì)你的代碼放在哪里有一個(gè)明確的計(jì)劃,并堅(jiān)持下去。
下圖展示了一個(gè)分層或六邊形的架構(gòu),只展示了三個(gè)程序集(我省去了業(yè)務(wù)邏輯,在六邊形的架構(gòu)中,你會(huì)有更多的程序集)。顯示的三個(gè)程序集是:
ASP.NET Core。這是表現(xiàn)層,提供 HTML 頁面或一個(gè)網(wǎng)絡(luò) API。這沒有數(shù)據(jù)庫訪問代碼,但依賴于
ServiceLayer和BusinessLayer中的各種方法。服務(wù)層。它包含數(shù)據(jù)庫訪問代碼,包括查詢對(duì)象和新增、更新和刪除方法。服務(wù)層使用適配器模式和命令模式來連接數(shù)據(jù)層和 ASP.NET Core(表現(xiàn))層。
數(shù)據(jù)層。它包含了應(yīng)用程序的
DbContext和實(shí)體類。然后,DDD 風(fēng)格的實(shí)體類包含訪問方法,以允許根實(shí)體及其聚合體被修改。

注:前面提到的庫
GenericServices(EF6.x)和EfCore.GenericServices(EF Core)實(shí)際上是一個(gè)提供ServiceLayer功能的庫,即在DataLayer和你的 Web/移動(dòng)/桌面應(yīng)用程序之間充當(dāng)適配器模式和命令模式。
從這個(gè)圖中我想說的是,通過使用不同的程序集,一個(gè)簡(jiǎn)單的命名標(biāo)準(zhǔn)(見圖中黑體字 Book)和文件夾,你可以建立一個(gè)應(yīng)用程序,其中你的數(shù)據(jù)庫代碼是獨(dú)立的,很容易找到。隨著你的應(yīng)用程序的增長(zhǎng),這可能是至關(guān)重要的。
8EF Core 方法單元測(cè)試
最后要看的部分是對(duì)使用 EF Core 的應(yīng)用程序進(jìn)行單元測(cè)試。倉儲(chǔ)模式的優(yōu)點(diǎn)之一是你可以在測(cè)試時(shí)用一個(gè)模擬來代替它。因此,直接使用 EF Core 就失去了模擬的選擇(技術(shù)上你可以模擬 EF Core,但很難做得好)。
值得慶幸的是,現(xiàn)在的 EF Core 已經(jīng)有了進(jìn)步,你可以用內(nèi)存數(shù)據(jù)庫來模擬數(shù)據(jù)庫了。內(nèi)存數(shù)據(jù)庫的創(chuàng)建速度更快,而且有一個(gè)默認(rèn)的起始點(diǎn)(即,空),所以針對(duì)它編寫測(cè)試要容易得多。參見我的文章“Using in-memory databases for unit testing EF Core applications” 詳細(xì)了解如何做到這一點(diǎn),另外還有一個(gè)名為 EfCore.TestSupport 的 NuGet 包,它提供了一些方法,使編寫 EF Core 單元測(cè)試更加快速。
9結(jié)論
我上一個(gè)使用 Rep/UoW 模式的項(xiàng)目要追溯到 2013 年,從那以后我再也沒有使用過 Rep/UoW 模式。我嘗試過一些方法,一個(gè)基于 EF6.x 名為 GenericServices 的自定義庫,以及現(xiàn)在一個(gè)更標(biāo)準(zhǔn)的基于 EF Core 實(shí)現(xiàn)查詢對(duì)象和 DDD 風(fēng)格的實(shí)體類方法的 EfCore.GenericServices 自定義庫。它們使得編寫代碼更容易,而且通常表現(xiàn)良好。但如果它們很慢,就很容易定位并對(duì)單個(gè)數(shù)據(jù)庫訪問進(jìn)行性能調(diào)整。
在我為 Manning 出版社寫的書中,有一章是對(duì)一個(gè)“賣”書的 ASP.NET Core 應(yīng)用程序進(jìn)行性能調(diào)整。這個(gè)過程使用了查詢對(duì)象和 DDD 實(shí)體方法,并表明它可以產(chǎn)生性能很好的數(shù)據(jù)庫訪問(見我的文章“ Entity Framework Core performance tuning – a worked example”)。
我自己的工作是使用查詢對(duì)象進(jìn)行讀取,并使用 DDD 風(fēng)格的實(shí)體類及其 CUD 和業(yè)務(wù)邏輯的訪問方法。我確實(shí)需要在一個(gè)適當(dāng)?shù)膽?yīng)用中使用這些東西,才能真正知道它們是否有效。請(qǐng)關(guān)注我的博客,了解更多關(guān)于 DDD 風(fēng)格的實(shí)體類,以及從中受益的架構(gòu),也許還會(huì)有一個(gè)新的庫:)。
編碼愉快!
