EF Core规范模式:重构混乱查询逻辑的设计范式与实践
你是否遇到过这样的场景:一个看似简单的列表查询,随着业务发展,查询条件越来越多,Where子句像滚雪球一样膨胀,最终变成一个几百行的“巨无霸”方法?或者,同样的过滤逻辑(如“只查询已发布且对当前用户可见的文章”)在控制器、服务层、仓储层被重复编写,稍一改动就牵一发而动全身?
这不是架构问题,而是查询逻辑的组织问题。在 EF Core 项目中,我们花费大量时间编写DbContext和 LINQ,却很少系统性地思考如何让查询代码像业务逻辑一样清晰、可复用、易测试。结果就是,项目中期开始,数据访问层逐渐变得难以维护,成为滋生 Bug 和性能问题的温床。
本文要解决的核心痛点正在于此。我们将深入探讨如何运用规范模式来“清理”你的 EF Core 查询。这不是简单的代码美化,而是一种从根本上提升数据访问代码质量的设计范式。你将看到,通过将查询条件封装为独立、可组合的“规范”对象,那些散落各处、重复混乱的 LINQ 代码将变得井然有序。
读完本文,你将能清晰地回答:规范模式是什么?为什么它比直接在服务层写 LINQ 更好?如何一步步在 EF Core 中实现它?以及,在真实项目中应用时有哪些必须注意的“坑”和最佳实践?让我们开始这场代码清理之旅。
1. 这篇文章真正要解决的问题:混乱的查询逻辑
在典型的 ASP.NET Core 三层架构中,数据访问逻辑的归宿往往是模糊的。很多开发者会不假思索地将 LINQ 查询写在服务层甚至控制器中:
// 反例:查询逻辑侵入服务层 public class ArticleService { private readonly AppDbContext _context; public async Task<List<Article>> GetPublishedArticlesForUser(int userId, DateTime? fromDate) { var query = _context.Articles .Where(a => a.Status == ArticleStatus.Published) .Where(a => a.IsDeleted == false) .Where(a => a.AuthorId == userId || a.Visibility == Visibility.Public); if (fromDate.HasValue) { query = query.Where(a => a.PublishDate >= fromDate.Value); } return await query.OrderByDescending(a => a.PublishDate).ToListAsync(); } }这段代码功能上没问题,但它隐藏了四个严重隐患:
- 不可复用:
已发布、未删除、对用户可见这个核心业务规则,可能在GetArticlesForDashboard、GetArticlesForRssFeed等方法中再次出现。一旦规则变化(例如增加“审核通过”状态),你需要修改所有地方。 - 难以测试:要单元测试这个服务方法,你必须构建一个完整的
DbSet<Article>模拟,或者直接操作内存数据库,测试成本很高。 - 职责混杂:服务层本应关注业务协调和流程,现在却充斥着数据过滤的具体细节。
- 阻碍优化:当你想为这个复杂的组合条件添加数据库索引,或者分析其执行计划时,发现查询逻辑分散在各处,难以统一分析和优化。
规范模式要解决的,正是这种“查询逻辑无处安放”的架构尴尬。它将查询条件提升为一等公民,封装成可独立定义、测试、组合和复用的对象,从而让数据访问层像业务层一样清晰、健壮。
2. 基础概念:什么是规范模式?
规范模式源于领域驱动设计中的一个概念:规格。其核心思想是,将业务规则(特别是用于选择和验证的规则)封装成一个独立的、可测试的谓词对象。
在 EF Core 的上下文中,一个“规范”就是一个定义了Where条件的对象。但它不止于此,一个完整的规范通常还可以包含:
Include(贪婪加载)OrderBy/ThenBy(排序)Skip/Take(分页)- 甚至投影(
Select)
你可以把它想象成LINQ 查询的乐高积木。每个积木(规范)代表一个明确的业务意图(如“已发布”、“属于某个用户”)。你可以单独使用它们,也可以轻松地将它们拼接起来,构建出复杂的查询,而无需重复编写或复制粘贴 LINQ 表达式。
与仓储模式的对比: 很多项目用仓储模式来封装数据访问。基础仓储(IRepository<T>)提供了GetById,GetAll,Add,Update等通用操作。这很好,但它没有解决“复杂查询”的问题。你可能会在仓储中定义无数个像GetPublishedArticlesByUser这样的具体方法,导致仓储接口爆炸。规范模式与仓储模式是互补的。一个良好的实践是:基础仓储处理简单的 CRUD,而规范则负责描述复杂的、动态的查询条件,并通过一个通用的ApplySpecification方法应用到查询上。
3. 环境准备与前置条件
在开始实现之前,请确保你的开发环境已就绪。
开发环境要求:
- IDE/编辑器:Visual Studio 2022+ 或 JetBrains Rider 或 VS Code。
- .NET SDK:.NET 6.0, .NET 7.0 或 .NET 8.0。本文示例基于 .NET 8,但核心概念适用于所有支持 EF Core 的版本。
- 数据库:SQL Server / PostgreSQL / MySQL / SQLite 等均可。EF Core 是数据库提供程序无关的。
项目与依赖:创建一个新的 ASP.NET Core Web API 项目或使用现有项目。通过 NuGet 包管理器或 CLI 安装必要的依赖:
# 在你的项目目录下执行 dotnet add package Microsoft.EntityFrameworkCore.SqlServer # 以 SQL Server 为例 dotnet add package Microsoft.EntityFrameworkCore.Tools # 用于迁移(可选)示例领域模型:为了后续演示,我们定义一个简单的博客领域模型:
// 文件路径:Models/Article.cs namespace CleanEfCoreQuery.Models; public class Article { public int Id { get; set; } public string Title { get; set; } = string.Empty; public string Content { get; set; } = string.Empty; public ArticleStatus Status { get; set; } public DateTime PublishDate { get; set; } public bool IsDeleted { get; set; } public Visibility Visibility { get; set; } public int AuthorId { get; set; } public User Author { get; set; } = null!; // 导航属性 public List<Comment> Comments { get; set; } = new(); } public enum ArticleStatus { Draft, Published, Archived } public enum Visibility { Private, Public } // 文件路径:Models/User.cs public class User { public int Id { get; set; } public string Name { get; set; } = string.Empty; public List<Article> Articles { get; set; } = new(); } // 文件路径:Models/Comment.cs public class Comment { public int Id { get; set; } public string Text { get; set; } = string.Empty; public int ArticleId { get; set; } public Article Article { get; set; } = null!; }对应的AppDbContext请自行配置。准备好这些,我们就可以开始构建规范模式的核心了。
4. 核心构建:定义规范接口与基类
规范模式的核心是接口。我们首先定义一个最基础的规范接口,它只做一件事:将一个 LINQ 表达式应用到IQueryable<T>上。
// 文件路径:Specifications/ISpecification.cs namespace CleanEfCoreQuery.Specifications; public interface ISpecification<T> { // 核心:定义查询的过滤条件 Expression<Func<T, bool>>? Criteria { get; } // 定义需要贪婪加载的导航属性 List<Expression<Func<T, object>>> Includes { get; } // 定义排序规则 List<Expression<Func<T, object>>> OrderByExpressions { get; } List<Expression<Func<T, object>>> OrderByDescendingExpressions { get; } // 定义分页 int? Take { get; } int? Skip { get; } }这个接口定义了规范的“能力”。但直接实现这个接口会很繁琐,我们需要一个基类来提供通用的属性和构建方法。
// 文件路径:Specifications/BaseSpecification.cs namespace CleanEfCoreQuery.Specifications; public abstract class BaseSpecification<T> : ISpecification<T> { // 实现接口属性 public Expression<Func<T, bool>>? Criteria { get; private set; } public List<Expression<Func<T, object>>> Includes { get; } = new(); public List<Expression<Func<T, object>>> OrderByExpressions { get; } = new(); public List<Expression<Func<T, object>>> OrderByDescendingExpressions { get; } = new(); public int? Take { get; private set; } public int? Skip { get; private set; } // 受保护的构建方法,供子类在构造函数中调用 protected void ApplyCriteria(Expression<Func<T, bool>> criteria) { Criteria = criteria; } protected void AddInclude(Expression<Func<T, object>> includeExpression) { Includes.Add(includeExpression); } protected void AddOrderBy(Expression<Func<T, object>> orderByExpression) { OrderByExpressions.Add(orderByExpression); } protected void AddOrderByDescending(Expression<Func<T, object>> orderByDescendingExpression) { OrderByDescendingExpressions.Add(orderByDescendingExpression); } protected void ApplyPaging(int skip, int take) { Skip = skip; Take = take; } }现在,创建新规范变得非常简单:继承BaseSpecification<T>,并在构造函数中通过调用基类方法定义规则。
5. 实现规范:从简单到复杂
让我们用具体的业务需求来演示如何创建规范。
示例1:一个简单的“已发布文章”规范
// 文件路径:Specifications/Articles/PublishedArticlesSpecification.cs namespace CleanEfCoreQuery.Specifications.Articles; public class PublishedArticlesSpecification : BaseSpecification<Article> { public PublishedArticlesSpecification() { // 定义核心过滤条件 ApplyCriteria(a => a.Status == ArticleStatus.Published && !a.IsDeleted); // 定义默认排序 AddOrderByDescending(a => a.PublishDate); } }示例2:一个更复杂的“用户可见文章”规范这个规范需要接收外部参数(当前用户ID),并组合更复杂的逻辑。
// 文件路径:Specifications/Articles/ArticlesVisibleToUserSpecification.cs namespace CleanEfCoreQuery.Specifications.Articles; public class ArticlesVisibleToUserSpecification : BaseSpecification<Article> { public ArticlesVisibleToUserSpecification(int currentUserId) { // 复合条件:已发布、未删除,并且(作者是当前用户 或 文章是公开的) ApplyCriteria(a => a.Status == ArticleStatus.Published && !a.IsDeleted && (a.AuthorId == currentUserId || a.Visibility == Visibility.Public)); // 包含作者信息 AddInclude(a => a.Author); // 包含评论列表 AddInclude(a => a.Comments); AddOrderByDescending(a => a.PublishDate); } }示例3:一个可复用的“分页”规范分页是一个横切关注点,可以单独抽离。
// 文件路径:Specifications/Common/PaginationSpecification.cs namespace CleanEfCoreQuery.Specifications.Common; public class PaginationSpecification<T> : BaseSpecification<T> { public PaginationSpecification(int pageNumber, int pageSize) { // 计算 Skip 和 Take ApplyPaging((pageNumber - 1) * pageSize, pageSize); } }关键点:每个规范类都代表一个明确的、可测试的、单一的业务意图。PublishedArticlesSpecification的意图就是“获取已发布的文章”,我们可以为其编写独立的单元测试,验证其Criteria是否正确。
6. 应用规范:构建规约评估器
定义了规范之后,我们需要一个工具,能将规范应用到 EF Core 的DbSet上,生成最终的IQueryable<T>。这个工具通常称为SpecificationEvaluator或规约评估器。
// 文件路径:Specifications/Evaluators/SpecificationEvaluator.cs namespace CleanEfCoreQuery.Specifications.Evaluators; public class SpecificationEvaluator { public static IQueryable<T> GetQuery<T>( IQueryable<T> inputQuery, ISpecification<T> specification) where T : class { var query = inputQuery; // 1. 应用过滤条件 (Where) if (specification.Criteria != null) { query = query.Where(specification.Criteria); } // 2. 应用贪婪加载 (Include) // 注意:Include 需要处理多层嵌套,这里简化处理 query = specification.Includes .Aggregate(query, (current, include) => current.Include(include)); // 3. 应用排序 (OrderBy) if (specification.OrderByExpressions.Any()) { IOrderedQueryable<T>? orderedQuery = null; var firstOrderBy = specification.OrderByExpressions.First(); orderedQuery = query.OrderBy(firstOrderBy); foreach (var orderBy in specification.OrderByExpressions.Skip(1)) { orderedQuery = orderedQuery.ThenBy(orderBy); } query = orderedQuery ?? query; } // 4. 应用降序排序 (OrderByDescending) - 处理逻辑类似,略 // ... 实际代码需要处理 OrderBy 和 OrderByDescending 的混合与优先级 // 5. 应用分页 (Skip/Take) if (specification.Skip.HasValue) { query = query.Skip(specification.Skip.Value); } if (specification.Take.HasValue) { query = query.Take(specification.Take.Value); } return query; } }这个评估器是连接规范与 EF Core 的桥梁。它接收一个初始的IQueryable<T>和一个规范对象,然后按顺序应用规范中定义的所有操作,返回一个新的IQueryable<T>。这里的关键是,它返回的仍然是IQueryable,这意味着查询还没有真正执行,EF Core 的延迟加载特性得以保留,我们还可以继续组合其他操作。
7. 集成到仓储层:通用规约仓储
现在,我们将规范模式与仓储模式结合。我们创建一个通用的Repository<T>,并为其添加一个能接受规范的方法。
// 文件路径:Data/IGenericRepository.cs namespace CleanEfCoreQuery.Data; public interface IGenericRepository<T> where T : class { Task<T?> GetByIdAsync(int id); Task<IReadOnlyList<T>> ListAllAsync(); // 核心方法:根据规约获取列表 Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec); // 核心方法:根据规约获取单个实体(或默认值) Task<T?> GetFirstOrDefaultAsync(ISpecification<T> spec); Task<T> AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entity); }// 文件路径:Data/GenericRepository.cs using CleanEfCoreQuery.Specifications; using CleanEfCoreQuery.Specifications.Evaluators; using Microsoft.EntityFrameworkCore; namespace CleanEfCoreQuery.Data; public class GenericRepository<T> : IGenericRepository<T> where T : class { protected readonly AppDbContext _context; public GenericRepository(AppDbContext context) { _context = context; } public virtual async Task<T?> GetByIdAsync(int id) { return await _context.Set<T>().FindAsync(id); } public virtual async Task<IReadOnlyList<T>> ListAllAsync() { return await _context.Set<T>().ToListAsync(); } // 使用规约的核心方法 public virtual async Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec) { // 1. 获取基础查询(DbSet) var query = _context.Set<T>().AsQueryable(); // 2. 应用规约 var evaluatedQuery = SpecificationEvaluator.GetQuery(query, spec); // 3. 执行查询 return await evaluatedQuery.ToListAsync(); } public virtual async Task<T?> GetFirstOrDefaultAsync(ISpecification<T> spec) { var query = _context.Set<T>().AsQueryable(); var evaluatedQuery = SpecificationEvaluator.GetQuery(query, spec); return await evaluatedQuery.FirstOrDefaultAsync(); } // ... 其他 CRUD 方法 }8. 完整示例:在服务层中使用规约
让我们回到开头的ArticleService,看看如何使用规约来重构它。
重构后的服务层:
// 文件路径:Services/ArticleService.cs namespace CleanEfCoreQuery.Services; public class ArticleService { private readonly IGenericRepository<Article> _articleRepository; public ArticleService(IGenericRepository<Article> articleRepository) { _articleRepository = articleRepository; } public async Task<List<Article>> GetPublishedArticlesForUser(int userId, DateTime? fromDate) { // 1. 创建核心业务规约 var spec = new ArticlesVisibleToUserSpecification(userId); // 2. (可选)动态组合额外的过滤条件 // 如果业务允许,可以创建一个可组合的规约,这里演示另一种思路:创建新规约 if (fromDate.HasValue) { // 假设我们有一个更灵活的规约构建器,这里为演示,我们创建一个新的组合规约 // 在实际项目中,你可能会使用“组合规约”模式(AndSpecification, OrSpecification) var finalSpec = new ArticlesVisibleToUserWithDateFilterSpecification(userId, fromDate.Value); return await _articleRepository.ListAsync(finalSpec); } // 3. 执行查询 return (await _articleRepository.ListAsync(spec)).ToList(); } } // 文件路径:Specifications/Articles/ArticlesVisibleToUserWithDateFilterSpecification.cs // 演示如何通过继承或组合来创建更具体的规约 public class ArticlesVisibleToUserWithDateFilterSpecification : ArticlesVisibleToUserSpecification { public ArticlesVisibleToUserWithDateFilterSpecification(int currentUserId, DateTime fromDate) : base(currentUserId) // 调用基类构造,复用基础条件 { // 获取基类已有的条件表达式 var baseCriteria = Criteria; // 组合新的条件:在原有条件上追加日期过滤 // 注意:这里需要处理表达式树的组合,是进阶话题。 // 更简单的做法是重写整个 Criteria,或者使用专门的组合规约类。 // 以下为概念性代码,实际实现更复杂: // Expression<Func<Article, bool>> dateFilter = a => a.PublishDate >= fromDate; // Criteria = baseCriteria.And(dateFilter); // 需要 ExpressionExtensions } }更优雅的组合方式:组合规约对于动态条件,更好的方式是实现AndSpecification<T>和OrSpecification<T>。
// 文件路径:Specifications/Composite/AndSpecification.cs public class AndSpecification<T> : BaseSpecification<T> { public AndSpecification(ISpecification<T> left, ISpecification<T> right) { // 使用 ExpressionVisitor 或第三方库(如 LinqKit)来组合表达式树 // 这是一个简化示例,实际组合逻辑较复杂 // var parameter = Expression.Parameter(typeof(T), “x”); // var combinedExpr = Expression.AndAlso( // Expression.Invoke(left.Criteria, parameter), // Expression.Invoke(right.Criteria, parameter)); // Criteria = Expression.Lambda<Func<T, bool>>(combinedExpr, parameter); } }在服务层中,你可以这样使用:
var visibleSpec = new ArticlesVisibleToUserSpecification(userId); var dateSpec = new ArticlesPublishedAfterSpecification(fromDate.Value); var combinedSpec = new AndSpecification<Article>(visibleSpec, dateSpec); var results = await _articleRepository.ListAsync(combinedSpec);9. 运行结果与效果验证
假设数据库中有以下数据:
| Id | Title | Status | AuthorId | Visibility | PublishDate | IsDeleted |
|---|---|---|---|---|---|---|
| 1 | 文章A | Published | 1 | Public | 2024-01-01 | false |
| 2 | 文章B | Draft | 1 | Private | 2024-01-02 | false |
| 3 | 文章C | Published | 2 | Public | 2024-01-03 | false |
| 4 | 文章D | Published | 2 | Private | 2024-01-04 | false |
当currentUserId = 2时,调用ArticlesVisibleToUserSpecification:
- 生成的SQL(概念):
SELECT * FROM Articles WHERE Status = 1 AND IsDeleted = 0 AND (AuthorId = 2 OR Visibility = 1) ORDER BY PublishDate DESC - 返回结果:文章C(作者是2,已发布,公开),文章D(作者是2,已发布,私有)。文章A(作者是1)对用户2不可见,因为它是私有的。
- 包含导航属性:查询会自动
LEFT JOIN到Users表和Comments表,填充Author和Comments属性,避免了 N+1 查询问题。
验证方式:
- 单元测试:你可以直接对规约类进行单元测试,验证其
Criteria表达式树是否正确,无需依赖数据库。[Fact] public void PublishedArticlesSpecification_Criteria_IsCorrect() { var spec = new PublishedArticlesSpecification(); var articlePublished = new Article { Status = ArticleStatus.Published, IsDeleted = false }; var articleDraft = new Article { Status = ArticleStatus.Draft, IsDeleted = false }; var func = spec.Criteria!.Compile(); // 将表达式树编译为委托 Assert.True(func(articlePublished)); Assert.False(func(articleDraft)); } - 集成测试:在内存数据库或测试数据库中,使用
GenericRepository和规约执行查询,验证返回的数据是否符合业务预期。 - 日志查看:在开发环境启用 EF Core 的敏感数据日志,查看实际生成的 SQL 语句,确认
Include和Where条件是否正确应用。
10. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 查询结果为空,但数据库有数据 | 1. 规约的Criteria表达式逻辑错误。2. 多个 Criteria在组合时逻辑关系(AND/OR)错误。3. 导航属性未正确加载,导致过滤条件依赖的导航属性为 null。 | 1. 单元测试规约的Criteria。2. 输出规约的 Criteria.ToString()查看表达式树。3. 检查 Includes列表是否包含了过滤条件所需的导航属性。 | 1. 修正业务逻辑。 2. 使用 LinqKit等库进行可靠的表达式组合。3. 在规约中添加必要的 AddInclude。 |
出现NullReferenceException | 在Criteria表达式中访问了可能为null的导航属性,而未使用空条件运算符。 | 检查规约中Criteria表达式,特别是涉及导航属性链(如a.Author.Profile.Name)的部分。 | 在表达式中使用?.(空条件运算符)或在规约中预先Include确保导航属性已加载。 |
| 生成的 SQL 性能低下 | 1.Includes过多或过深,导致复杂连接。2. 规约组合导致 SQL 条件过于复杂,索引失效。 | 1. 使用 SQL Server Profiler 或 EF Core 日志查看生成的 SQL。 2. 分析执行计划。 | 1. 按需加载,使用投影(Select)代替贪婪加载不需要的字段。2. 考虑将复杂规约拆分为多个查询,或在数据库层面优化索引。 |
| 分页或排序结果不正确 | 1. 多个OrderBy优先级处理错误。2. 分页前排序不稳定(如按非唯一字段排序)。 | 1. 检查SpecificationEvaluator中排序逻辑,确保OrderBy和OrderByDescending正确应用了ThenBy。2. 确保排序字段组合能唯一确定顺序,或增加一个唯一字段(如 Id)作为最终排序条件。 | 1. 完善评估器的排序逻辑。 2. 在规约中明确添加最终排序条件,如 AddOrderBy(a => a.Id)。 |
| 无法组合两个规约的条件 | 直接使用And/Or连接两个Expression<Func<T, bool>>对象比较复杂。 | 表达式树是不可变的,需要借助工具进行组合。 | 引入LinqKit库,使用PredicateBuilder或Expand()方法,或者实现自己的AndSpecification/OrSpecification。 |
11. 最佳实践与工程建议
- 保持规约的单一职责:一个规约类应只代表一个明确的业务意图。不要创建
GetDashboardDataSpecification这种包含无数条件的“上帝规约”。 - 为规约编写单元测试:规约是纯业务逻辑的体现,不依赖外部资源,非常适合单元测试。测试其
Criteria是否能正确过滤内存中的对象集合。 - 谨慎使用
Include:贪婪加载是性能杀手。只在当前业务场景确实需要完整对象图时才使用Include。考虑使用投影(Select)来只查询需要的字段,这可以显著提升查询性能并减少内存占用。你可以将投影逻辑也封装进规约。 - 考虑规约的命名:命名应体现其业务意图,而非技术细节。例如,
ActiveUsersSpecification比UsersWhereIsActiveTrueSpecification更好。 - 处理动态查询:对于高度动态的查询(如高级搜索),规约模式可能不是最优解。此时,可以考虑使用动态 LINQ 库(如
System.Linq.Dynamic.Core)或显式构建IQueryable并在服务层组合。规约模式更适合于那些定义良好、可复用的业务查询单元。 - 与 CQRS 结合:在更复杂的系统中,可以将规约模式与 CQRS(命令查询职责分离)架构结合。规约专门用于定义查询端的复杂条件,使查询模型更加清晰。
- 性能优化:复杂的规约组合可能导致 SQL 语句冗长。定期监控生成的 SQL,确保其能有效利用数据库索引。对于特别复杂的查询,有时编写一个优化的存储过程或视图可能是更务实的选择。
- 依赖注入:
GenericRepository<T>和具体的规约类通常不需要注册到 DI 容器。规约是瞬态对象,在需要时new即可。仓储接口IGenericRepository<T>需要注册。
12. 总结与后续方向
通过本文的探讨,你应该已经认识到,用规范模式“清理” EF Core 查询,本质上是对查询逻辑进行领域建模。它将散落的、隐式的业务规则提升为显式的、可测试的、可复用的软件组件。
回顾核心收益:
- 清晰度:查询条件有了自己的“家”,代码库结构一目了然。
- 可复用性:核心业务规则(如“已发布”)被封装一次,处处使用。
- 可测试性:规约逻辑可以脱离数据库进行单元测试。
- 可组合性:通过
AndSpecification、OrSpecification可以灵活构建复杂查询。
你可以继续深化的方向:
- 实现
Select投影规约:将查询结果的形状(DTO)也封装进规约,实现从数据库到 API 模型的直接高效映射。 - 集成
LinqKit:使用PredicateBuilder来更优雅、安全地组合多个规约的Criteria表达式。 - 构建规约构建器:对于高度动态的查询界面,可以设计一个流畅接口(Fluent API)的构建器,来动态组装规约。
- 探索 Ardalis.Specification:这是一个非常成熟、功能丰富的规约模式实现库(
Ardalis.Specification和Ardalis.Specification.EntityFrameworkCore),提供了开箱即用的组合规约、排序、分页、缓存甚至二级缓存支持,非常适合在生产项目中直接采用。
从今天开始,审视你项目中的那些冗长的Where语句,思考它们能否被提取成一个有意义的Specification类。这一步小小的重构,将为你的数据访问层带来持久的秩序与健壮性。