关键词:
原文链接: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.netcoremvc的设计模式(代码片段)
ASP.NETCoreMVC1、MVC2、MVC如何工作2、在ASP.NETCore中安装MVC3、UseMvcWithDefaultRoute()4、AddMvc和AddMvcCore是什么关系5、ASP.NETCoreMVC中的Model模型6、ASP.NETCore中的依赖注入介绍7、ASP.NETCoreMVC中的控制器1、MVC模型(Model),视图(View)、控制器( 查看详情
在生产模式下删除 ASP.NET Core 2.0 中的控制台和调试记录器
】在生产模式下删除ASP.NETCore2.0中的控制台和调试记录器【英文标题】:RemoveconsoleanddebugloggersinASP.NETCore2.0wheninproductionmode【发布时间】:2018-02-0917:00:45【问题描述】:在ASP.NETCore2.0中我们有这个publicstaticIWebHostBuildWebHost(string[]args)... 查看详情
asp.netcore在iis下的两种部署模式
...relServer最大的优势体现在它的跨平台的能力,如果ASP.NETCORE应用只需要部署在Windows环境下,IIS也是不错的选择。ASP.NETCORE应用针对IIS具有两种部署模式,它们都依赖于一个IIS针对ASP.NETCORECore的扩展模块。一、ASP.NETCORECo... 查看详情
iis在asp.netcore下的两种部署模式(代码片段)
KestrelServer最大的优势体现在它的跨平台的能力,如果ASP.NETCORE应用只需要部署在Windows环境下,IIS也是不错的选择。ASP.NETCORE应用针对IIS具有两种部署模式,它们都依赖于一个IIS针对ASP.NETCORECore的扩展模块。KestrelServer最大的优势... 查看详情
asp.netcore依赖注入基本用法(代码片段)
ASP.NETCore依赖注入ASP.NETCore从框架层对依赖注入提供支持。也就是说,如果你不了解依赖注入,将很难适应ASP.NETCore的开发模式。本文将介绍依赖注入的基本概念,并结合代码演示如何在ASP.NETCore中使用依赖注入。什么是依赖注入... 查看详情
用工厂模式解决asp.netcore中依赖注入的一个烦恼
这是最近在实际开发中遇到的一个问题,用asp.netcore开发一个后端webapi,根据指定的key清除2台memcached服务器上的缓存。背景是我们在进行.netcore迁移工作,asp.net项目与asp.netcore项目并存,为了避免两种类型项目的缓存冲突,我们... 查看详情
详解asp.netcore中的cookies(代码片段)
目录详解Asp.NetCore中的cookies搞懂cookiesAsp.Net中cookies的实现从http中获取cookies将cookies写入http中总结及感想详解Asp.NetCore中的cookies搞懂cookies我之前写过一篇文章来介绍cookies,如果你对cookies不是很了解请移步理解cookies这篇文章,这... 查看详情
asp.netcore中的缓存[1]:如何在一个asp.netcore应用中使用缓存
.NETCore针对缓存提供了很好的支持,我们不仅可以选择将数据缓存在应用进程自身的内存中,还可以采用分布式的形式将缓存数据存储在一个“中心数据库”中。对于分布式缓存,.NETCore提供了针对Redis和SQLServer的原生支持... 查看详情
理解asp.netcore:处理管道(代码片段)
理解ASP.NETCore处理管道在ASP.NETCore的管道处理部分,实现思想已经不是传统的面向对象模式,而是切换到了函数式编程模式。这导致代码的逻辑大大简化,但是,对于熟悉面向对象编程,而不是函数式编程思路的开发者来说,是... 查看详情
理解asp.netcore
注:本文隶属于《理解ASP.NETCore》系列文章,请查看置顶博客或点击此处查看全文目录之前,我们已经了解了ASP.NETCore中的身份认证,现在,我们来聊一下授权。老规矩,示例程序源码XXTk.Auth.Samples已经提交了,需要的请自取。概... 查看详情
asp.net与.netcore的语法相同吗
asp.net与netcore的语法是不相同的,是有本质性能的区别的参考技术A1、asp.netcore是跨平台的,你可以在linux,mac,windows上编写编译代码。只需要安装.netcore和visualstudiocode(或visualstudio)就可以编写。asp.netcore是未来发展方向吧,构建... 查看详情
asp.netcore2.0中的httpcontext
ASP.NETCore2.0中的HttpContexthttps://blog.csdn.net/weixin_34174322/article/details/87012345 将Net项目升级Core项目经验:(二)修复迁移后NetStandard项目中的错误https://www.colabug.com/2742683.htmlNETCore中怎么使用HttpContext.Cur 查看详情
[十一]asp.netcore中的taghelper(代码片段)
Taghelper一、ASP.NETCore中的Taghelper(1)导入内置TagHelpers(2)使用Taghelper生成链接(3)为什么要使用Taghelper(标记助手)(4)ASP.NETCoreImage标记助手(TagHelper)(5)A 查看详情
abp框架-规约
...辑可以使用boolean逻辑重新链接业务逻辑(维基百科).实践中的大部分情况,它是为实体或其它业务对象,定义可复用的过滤器. 示例在此小节,我们将看到规约模式的必要性,这节是通用的,与ABP的实现无关.假设你有一个服务方法用... 查看详情
《asp.netcore6框架揭秘》实例演示[10]:options基本编程模式
...#xff0c;这篇文章演示几种典型的编程模式。[本文节选《ASP.NETCore6框架揭秘》第6章][601]将配置绑定为Options对象(源代码 查看详情
创建第一个asp.netcore程序使用dbfirst模式连接数据库
第一步:第二步:第三步:第四步:在控制台输入命令 (1)Install-Package Microsoft.EntityFrameworkCore (2)Install-Package Microsoft.EntityFrameworkCore.SqlServer(3)Install-Package Microsoft.EntityFramewor 查看详情
Asp.net Core中的多对多实现问题[重复]
】Asp.netCore中的多对多实现问题[重复]【英文标题】:ManytoManyimplementationprobleminAsp.netCore[duplicate]【发布时间】:2021-09-1902:55:32【问题描述】:我首先使用代码。我正在尝试使用.Net6在Asp.net核心中实现多对多关系。我的订单模式:.... 查看详情
《asp.netcore6框架揭秘》实例演示[13]:日志的基本编程模式
《ASP.NETCore6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式》介绍了四种常用的诊断日志框架。其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net、NLog和Serilog等。虽然这些... 查看详情