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