ASP.NET Core 中的规约模式(Specification Pattern )——增强泛型仓储模式
原文鏈接:https://codewithmukesh.com/blog/specification-pattern-in-aspnet-core/
在本文中,我們將討論在 ASP.NET Core 應(yīng)用程序中實(shí)現(xiàn)規(guī)約模式以及它如何增強(qiáng)現(xiàn)有的泛型倉儲模式。我們將從頭開始構(gòu)建具有泛型倉儲模式、Entity Framework Core的 ASP.NET Core WebAPI,并最終實(shí)現(xiàn)規(guī)約模式模式。您可以在此處找到此實(shí)現(xiàn)的完整源代碼[1]。讓我們開始吧。
理解規(guī)約模式:為什么?
讓我們通過一個(gè)簡單的示例來了解使用規(guī)約模式的必要性。下面是Developer類的代碼片段,它具有Name、Email、Experience等所需的屬性。
public?class?Developer {public?int?Id?{?get;?set;?}public?string?Name?{?get;?set;?}public?string?Email?{?get;?set;?}public?int?YearsOfExperience?{get;set;}public?decimal?EstimatedIncome?{get;set;}public?int?Followers?{?get;?set;?} }現(xiàn)在,我們可能會有一個(gè)服務(wù)層,它通過像Entity Framework Core這樣的抽象從DB返回?cái)?shù)據(jù)集。這是它的樣子。
public?class?DeveloperService?:?IDeveloperService {private?readonly?ApplicationDbContext?_context;public?DeveloperService(ApplicationDbContext?context){_context?=?context;}public?async?Task<IEnumerable<Developer>>?GetDeveloperCount(){//?return?a?count?of?all?developers?in?the?database} }雖然您將獲得所有開發(fā)人員的數(shù)量,但更實(shí)際和合乎邏輯的要求是使用某種過濾器獲得開發(fā)人員的數(shù)量,同意嗎?例如,獲取估計(jì)收入為 100,000 美元或以上的開發(fā)人員的數(shù)量,或具有 5 年或以上經(jīng)驗(yàn)的開發(fā)人員的數(shù)量。可能性是無限的。
但是,這最終會讓您擁有大量的服務(wù)層函數(shù),例如 GetDeveloperCountWithSalariesGreaterThan(decimal minSalary)、GetDeveloperCountWithExperienceMoreThan(int minExp) 等等。需求越多,您最終擁有的功能數(shù)量就越多。如果您需要薪水高于 x 且經(jīng)驗(yàn)高于 y 年的開發(fā)人員數(shù)量怎么辦? ?這是另一個(gè)可能導(dǎo)致額外方法的挑戰(zhàn)。
您可能會爭辯說您可以將這些過濾器直接應(yīng)用于Entity Framework Core實(shí)體,例如
await?_context.Developers.Where(a=>a.Salary?>?10000?&&?a.Experience?>?6).ToListAsync()但是,不,這與您需要的干凈的應(yīng)用程序代碼庫相去甚遠(yuǎn)。這種方法最終會很快破壞應(yīng)用程序的可伸縮性,相信我,這根本無法維護(hù)。小提示,您的應(yīng)用程序中始終需要一個(gè)位于應(yīng)用程序和數(shù)據(jù)庫之間的服務(wù)層,并全權(quán)負(fù)責(zé)處理業(yè)務(wù)邏輯。
這是您的應(yīng)用程序需要使用規(guī)約模式的地方。注意,泛型倉儲模式有一些限制,這些限制是通過使用規(guī)約模式解決的。我們將建立一個(gè)項(xiàng)目,然后使用規(guī)約。
我們將建造什么
為了演示 ASP.NET Core 中的規(guī)約模式,我們將構(gòu)建一個(gè)具有2個(gè)端點(diǎn)的簡單Web API應(yīng)用程序:
返回特定的開發(fā)人員詳細(xì)信息
返回開發(fā)人員列表
但是,我們將添加泛型倉儲模式和工作單元的組合,使這個(gè)實(shí)現(xiàn)更加合乎邏輯和實(shí)用。我們將在這里專門識別和實(shí)現(xiàn)規(guī)約模式的用例。這幾乎是您使用 ASP.NET Core 5.0 構(gòu)建完整應(yīng)用程序時(shí)所需的一切。讓我們開始吧。
PS,你可以在這里找到這個(gè)實(shí)現(xiàn)的完整源代碼。
設(shè)置項(xiàng)目
首先,讓我們打開 Visual Studio 2019+ 并創(chuàng)建一個(gè)新的解決方案和一個(gè) WebAPI 項(xiàng)目。請注意,我們也將在此實(shí)現(xiàn)中遵循六邊形架構(gòu),以保持解決方案的良好組織。
添加API項(xiàng)目后,讓我們再向此解決方案添加2個(gè)類庫項(xiàng)目。我們稱之為Data和Core。
Data是與數(shù)據(jù)庫和上下文相關(guān)的所有實(shí)現(xiàn)所在的地方。
Core是我們將添加接口和域?qū)嶓w的地方。
這就是現(xiàn)階段解決方案的樣子。
添加所需的模型
如前所述,在Core項(xiàng)目中,創(chuàng)建一個(gè)名為Entities的新文件夾并向其中添加2個(gè)類,即Developer和Address。
public?class?Address {public?int?Id?{?get;?set;?}public?string?City?{?get;?set;?}public?string?Street?{?get;?set;?} } public?class?Developer {public?int?Id?{?get;?set;?}public?string?Name?{?get;?set;?}public?string?Email?{?get;?set;?}public?int?YearsOfExperience?{?get;?set;?}public?decimal?EstimatedIncome?{?get;?set;?}public?Address?Address?{?get;?set;?} }添加 DBContext 、Migrations和必需的包
現(xiàn)在,讓我們將所需的NuGet包安裝到相應(yīng)的項(xiàng)目中。
打開包管理器控制臺并從下拉列表中將Data項(xiàng)目設(shè)置為默認(rèn)項(xiàng)目。 運(yùn)行以下命令以安裝所需的軟件包。
Install-Package?Microsoft.EntityFrameworkCore Install-Package?Microsoft.EntityFrameworkCore.SqlServer Install-Package?Microsoft.EntityFrameworkCore.Tools接下來,將API項(xiàng)目設(shè)置為默認(rèn)項(xiàng)目,并運(yùn)行以下命令。
Install-Package?Microsoft.EntityFrameworkCore.Design在設(shè)置應(yīng)用程序上下文類之前,讓我們添加連接字符串。為此,從API項(xiàng)目打開 appsettings.json并添加以下內(nèi)容。
請注意,我們目前正在使用SQLServer Local DB進(jìn)行此演示。
"ConnectionStrings":?{"DefaultConnection":?"Data?Source=(localdb)\\mssqllocaldb;Initial?Catalog=specification-pattern-demo;Integrated?Security=True;MultipleActiveResultSets=True" },完成后,讓我們創(chuàng)建所需的上下文類,以幫助我們訪問數(shù)據(jù)庫。為此,在數(shù)據(jù)項(xiàng)目下,添加一個(gè)新類并將其命名為ApplicationDbContext。
public?class?ApplicationDbContext?:?DbContext {public?ApplicationDbContext(DbContextOptions?options)?:?base(options){}public?DbSet<Developer>?Developers?{?get;?set;?}public?DbSet<Address>?Addresses?{?get;?set;?} }在這里,您可以看到我們提到了要包含在 Application Db Context 中的 Developer 和 Address 類。
接下來,我們需要將此上下文添加到我們的ASP.NET Core應(yīng)用程序的服務(wù)容器并配置連接詳細(xì)信息。在API工程中打開Startup.cs,在ConfigureServices方法下添加如下內(nèi)容。
services.AddDbContext<ApplicationDbContext>(options?=>?options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));最后,我們準(zhǔn)備添加遷移并更新數(shù)據(jù)庫。再次打開包管理器控制臺并將Data項(xiàng)目設(shè)置為默認(rèn)項(xiàng)目。運(yùn)行以下命令:
add-migration?initial update-database這是演示相同內(nèi)容的屏幕截圖。請注意,您可能會收到有關(guān)上述小數(shù)屬性精度的警告。我們暫時(shí)可以忽略它。
完成后,我們的數(shù)據(jù)庫現(xiàn)在應(yīng)該準(zhǔn)備好了所需的表和相應(yīng)的字段。出于演示目的,我使用 Visual Studio 2019 IDE 的 SQL Server 對象資源管理器工具將一些示例數(shù)據(jù)直接添加到數(shù)據(jù)庫中。
實(shí)現(xiàn)泛型倉儲模式
由于我們的需求是返回開發(fā)人員的結(jié)果集,所以我們創(chuàng)建一個(gè)泛型倉儲模式,以便它可以使用 ApplicationDbContext 從數(shù)據(jù)庫中查詢數(shù)據(jù)。使用泛型倉儲模式的重要性在于,此代碼也可以重用于多個(gè)其他實(shí)體。
例如,我們稍后添加一個(gè)名為 Product 的新實(shí)體,您不一定需要添加用于從數(shù)據(jù)庫訪問 Product 數(shù)據(jù)的新類,但您可以在大多數(shù)用例中使用現(xiàn)有的泛型倉儲庫實(shí)現(xiàn)。請注意,我們將在本文后面的部分討論和解決泛型倉儲庫模式的一些限制。
在 Core 項(xiàng)目下,添加一個(gè)新文件夾并將其命名為 Interfaces。在這里,添加一個(gè)新接口IGenericRepository。
public?interface?IGenericRepository<T>?where?T:?class {Task<T>?GetByIdAsync(int?id);Task<List<T>>?GetAllAsync(); }創(chuàng)建泛型倉儲實(shí)現(xiàn)
現(xiàn)在,讓我們實(shí)現(xiàn)上面創(chuàng)建的接口。由于我們遵循六邊形/洋蔥架構(gòu),我們將不得不在應(yīng)用程序核心之外添加實(shí)現(xiàn)。這意味著,所有與數(shù)據(jù)相關(guān)的實(shí)現(xiàn)都將添加到數(shù)據(jù)項(xiàng)目中。
在這里,添加一個(gè)新類 GenericRepository。
public?class?GenericRepository<T>?:?IGenericRepository<T>?where?T?:?class {protected?readonly?ApplicationDbContext?_context;public?GenericRepository(ApplicationDbContext?context){_context?=?context;}public?async?Task<List<T>>?GetAllAsync(){return?await?_context.Set<T>().ToListAsync();}public?async?Task<T>?GetByIdAsync(int?id){return?await?_context.Set<T>().FindAsync();} }可以看到我們正在將 ApplicationDbContext 的實(shí)例注入到這個(gè)倉儲實(shí)現(xiàn)的構(gòu)造函數(shù)中。此實(shí)例進(jìn)一步用于從數(shù)據(jù)庫讀取數(shù)據(jù)。
最后在API工程的Startup.cs中添加如下內(nèi)容,將IGenericRepository接口注冊到應(yīng)用的服務(wù)容器中。
services.AddScoped(typeof(IGenericRepository<>),?(typeof(GenericRepository<>)));泛型倉儲模式的問題:反模式?
一些開發(fā)人員認(rèn)為泛型倉儲是一種反模式。如果使用不當(dāng),是的,任何模式都會弄亂您的代碼。對泛型倉儲的主要抱怨是單個(gè)方法可能會將整個(gè)數(shù)據(jù)庫訪問代碼暴露給用戶。這也可能意味著需要針對每種需求組合使用多種方法(如本文開頭所述)。例如,看下面的接口聲明:
List<T>?FindAsync(Expression<Func<T,?bool>>?query);此方法可以作為泛型倉儲模式的一部分來解決我們遇到的問題。但是由于該方法過于籠統(tǒng),泛型倉儲不可能知道我們傳遞給它的表達(dá)式。另一個(gè)想法可能是從 IGenericRepository 接口中刪除此方法并在新接口中使用它,例如,從 IGenericRepository 派生的 IDeveloperRepository。這可能會奏效,但考慮到未來實(shí)體的添加和需求的變化,這種變化不是一個(gè)明智的選擇。
想象一下有 20-30 個(gè)新實(shí)體并且必須創(chuàng)建大量新倉儲?不是個(gè)好主意,是嗎?考慮在 IDevloperRepository 及其實(shí)現(xiàn)中具有多種方法,例如 GetDevelopersWithSalariesGreaterThan(decimal salary)和 GetDevelopersWithExperienceLessThan(int years),不簡潔,是嗎?
如果有更簡潔的方法來解決這個(gè)需求呢?這正是規(guī)約模式派上用場的地方。
在 ASP.NET Core 中使用規(guī)約模式增強(qiáng)倉儲模式
規(guī)約模式乍一看可能會覺得很復(fù)雜。我也感覺到了。但是,一旦您添加了某些基類和評估器,您所要做的就是創(chuàng)建規(guī)約類,根據(jù)您的要求,這些類通常為 2 到 10 行。讓我們開始使用 ASP.NET Core 中的規(guī)約模式。
在 Core 項(xiàng)目下,添加一個(gè)新文件夾并將其命名為 Specifications。這是所有與規(guī)約相關(guān)的接口都要去的地方。
創(chuàng)建一個(gè)新接口并將其命名為 ISpecification.cs
public?interface?ISpecification<T> {Expression<Func<T,?bool>>?Criteria?{?get;?}List<Expression<Func<T,?object>>>?Includes?{?get;?}Expression<Func<T,?object>>?OrderBy?{?get;?}Expression<Func<T,?object>>?OrderByDescending?{?get;?} }這只是一個(gè)最小的實(shí)現(xiàn)。讓我解釋每個(gè)聲明的方法定義。
Criteria - 您可以在此處添加基于實(shí)體的表達(dá)式。
Includes – 如果要包含外鍵表數(shù)據(jù),可以使用此方法添加它。
OrderBy 和 OrderByDescending 是不言自明的。
接下來,在同一文件夾中,添加一個(gè)新類 BaseSpecifcation。這將是 ISpecification 接口的實(shí)現(xiàn)。
public?class?BaseSpecifcation<T>?:?ISpecification<T> {public?BaseSpecifcation(){}public?BaseSpecifcation(Expression<Func<T,?bool>>?criteria){Criteria?=?criteria;}public?Expression<Func<T,?bool>>?Criteria?{?get;?}public?List<Expression<Func<T,?object>>>?Includes?{?get;?}?=?new?List<Expression<Func<T,?object>>>();public?Expression<Func<T,?object>>?OrderBy?{?get;?private?set;?}public?Expression<Func<T,?object>>?OrderByDescending?{?get;?private?set;?}protected?void?AddInclude(Expression<Func<T,?object>>?includeExpression){Includes.Add(includeExpression);}protected?void?AddOrderBy(Expression<Func<T,?object>>?orderByExpression){OrderBy?=?orderByExpression;}protected?void?AddOrderByDescending(Expression<Func<T,?object>>?orderByDescExpression){OrderByDescending?=?orderByDescExpression;} }在這里,我們將添加3個(gè)基本方法和一個(gè)構(gòu)造函數(shù)。
將表達(dá)式添加到 Includes 屬性
將表達(dá)式添加到 OrderBy 屬性
將表達(dá)式添加到 OrderByDescending 屬性
您可以注意到我們還有一個(gè)接受條件的構(gòu)造函數(shù)。Criteria 可以是 ( x=>x.Salary > 100 ) ?等。你明白了,是嗎?
升級泛型倉儲
首先,讓我們在 IGenericRepository 接口中添加一個(gè)方法。
IEnumerable<T>?FindWithSpecificationPattern(ISpecification<T>?specification?=?null);接下來,讓我們在 GenericRepository 類中實(shí)現(xiàn)新方法。
public?IEnumerable<T>?FindWithSpecificationPattern(ISpecification<T>?specification?=?null) {return?SpecificationEvaluator<T>.GetQuery(_context.Set<T>().AsQueryable(),?specification); }現(xiàn)在,設(shè)置所有這些背后的想法是創(chuàng)建可以返回特定結(jié)果集的單獨(dú)規(guī)約類。這些新規(guī)約類中的每一個(gè)都將從 BaseSpecification 類繼承。明白了嗎?現(xiàn)在讓我們創(chuàng)建這些規(guī)約類,以便它有意義 ????
因此,讓我們得出 2 個(gè)要求/規(guī)約:
1.按薪水降序返回開發(fā)人員列表的規(guī)約。
2.另一個(gè)規(guī)約返回具有 N 或以上經(jīng)驗(yàn)的開發(fā)人員列表及其地址。
在 Core 項(xiàng)目的同一個(gè) Specification 文件夾下,添加我們的第一個(gè)規(guī)約類 DeveloperByIncomeSpecification
public?class?DeveloperByIncomeSpecification?:?BaseSpecifcation<Developer> {public?DeveloperByIncomeSpecification(){????????????AddOrderByDescending(x?=>?x.EstimatedIncome);} }在這里,您可以看到我們從 BaseSpecification 類派生并在構(gòu)造函數(shù)中使用 AddOrderByDescending 方法。理想情況下,此規(guī)約將返回一個(gè)按收入遞減順序排列的開發(fā)人員列表。
接下來,讓我們添加另一個(gè)類,DeveloperWithAddressSpecification
public?class?DeveloperWithAddressSpecification?:?BaseSpecifcation<Developer> {public?DeveloperWithAddressSpecification(int?years)?:?base(x=>x.EstimatedIncome?>?years){AddInclude(x?=>?x.Address);} }因此,這里我們將查詢表達(dá)式傳遞給 Specification Class 的基類,它是 BaseSpecification 的構(gòu)造函數(shù),然后將其添加到我們之前創(chuàng)建的 Criteria 屬性中。其實(shí)很簡單。
現(xiàn)在,隨著我們的規(guī)約類準(zhǔn)備就緒,讓我們添加 api 端點(diǎn)。
在 API 項(xiàng)目下,在 Controllers 文件夾下添加一個(gè)新的 API Controller,并將其命名為 DevelopersController。
public?class?DevelopersController?:?ControllerBase {public?readonly?IGenericRepository<Developer>?_repository;public?DevelopersController(IGenericRepository<Developer>?repository){_repository?=?repository;}[HttpGet]public?async?Task<IActionResult>?GetAll(){var?developers?=?await?_repository.GetAllAsync();return?Ok(developers);}[HttpGet("{id}")]public?async?Task<IActionResult>?GetById(int?id){var?developer?=?await?_repository.GetByIdAsync(id);return?Ok(developer);}[HttpGet("specify")]public?async?Task<IActionResult>?Specify(){var?specification?=?new?DeveloperWithAddressSpecification(3);//var?specification?=?new?DeveloperByIncomeSpecification();var?developers?=?_repository.FindWithSpecificationPattern(specification);return?Ok(developers);} }第 3 – 7 行:將 IGenericRepository 注入到 Controller 的構(gòu)造函數(shù)中。第 8 – 19 行:使用倉儲實(shí)例返回所有開發(fā)人員和具有特定 Id 的開發(fā)人員的標(biāo)準(zhǔn)端點(diǎn)。
第 20 – 27 行:這是控制器最有趣的部分。這里的第 23 行和第 24 行是我們之前創(chuàng)建的 2 個(gè)規(guī)約類。這只是為了證明可以在控制器或使用 GenericRepository 的任何地方創(chuàng)建任何此類規(guī)約實(shí)例。我們將使用 DeveloperWithAddressSpecification(3) 進(jìn)行演示。
現(xiàn)在讓我們運(yùn)行應(yīng)用程序并檢查指定端點(diǎn)的結(jié)果。
可以看到還返回了地址數(shù)據(jù)。現(xiàn)在,回到控制器,注釋掉第 24 行,讓我們暫時(shí)使用 DeveloperByIncomeSpecification。再次運(yùn)行應(yīng)用程序。
現(xiàn)在您可以注意到?jīng)]有返回地址數(shù)據(jù)。為什么?很簡單,因?yàn)槲覀兪褂昧瞬煌囊?guī)約,沒有提到添加 Address 實(shí)體。相反,該規(guī)約按收入的遞減順序返回開發(fā)人員的集合。簡單,但整潔對嗎?這可能是 ASP.NET Core 應(yīng)用程序中最酷的設(shè)計(jì)模式之一。
很奇怪,但這實(shí)際上是您可以理解規(guī)約模式是什么的時(shí)候???? 根據(jù)維基百科 - 在計(jì)算機(jī)編程中,規(guī)約模式是一種特定的軟件設(shè)計(jì)模式,其中可以通過使用布爾邏輯將業(yè)務(wù)規(guī)則鏈接在一起來重新組合業(yè)務(wù)規(guī)則。該模式經(jīng)常用于領(lǐng)域驅(qū)動設(shè)計(jì)的上下文中。
現(xiàn)在更有意義了,是嗎?業(yè)務(wù)規(guī)則(我們要求返回具有一定經(jīng)驗(yàn)水平或更高級別的開發(fā)人員)通過鏈接標(biāo)準(zhǔn)(這發(fā)生在 DeveloperWithAddressSpecification 類中)組合在一起,這是一個(gè)布爾邏輯。很簡單,但是太強(qiáng)大了????
展望未來,這種模式的可能性是無窮無盡的,并且非常有助于擴(kuò)展應(yīng)用程序。這種模式也可能支持Data-Shaping和分頁。非常強(qiáng)大的模式,學(xué)習(xí)曲線很小,是嗎?這是這篇文章的總結(jié)。
總結(jié)
在本文中,我們介紹了 ASP.NET Core 應(yīng)用程序中的規(guī)約模式,以及它如何通過占上風(fēng)來增強(qiáng)泛型倉儲模式。我們還構(gòu)建了一個(gè)完整的 Web API 應(yīng)用程序,該應(yīng)用程序遵循洋蔥架構(gòu)以進(jìn)行干凈的代碼管理。你也可以在我的 Github 上找到完整的源代碼。有任何建議或問題嗎?請隨時(shí)將它們留在下面的評論部分。Thanks and Happy Coding!????
歡迎關(guān)注我的個(gè)人公眾號”My IO“
參考資料
[1]
完整源代碼: https://github.com/iammukeshm/specification-pattern-asp-net-core
總結(jié)
以上是生活随笔為你收集整理的ASP.NET Core 中的规约模式(Specification Pattern )——增强泛型仓储模式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 五个 .NET 性能小贴士
- 下一篇: 在 .NET 中创建对象的几种方式的对比