你是否遇到过这样的场景一个简单的列表查询随着业务发展查询条件越来越多Where子句越写越长最终变成一个几百行的“意大利面条式”查询方法或者同样的查询逻辑如“查询已发布且未删除的文章”在控制器、服务层、仓储层被重复编写一旦业务规则变更就需要满世界修改这不是代码冗余的问题而是查询逻辑缺乏组织和复用的典型症状。在 Entity Framework Core 项目中这个问题尤为突出。开发者往往将查询逻辑直接写在DbContext或仓储方法中导致代码难以测试、维护和扩展。今天要讨论的“规范模式”正是解决这一痛点的利器。它不是一个新概念但在 EF Core 的语境下它能将你的查询从混乱的IQueryable拼接中解放出来实现逻辑的封装、复用和清晰分离。更重要的是它能与“查询清理”的思想结合帮助你构建出高性能、可维护的数据访问层。本文将带你深入理解如何用规范模式来“清理”你的 EF Core 查询。你会看到这不仅仅是写一个ISpecification接口那么简单而是关乎如何设计一个清晰、健壮且易于测试的查询架构。1. 这篇文章真正要解决的问题很多开发者对 EF Core 又爱又恨。“爱”的是它的开发效率用 LINQ 就能操作数据库“恨”的是随着项目复杂度的提升查询代码会迅速变得难以控制。我们面临的核心问题有几个逻辑分散与重复判断“有效用户”的逻辑可能在用户查询、订单查询、报表查询中重复出现。一旦“有效”的定义改变比如增加“邮箱已验证”条件就需要修改多处。可测试性差一个包含复杂Where、Include、OrderBy的查询方法很难进行单元测试。你通常需要启动一个内存数据库或连接真实数据库这属于集成测试速度慢且不稳定。组合能力弱当需要动态组合查询条件时如前端传入多个过滤参数代码中会充斥大量的if语句来拼接IQueryable可读性急剧下降。关注点混淆数据访问逻辑怎么查和业务逻辑为什么查经常混杂在一起违反了单一职责原则。规范模式的核心价值就在于它将查询条件、排序规则、分页逻辑以及数据投影Select等封装成一个独立的、可复用的“规范”对象。这个对象本身不执行查询它只是描述了一个查询应该是什么样子。然后由一个统一的“规约执行器”来应用这些规范生成最终的IQueryable或执行查询。通过这种方式我们实现了“查询逻辑”的客体化。你可以像组合乐高积木一样组合不同的规范也可以轻松地对单个规范进行单元测试。这就是对 EF Core 查询代码最彻底的“清理”。2. 基础概念与核心原理在深入代码之前我们先厘清几个关键概念。什么是规范模式规范模式是一种特定领域的设计模式它使用一个对象来封装业务规则用于判断另一个对象是否满足该规则。在数据访问层这个“规则”就是查询条件。我们将“查询已发布文章”这个规则封装成一个PublishedPostSpecification类的实例。核心组件一个典型的规范模式实现包含以下部分规约接口定义规约的基本契约通常包含一个IsSatisfiedBy方法用于内存对象判断或一个Apply方法用于构建IQueryable。在 EF Core 中我们更关注后者。具体规约类实现接口封装具体的查询逻辑。例如ActiveUserSpecification、ProductInStockSpecification。组合器提供And、Or、Not等方法允许将多个规约组合成一个新的规约。这是实现逻辑复用的关键。规约求值器/执行器负责将规约对象应用到IQueryable上并最终执行查询返回数据。与仓储模式的关系规范模式常与仓储模式结合使用。传统的仓储接口可能会有GetActiveUsers、GetExpiredOrders等方法导致方法爆炸。引入规范模式后仓储接口可以简化为public interface IRepositoryT where T : class { TaskT? GetByIdAsync(int id); TaskIEnumerableT GetAllAsync(); // 核心变化接受规约进行查询 TaskIEnumerableT FindAsync(ISpecificationT spec); // 可能还有 FindPagedAsync, CountAsync 等 }这样仓储只负责基础 CRUD 和规约的执行而具体的查询逻辑则由规约类定义职责更加清晰。“清理”体现在何处逻辑归类相似的查询条件被归类到同一个规约中。消除重复通用逻辑通过规约复用。动态组合通过组合器灵活应对复杂查询场景。易于测试可以单独测试一个规约是否正确地构建了表达式树。3. 环境准备与前置条件为了实践本文的示例你需要准备以下环境开发环境.NET 6, .NET 7, .NET 8 或更高版本。本文示例基于 .NET 8但核心概念适用于所有支持 EF Core 的版本。IDEVisual Studio 2022、Rider 或 VS Code。数据库示例使用 SQL Server LocalDB 或 SQLite便于演示但 EF Core 支持的数据库均可。项目类型一个 ASP.NET Core Web API 或 Console 应用程序。NuGet 包Microsoft.EntityFrameworkCore.SqlServer(或Microsoft.EntityFrameworkCore.Sqlite)Microsoft.EntityFrameworkCore.Tools(可选用于迁移)你可以通过以下命令创建一个新的控制台项目并添加 EF Core 支持dotnet new console -n EfCoreSpecificationDemo cd EfCoreSpecificationDemo dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Design4. 核心流程拆解实现一个基础的规范模式让我们从零开始构建一个适用于 EF Core 的规范模式实现。这个过程分为定义接口、实现基础规约、实现组合逻辑以及创建执行器。4.1 定义规约接口首先我们定义一个最核心的接口ISpecificationT。它的主要职责是提供一个方法将自身描述的规则应用到给定的IQueryableT上。// 文件路径SpecificationPattern/ISpecification.cs namespace EfCoreSpecificationDemo.SpecificationPattern; public interface ISpecificationT { /// summary /// 将规约条件应用到查询上 /// /summary /// param namequery原始查询/param /// returns应用了规约条件后的查询/returns IQueryableT Apply(IQueryableT query); }这个接口非常简单但它是所有规约的基石。4.2 实现抽象基类与组合器接下来我们实现一个抽象基类SpecificationT它实现了ISpecificationT接口并提供了And,Or,Not等组合方法。这些组合方法利用了 LINQ 的Expression树。// 文件路径SpecificationPattern/Specification.cs using System.Linq.Expressions; namespace EfCoreSpecificationDemo.SpecificationPattern; public abstract class SpecificationT : ISpecificationT { // 核心一个返回布尔值的表达式树代表查询条件 public abstract ExpressionFuncT, bool Criteria { get; } // 可选的“包含”表达式用于实现 Eager Loading (Include) public virtual ListExpressionFuncT, object Includes { get; } new(); // 可选的排序表达式 public virtual ExpressionFuncT, object? OrderBy { get; private set; } public virtual ExpressionFuncT, object? OrderByDescending { get; private set; } // 可选的翻页属性 public virtual int Take { get; private set; } public virtual int Skip { get; private set; } public virtual bool IsPagingEnabled { get; private set; } false; /// summary /// 应用规约到 IQueryable /// /summary public IQueryableT Apply(IQueryableT query) { // 1. 应用过滤条件 query query.Where(Criteria); // 2. 应用包含Include query Includes.Aggregate(query, (current, include) current.Include(include)); // 3. 应用排序 if (OrderBy ! null) query query.OrderBy(OrderBy); else if (OrderByDescending ! null) query query.OrderByDescending(OrderByDescending); // 4. 应用分页 if (IsPagingEnabled) { query query.Skip(Skip).Take(Take); } return query; } // 组合方法与另一个规约进行 AND 运算 public SpecificationT And(SpecificationT other) { return new AndSpecificationT(this, other); } // 组合方法与另一个规约进行 OR 运算 public SpecificationT Or(SpecificationT other) { return new OrSpecificationT(this, other); } // 组合方法对当前规约进行 NOT 运算 public SpecificationT Not() { return new NotSpecificationT(this); } // 构建器方法添加 Include protected void AddInclude(ExpressionFuncT, object includeExpression) { Includes.Add(includeExpression); } // 构建器方法设置排序 protected void ApplyOrderBy(ExpressionFuncT, object orderByExpression) { OrderBy orderByExpression; OrderByDescending null; } protected void ApplyOrderByDescending(ExpressionFuncT, object orderByDescendingExpression) { OrderByDescending orderByDescendingExpression; OrderBy null; } // 构建器方法设置分页 protected void ApplyPaging(int skip, int take) { Skip skip; Take take; IsPagingEnabled true; } }这个基类已经相当强大。它封装了过滤、包含、排序、分页等常见查询操作。注意Apply方法它按照一个合理的顺序过滤 - 包含 - 排序 - 分页来构建查询。4.3 实现组合规约类我们需要实现AndSpecification、OrSpecification和NotSpecification来支持组合逻辑。这里以AndSpecification为例// 文件路径SpecificationPattern/AndSpecification.cs using System.Linq.Expressions; namespace EfCoreSpecificationDemo.SpecificationPattern; public class AndSpecificationT : SpecificationT { private readonly SpecificationT _left; private readonly SpecificationT _right; public AndSpecification(SpecificationT left, SpecificationT right) { _left left; _right right; } // 关键使用 Expression.AndAlso 将两个条件表达式合并 public override ExpressionFuncT, bool Criteria { get { var leftExpr _left.Criteria; var rightExpr _right.Criteria; var param Expression.Parameter(typeof(T), x); var combinedBody Expression.AndAlso( Expression.Invoke(leftExpr, param), Expression.Invoke(rightExpr, param) ); return Expression.LambdaFuncT, bool(combinedBody, param); } } // 合并两个规约的 Includes 列表 public override ListExpressionFuncT, object Includes { get { var includes new ListExpressionFuncT, object(); includes.AddRange(_left.Includes); includes.AddRange(_right.Includes); // 简单去重根据表达式字符串实际项目可能需要更复杂的比较 return includes.DistinctBy(exp exp.ToString()).ToList(); } } }OrSpecification和NotSpecification的实现类似分别使用Expression.OrElse和Expression.Not。4.4 定义泛型仓储接口与实现现在让我们定义一个支持规约的泛型仓储接口。// 文件路径Data/IRepository.cs using EfCoreSpecificationDemo.SpecificationPattern; namespace EfCoreSpecificationDemo.Data; public interface IRepositoryT where T : class { TaskT? GetByIdAsync(int id); TaskIReadOnlyListT GetAllAsync(); // 核心方法根据规约查询 TaskIReadOnlyListT FindAsync(ISpecificationT specification); // 计数 Taskint CountAsync(ISpecificationT specification); // 添加与保存 TaskT AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entity); Task SaveChangesAsync(); }其基于 EF Core 的实现// 文件路径Data/EfRepository.cs using EfCoreSpecificationDemo.SpecificationPattern; using Microsoft.EntityFrameworkCore; namespace EfCoreSpecificationDemo.Data; public class EfRepositoryT : IRepositoryT where T : class { protected readonly DbContext _dbContext; protected readonly DbSetT _dbSet; public EfRepository(DbContext dbContext) { _dbContext dbContext; _dbSet dbContext.SetT(); } public virtual async TaskT? GetByIdAsync(int id) await _dbSet.FindAsync(id); public virtual async TaskIReadOnlyListT GetAllAsync() await _dbSet.ToListAsync(); public virtual async TaskIReadOnlyListT FindAsync(ISpecificationT specification) { // 应用规约然后执行查询 var query specification.Apply(_dbSet.AsQueryable()); return await query.ToListAsync(); } public virtual async Taskint CountAsync(ISpecificationT specification) { var query specification.Apply(_dbSet.AsQueryable()); return await query.CountAsync(); } public async TaskT AddAsync(T entity) { await _dbSet.AddAsync(entity); return entity; } public Task UpdateAsync(T entity) { _dbContext.Entry(entity).State EntityState.Modified; return Task.CompletedTask; } public Task DeleteAsync(T entity) { _dbSet.Remove(entity); return Task.CompletedTask; } public async Task SaveChangesAsync() await _dbContext.SaveChangesAsync(); }注意FindAsync方法它接收一个ISpecificationT调用其Apply方法构建查询然后异步执行。这是连接规约模式和 EF Core 的桥梁。5. 完整示例与代码实现让我们用一个完整的博客系统示例来演示如何使用上述架构。假设我们有Blog和Post两个实体。5.1 定义实体与 DbContext// 文件路径Entities/Blog.cs namespace EfCoreSpecificationDemo.Entities; public class Blog { public int Id { get; set; } public string Name { get; set; } string.Empty; public string Url { get; set; } string.Empty; public bool IsActive { get; set; } true; public DateTime CreatedOn { get; set; } DateTime.UtcNow; // 导航属性 public virtual ICollectionPost Posts { get; set; } new ListPost(); }// 文件路径Entities/Post.cs namespace EfCoreSpecificationDemo.Entities; public class Post { public int Id { get; set; } public string Title { get; set; } string.Empty; public string Content { get; set; } string.Empty; public bool IsPublished { get; set; } public bool IsDeleted { get; set; } public DateTime? PublishedDate { get; set; } public int ViewCount { get; set; } public DateTime CreatedOn { get; set; } DateTime.UtcNow; // 外键与导航属性 public int BlogId { get; set; } public virtual Blog Blog { get; set; } null!; }// 文件路径Data/AppDbContext.cs using EfCoreSpecificationDemo.Entities; using Microsoft.EntityFrameworkCore; namespace EfCoreSpecificationDemo.Data; public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetBlog Blogs SetBlog(); public DbSetPost Posts SetPost(); protected override void OnModelCreating(ModelBuilder modelBuilder) { // 可以在这里配置一些实体关系或索引 modelBuilder.EntityPost() .HasQueryFilter(p !p.IsDeleted); // 全局查询过滤器自动过滤已删除的帖子 } }5.2 创建具体的规约现在创建几个针对Post实体的规约。// 文件路径Specifications/PublishedPostsSpecification.cs using EfCoreSpecificationDemo.Entities; using EfCoreSpecificationDemo.SpecificationPattern; namespace EfCoreSpecificationDemo.Specifications; public class PublishedPostsSpecification : SpecificationPost { public PublishedPostsSpecification() { // 只查询已发布的文章 } public override ExpressionFuncPost, bool Criteria p p.IsPublished; }// 文件路径Specifications/RecentPostsSpecification.cs using EfCoreSpecificationDemo.Entities; using EfCoreSpecificationDemo.SpecificationPattern; namespace EfCoreSpecificationDemo.Specifications; public class RecentPostsSpecification : SpecificationPost { private readonly int _days; public RecentPostsSpecification(int days 7) { _days days; // 按发布日期倒序排列 ApplyOrderByDescending(p p.PublishedDate); } public override ExpressionFuncPost, bool Criteria p p.PublishedDate.HasValue p.PublishedDate.Value DateTime.UtcNow.AddDays(-_days); }// 文件路径Specifications/PostsWithBlogSpecification.cs using EfCoreSpecificationDemo.Entities; using EfCoreSpecificationDemo.SpecificationPattern; namespace EfCoreSpecificationDemo.Specifications; public class PostsWithBlogSpecification : SpecificationPost { public PostsWithBlogSpecification() { // 包含关联的 Blog 数据Eager Loading AddInclude(p p.Blog); } // 此规约没有额外的过滤条件查询所有 public override ExpressionFuncPost, bool Criteria p true; }5.3 在服务层中使用规约创建一个服务类来协调这些规约和仓储。// 文件路径Services/PostService.cs using EfCoreSpecificationDemo.Data; using EfCoreSpecificationDemo.Entities; using EfCoreSpecificationDemo.Specifications; namespace EfCoreSpecificationDemo.Services; public class PostService { private readonly IRepositoryPost _postRepository; public PostService(IRepositoryPost postRepository) { _postRepository postRepository; } // 场景1获取最近一周已发布的文章并包含博客信息 public async TaskIReadOnlyListPost GetRecentPublishedPostsAsync() { // 组合规约最近发布的 AND 已发布的 AND 包含博客信息 var spec new RecentPostsSpecification(7) .And(new PublishedPostsSpecification()) .And(new PostsWithBlogSpecification()); // 注意组合时 Includes 会被合并 return await _postRepository.FindAsync(spec); } // 场景2获取某个博客下所有已发布文章分页 public async TaskIReadOnlyListPost GetPublishedPostsByBlogAsync(int blogId, int pageIndex, int pageSize) { // 创建一个新的组合规约 var spec new PublishedPostsSpecification() .And(new PostsFromBlogSpecification(blogId)) // 假设有这个规约 .And(new PostsWithBlogSpecification()); // 动态添加分页注意我们的基类需要扩展以支持动态设置分页或创建新的规约 // 更佳实践创建一个 PagedSpecification 或使用构建器模式。 // 此处为演示我们创建一个新的继承类来设置分页。 var pagedSpec new PagedSpecificationPost(spec, pageIndex * pageSize, pageSize); return await _postRepository.FindAsync(pagedSpec); } // 场景3统计活跃博客的数量博客下有至少一篇最近一个月发布的文章 public async Taskint CountActiveBlogsAsync() { // 这里需要关联查询更复杂的规约可能涉及子查询或特定仓储方法。 // 为简化演示我们假设有一个 BlogRepository 和对应的规约。 // 这展示了规约模式的边界对于非常复杂的跨实体聚合查询可能需要专门的查询对象或仓储方法。 throw new NotImplementedException(此示例略过复杂关联统计的实现。); } } // 辅助规约分页规约 public class PagedSpecificationT : SpecificationT { public PagedSpecification(SpecificationT innerSpec, int skip, int take) { // 这里需要“继承”innerSpec的所有条件、包含和排序。 // 一种实现方式是使用组合而非继承或者修改基类以支持更灵活的构建。 // 这是一个高级话题提示我们基础实现可能需要改进。 } // ... 具体实现需要考虑如何复制另一个规约的表达式树较为复杂。 }5.4 在 Program.cs 中集成与运行// 文件路径Program.cs using EfCoreSpecificationDemo.Data; using EfCoreSpecificationDemo.Entities; using EfCoreSpecificationDemo.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; var services new ServiceCollection(); // 1. 配置 DbContext (使用 SQLite 内存数据库方便演示) services.AddDbContextAppDbContext(options options.UseSqlite(Data Source:memory:)); // 2. 注册泛型仓储 services.AddScoped(typeof(IRepository), typeof(EfRepository)); // 3. 注册服务 services.AddScopedPostService(); var serviceProvider services.BuildServiceProvider(); // 确保数据库创建并填充种子数据 using (var scope serviceProvider.CreateScope()) { var dbContext scope.ServiceProvider.GetRequiredServiceAppDbContext(); await dbContext.Database.EnsureCreatedAsync(); await SeedDataAsync(dbContext); // 假设有一个填充测试数据的方法 } // 使用服务 using (var scope serviceProvider.CreateScope()) { var postService scope.ServiceProvider.GetRequiredServicePostService(); var recentPosts await postService.GetRecentPublishedPostsAsync(); Console.WriteLine($找到 {recentPosts.Count} 篇近期发布的文章。); foreach (var post in recentPosts) { Console.WriteLine($- {post.Title} (来自博客: {post.Blog?.Name})); } } async Task SeedDataAsync(AppDbContext context) { if (!await context.Blogs.AnyAsync()) { var blog new Blog { Name 技术博客, Url https://tech.example.com, IsActive true }; context.Blogs.Add(blog); await context.SaveChangesAsync(); var posts new ListPost { new Post { Title EF Core 入门, Content ..., IsPublished true, PublishedDate DateTime.UtcNow.AddDays(-1), BlogId blog.Id }, new Post { Title 规范模式详解, Content ..., IsPublished true, PublishedDate DateTime.UtcNow.AddDays(-5), BlogId blog.Id }, new Post { Title 未发布的草稿, Content ..., IsPublished false, BlogId blog.Id }, }; context.Posts.AddRange(posts); await context.SaveChangesAsync(); } }6. 运行结果与效果验证运行上述控制台程序预期输出如下找到 2 篇近期发布的文章。 - EF Core 入门 (来自博客: 技术博客) - 规范模式详解 (来自博客: 技术博客)如何验证规约生效日志验证在DbContext配置中启用敏感数据日志可以查看生成的 SQL。你会看到类似这样的语句SELECT p.*, b.* FROM Posts AS p INNER JOIN Blogs AS b ON p.BlogId b.Id WHERE p.IsPublished 1 AND p.PublishedDate __startDate_0 ORDER BY p.PublishedDate DESC这表明IsPublished条件、PublishedDate条件、JOIN和ORDER BY都被正确应用了。单元测试你可以为每个Specification编写单元测试验证其Criteria表达式树是否正确。也可以为FindAsync方法编写集成测试使用内存数据库验证返回的数据是否符合预期。7. 常见问题与排查思路问题现象可能原因排查方式解决方案查询结果为空但数据库有数据1. 规约的Criteria条件太严格。2. 全局查询过滤器如HasQueryFilter干扰。3. 组合规时表达式合并出错。1. 检查规约的Criteria属性。2. 检查DbContext的OnModelCreating配置。3. 在调试器中查看组合后表达式的结构。1. 简化规约条件进行测试。2. 使用IgnoreQueryFilters()临时忽略全局过滤器。3. 确保组合规约类如AndSpecification正确合并表达式。出现NullReferenceException特别是在包含(Include)之后1. 在规约中使用了Include但查询结果中导航属性仍为null。2. 规约的Includes列表在组合时未正确合并或去重。1. 检查生成的 SQL 是否包含JOIN。2. 检查规约的Includes列表内容。1. 确保Include表达式正确指向导航属性。2. 在规约基类的Apply方法中确保Aggregate正确应用了所有Include。3. 考虑使用ThenInclude进行多级包含这需要扩展规约设计。分页或排序未生效1. 分页或排序属性未在规约中正确设置。2. 多个规约组合时排序被覆盖。1. 在调试器中检查规约的OrderBy、Skip、Take属性。2. 检查组合逻辑确保排序规则不被意外覆盖。1. 确保在规约构造函数或方法中正确调用了ApplyOrderBy或ApplyPaging。2. 设计规约组合策略例如规定只有最后一个排序规约生效或支持多级排序。性能问题生成过于复杂的 SQL 或查询缓慢1. 规约组合过于复杂导致 SQL 语句冗长。2. 包含了不必要的数据如Select *。3. 缺少合适的数据库索引。1. 使用 SQL Server Profiler 或 EF Core 日志查看生成的 SQL。2. 分析查询执行计划。1. 考虑对非常复杂的查询创建专用的规约或视图。2. 在规约中增加“投影”支持只查询需要的字段。3. 为频繁查询的字段如IsPublished,PublishedDate添加数据库索引。无法实现某些复杂查询如子查询、分组基础规约模式主要针对Where、Include、OrderBy、分页。更复杂的 LINQ 操作支持不足。评估查询复杂度。1. 扩展规约接口增加对GroupBy、Select投影等的支持。2. 对于极其复杂的查询可以回退到在仓储中编写特定的查询方法这并不违反原则。规约模式是工具不是枷锁。8. 最佳实践与工程建议规约的单一职责每个规约类应只代表一个明确的、可命名的业务规则如ActiveUsersSpecification、OrdersFromLastMonthSpecification。避免创建“万能”规约。使用构建器模式增强灵活性对于需要动态配置的规约如分页参数、排序字段可以考虑使用构建器模式Fluent API来创建规约使代码更清晰。var spec new PostSpecificationBuilder() .PublishedOnly() .FromBlog(blogId) .IncludeBlog() .OrderByDescending(p p.PublishedDate) .Paginate(pageIndex, pageSize) .Build();支持投影Select为了优化性能经常只需要实体的部分字段。可以扩展规约模式支持定义Select表达式让仓储返回IQueryableProjectionType或IEnumerableProjectionType。规约的单元测试规约的核心是表达式树。你可以轻松地为规约编写单元测试验证其Criteria是否正确。使用内存中的对象列表如ListPost和AsQueryable()进行测试无需数据库。与 CQRS 结合在更复杂的系统中可以考虑将规约模式与命令查询职责分离CQRS架构结合。规约专门用于构建查询端Read Side的复杂查询使查询模型更加专注和高效。谨慎使用组合虽然And、Or、Not组合很强大但过度组合可能导致表达式树非常复杂影响 SQL 生成效率和可读性。对于固定的、常用的查询组合可以考虑创建新的、命名的规约类。依赖注入规约类通常是无状态的只包含表达式树定义。它们一般不需要依赖注入可以直接new出来。仓储和服务层才需要被注入。版本控制与演化当业务规则变化时你只需要修改对应的规约类。例如“有效用户”的定义从“已激活”变为“已激活且邮箱已验证”你只需修改ActiveUserSpecification.Criteria。所有使用该规约的查询都会自动更新。通过遵循这些实践规范模式就能成为你 EF Core 项目数据访问层的坚实支柱显著提升代码的整洁度、可维护性和可测试性。它不仅仅是一种技术实现更是一种让查询逻辑变得清晰、可组合和可管理的设计思想。