
在实际 .NET 项目中随着业务逻辑的膨胀数据访问层DAL的代码往往会变得臃肿不堪。一个典型的Repository类可能充斥着数十个根据特定业务场景定制的查询方法这些方法内部又包含了复杂的Where、Include、OrderBy等 LINQ 表达式。这不仅导致代码重复也让单元测试变得困难更关键的是当查询逻辑需要跨多个方法或服务复用时你只能复制粘贴埋下了维护的噩梦。EF Core 的规范模式Specification Pattern正是为了解决这类问题而引入的一种设计模式它旨在将查询的“定义”即查询什么数据以及如何查询与查询的“执行”分离开来。规范模式的核心思想是将一个查询封装成一个独立、可组合、可测试的对象。这不仅仅是简单的“提取方法”而是建立一个清晰的契约一个规范对象负责描述一个完整的查询意图。通过应用此模式你可以将分散在服务层或仓储层中的复杂 LINQ 逻辑抽离出来形成一系列可重用的“查询组件”。本文将带你从零开始理解规范模式在 EF Core 中的价值并一步步构建一个具备基本查询、排序、分页、包含导航属性等能力的规范实现最终用它来清理和重构你那杂乱的数据访问代码。1. 理解规范模式从混乱的查询到清晰的契约在深入代码之前我们必须先厘清规范模式要解决的根本问题。假设你有一个Product产品实体常见的查询需求可能包括“获取所有上架的产品”、“获取某个分类下价格高于100的产品”、“获取包含供应商信息的畅销产品列表”。在没有规范模式的情况下你可能会在ProductRepository中写出如下代码public class ProductRepository { private readonly AppDbContext _context; public async TaskListProduct GetActiveProductsAsync() { return await _context.Products .Where(p p.IsActive) .ToListAsync(); } public async TaskListProduct GetExpensiveProductsInCategoryAsync(int categoryId, decimal minPrice) { return await _context.Products .Where(p p.CategoryId categoryId p.Price minPrice) .Include(p p.Supplier) // 可能需要包含供应商 .OrderByDescending(p p.Price) .ToListAsync(); } // 更多类似的方法... }这种写法的弊端非常明显代码重复Where(p p.IsActive)这个条件可能在其他查询中也需要但你不得不重复编写。难以测试要测试GetExpensiveProductsInCategoryAsync方法你需要搭建完整的数据库环境或复杂的DbContextMock。难以组合如果需要一个同时满足“上架”和“价格高于100”的查询你需要写第三个方法或者在前端拼接逻辑。职责模糊仓储类既负责数据持久化抽象又承担了具体的、多变的查询逻辑。规范模式通过引入一个ISpecificationT接口来解决这些问题。这个接口定义了一个查询规约应该具备的能力它能产生一个可以被 EF Core 执行的IQueryableT表达式。这样查询的逻辑就被封装在了一个个独立的规约类中仓储的职责退化为“按规约获取数据”。一个基础的规范接口可能长这样public interface ISpecificationT { // 查询条件表达式 ExpressionFuncT, bool? Criteria { get; } // 需要包含的导航属性Include ListExpressionFuncT, object Includes { get; } // 排序表达式 ListOrderByExpressionT OrderBys { get; } // 分页参数 int? Skip { get; } int? Take { get; } // 一个将规约应用到 IQueryable 上的方法 IQueryableT Apply(IQueryableT query); }通过实现这个接口GetActiveProductsAsync对应的规约类就是ActiveProductSpecification其Criteria属性返回p p.IsActive。仓储类只需要一个通用方法如ListAsync(ISpecificationT spec)即可应对所有查询。这实现了查询逻辑的模块化、可复用和可测试。2. 环境准备与项目结构在开始实现之前我们需要一个干净的 EF Core 项目环境。本文假设你使用 .NET 6/8 和 EF Core 7/8。2.1 创建项目与模型首先创建一个新的 ASP.NET Core Web API 或类库项目。dotnet new webapi -n EfCoreSpecificationDemo cd EfCoreSpecificationDemo添加 EF Core 和 SQL Server 提供程序包这里以 SQL Server 为例你也可以使用其他数据库dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Design接下来定义我们的领域模型。为了演示规范模式的多种能力我们创建一个包含关联关系的简单模型// Models/Product.cs public class Product { public int Id { get; set; } public string Name { get; set; } string.Empty; public string? Description { get; set; } public decimal Price { get; set; } public bool IsActive { get; set; } public int StockQuantity { get; set; } // 外键与导航属性 public int CategoryId { get; set; } public Category Category { get; set; } null!; public int? SupplierId { get; set; } public Supplier? Supplier { get; set; } } // Models/Category.cs public class Category { public int Id { get; set; } public string Name { get; set; } string.Empty; public ListProduct Products { get; set; } new(); } // Models/Supplier.cs public class Supplier { public int Id { get; set; } public string CompanyName { get; set; } string.Empty; public ListProduct Products { get; set; } new(); }2.2 配置 DbContext 与数据库迁移创建AppDbContext并配置模型关系// Data/AppDbContext.cs using Microsoft.EntityFrameworkCore; using EfCoreSpecificationDemo.Models; namespace EfCoreSpecificationDemo.Data { public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetProduct Products SetProduct(); public DbSetCategory Categories SetCategory(); public DbSetSupplier Suppliers SetSupplier(); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 这里可以添加一些种子数据或更复杂的配置但非本文必需 modelBuilder.EntityProduct() .HasOne(p p.Category) .WithMany(c c.Products) .HasForeignKey(p p.CategoryId); modelBuilder.EntityProduct() .HasOne(p p.Supplier) .WithMany(s s.Products) .HasForeignKey(p p.SupplierId) .OnDelete(DeleteBehavior.SetNull); // 设置删除行为 } } }在appsettings.json中配置数据库连接字符串{ ConnectionStrings: { DefaultConnection: Server(localdb)\\mssqllocaldb;DatabaseSpecificationDemoDb;Trusted_ConnectionTrue;MultipleActiveResultSetstrue } }在Program.cs中注册DbContextbuilder.Services.AddDbContextAppDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(DefaultConnection)));最后创建并应用初始迁移dotnet ef migrations add InitialCreate dotnet ef database update至此基础项目环境搭建完成。接下来我们将进入核心部分实现规范模式。3. 实现一个基础且可扩展的规范模式我们将分步构建一个功能完整的规范模式实现。首先从核心接口和基类开始。3.1 定义核心接口与基类在项目中创建Specifications文件夹并添加以下文件。首先定义一个表示排序的辅助类// Specifications/OrderByExpression.cs namespace EfCoreSpecificationDemo.Specifications { public class OrderByExpressionT { public ExpressionFuncT, object Expression { get; } public bool IsDescending { get; } public OrderByExpression(ExpressionFuncT, object expression, bool isDescending false) { Expression expression ?? throw new ArgumentNullException(nameof(expression)); IsDescending isDescending; } } }接着定义最核心的ISpecificationT接口。我们将设计一个功能丰富但易于使用的接口。// Specifications/ISpecification.cs using System.Linq.Expressions; namespace EfCoreSpecificationDemo.Specifications { public interface ISpecificationT { // 核心查询条件 ExpressionFuncT, bool? Criteria { get; } // 包含的导航属性用于 Include IReadOnlyListExpressionFuncT, object Includes { get; } // 包含的导航属性字符串形式用于 ThenInclude 或字符串 Include IReadOnlyListstring IncludeStrings { get; } // 排序规则列表支持多级排序 IReadOnlyListOrderByExpressionT OrderBys { get; } // 分页参数 int? Skip { get; } int? Take { get; } // 是否启用跟踪AsNoTracking bool IsNoTracking { get; } // 将规约应用到 IQueryable 的核心方法 IQueryableT Apply(IQueryableT query); } }然后我们创建一个抽象的基类BaseSpecificationT来提供接口的默认实现和构建器模式Fluent API这将使创建规约更加直观。// Specifications/BaseSpecification.cs using System.Linq.Expressions; namespace EfCoreSpecificationDemo.Specifications { public abstract class BaseSpecificationT : ISpecificationT { // 接口实现 public ExpressionFuncT, bool? Criteria { get; protected set; } protected ListExpressionFuncT, object IncludesInternal { get; } new(); public IReadOnlyListExpressionFuncT, object Includes IncludesInternal.AsReadOnly(); protected Liststring IncludeStringsInternal { get; } new(); public IReadOnlyListstring IncludeStrings IncludeStringsInternal.AsReadOnly(); protected ListOrderByExpressionT OrderBysInternal { get; } new(); public IReadOnlyListOrderByExpressionT OrderBys OrderBysInternal.AsReadOnly(); public int? Skip { get; protected set; } public int? Take { get; protected set; } public bool IsNoTracking { get; protected set; } // 构建器方法Fluent API protected void AddCriteria(ExpressionFuncT, bool criteria) { // 这里处理逻辑可以是替换或组合AND/OR为简化我们直接替换或与现有条件组合。 // 更复杂的实现可以使用 PredicateBuilder这里我们采用简单替换。 Criteria criteria; } protected void AddInclude(ExpressionFuncT, object includeExpression) { IncludesInternal.Add(includeExpression); } protected void AddInclude(string includeString) { IncludeStringsInternal.Add(includeString); } protected void AddOrderBy(ExpressionFuncT, object orderByExpression, bool descending false) { OrderBysInternal.Add(new OrderByExpressionT(orderByExpression, descending)); } protected void ApplyPaging(int skip, int take) { Skip skip; Take take; } protected void ApplyNoTracking() { IsNoTracking true; } // 核心应用方法 public virtual IQueryableT Apply(IQueryableT query) { // 1. 应用查询条件 if (Criteria ! null) { query query.Where(Criteria); } // 2. 应用 Include (表达式) foreach (var include in Includes) { query query.Include(include); } // 3. 应用 Include (字符串) - 用于复杂的多级包含 foreach (var includeString in IncludeStrings) { query query.Include(includeString); } // 4. 应用排序 if (OrderBys.Any()) { var firstOrder OrderBys[0]; var orderedQuery firstOrder.IsDescending ? query.OrderByDescending(firstOrder.Expression) : query.OrderBy(firstOrder.Expression); for (int i 1; i OrderBys.Count; i) { var thenOrder OrderBys[i]; orderedQuery thenOrder.IsDescending ? ((IOrderedQueryableT)orderedQuery).ThenByDescending(thenOrder.Expression) : ((IOrderedQueryableT)orderedQuery).ThenBy(thenOrder.Expression); } query orderedQuery; } // 5. 应用分页 if (Skip.HasValue) { query query.Skip(Skip.Value); } if (Take.HasValue) { query query.Take(Take.Value); } // 6. 应用跟踪行为 if (IsNoTracking) { query query.AsNoTracking(); } return query; } } }这个基类完成了大部分繁重的工作。它维护了规约的状态条件、包含、排序等并在Apply方法中按正确顺序将它们应用到IQueryable上。注意排序的处理第一个排序使用OrderBy/OrderByDescending后续的排序使用ThenBy/ThenByDescending。3.2 创建具体的规约类现在我们可以创建代表具体业务查询的规约类了。每个类继承自BaseSpecificationT并在构造函数中通过调用基类的保护方法如AddCriteria,AddInclude来定义其行为。让我们实现文章开头提到的几个查询// Specifications/ProductSpecs/ActiveProductSpecification.cs namespace EfCoreSpecificationDemo.Specifications.ProductSpecs { public class ActiveProductSpecification : BaseSpecificationProduct { public ActiveProductSpecification() { AddCriteria(p p.IsActive); // 默认按名称排序 AddOrderBy(p p.Name); } } } // Specifications/ProductSpecs/ProductsByCategoryAndPriceSpecification.cs namespace EfCoreSpecificationDemo.Specifications.ProductSpecs { public class ProductsByCategoryAndPriceSpecification : BaseSpecificationProduct { public ProductsByCategoryAndPriceSpecification(int categoryId, decimal minPrice) { AddCriteria(p p.CategoryId categoryId p.Price minPrice); AddInclude(p p.Supplier); // 包含供应商信息 AddOrderBy(p p.Price, descending: true); // 按价格降序 } } } // Specifications/ProductSpecs/ProductSearchSpecification.cs namespace EfCoreSpecificationDemo.Specifications.ProductSpecs { public class ProductSearchSpecification : BaseSpecificationProduct { public ProductSearchSpecification(string? searchTerm, bool? isActive, int? categoryId) { // 动态构建查询条件 if (!string.IsNullOrWhiteSpace(searchTerm)) { // 注意字符串查询在数据库中的性能生产环境考虑全文搜索 AddCriteria(p p.Name.Contains(searchTerm) || (p.Description ! null p.Description.Contains(searchTerm))); } if (isActive.HasValue) { // 组合条件这里使用逻辑与AND。更复杂的组合需要 PredicateBuilder。 var existingCriteria Criteria; if (existingCriteria null) { AddCriteria(p p.IsActive isActive.Value); } else { // 简化处理实际项目应使用 Expression.AndAlso 组合 // 此处为演示我们假设只处理一个简单条件组合复杂情况建议使用第三方库或 PredicateBuilder AddCriteria(p existingCriteria.Compile()(p) p.IsActive isActive.Value); } } if (categoryId.HasValue) { // 同上条件组合 var existingCriteria Criteria; if (existingCriteria null) { AddCriteria(p p.CategoryId categoryId.Value); } else { AddCriteria(p existingCriteria.Compile()(p) p.CategoryId categoryId.Value); } } // 默认包含分类信息并排序 AddInclude(p p.Category); AddOrderBy(p p.Name); ApplyNoTracking(); // 搜索列表通常不需要跟踪 } } } // Specifications/ProductSpecs/PaginatedProductSpecification.cs namespace EfCoreSpecificationDemo.Specifications.ProductSpecs { public class PaginatedProductSpecification : BaseSpecificationProduct { public PaginatedProductSpecification(int pageNumber, int pageSize) { int skip (pageNumber - 1) * pageSize; ApplyPaging(skip, pageSize); AddOrderBy(p p.Id); // 分页必须有确定的排序 ApplyNoTracking(); } } }注意ProductSearchSpecification中动态构建条件的部分。我们演示了简单的条件叠加但在生产环境中直接使用Compile()和委托组合会破坏IQueryable的表达式树特性导致查询无法被翻译成 SQL。正确的做法是使用System.Linq.Expressions命名空间下的Expression类方法如Expression.AndAlso来动态构建表达式树或者使用像LinqKit库中的PredicateBuilder这样的工具。为了保持示例清晰这里仅作示意后文会讨论更健壮的实现。3.3 构建通用仓储Repository有了规约我们需要一个能使用规约的仓储。我们将创建一个通用的RepositoryT和一个针对特定实体的IProductRepository作为示例。首先定义通用仓储接口// Data/IRepository.cs using System.Linq.Expressions; using EfCoreSpecificationDemo.Specifications; namespace EfCoreSpecificationDemo.Data { public interface IRepositoryT where T : class { TaskT? GetByIdAsync(int id); TaskIReadOnlyListT ListAllAsync(); TaskIReadOnlyListT ListAsync(ISpecificationT specification); // 计数和存在性检查也很有用 Taskint CountAsync(ISpecificationT? specification null); Taskbool AnyAsync(ISpecificationT? specification null); // 添加、更新、删除这些通常属于工作单元模式但简单起见放在这里 TaskT AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entity); } }然后实现基于 EF Core 的通用仓储// Data/Repository.cs using Microsoft.EntityFrameworkCore; using System.Linq.Expressions; using EfCoreSpecificationDemo.Specifications; namespace EfCoreSpecificationDemo.Data { public class RepositoryT : IRepositoryT where T : class { protected readonly AppDbContext _dbContext; protected readonly DbSetT _dbSet; public Repository(AppDbContext dbContext) { _dbContext dbContext; _dbSet _dbContext.SetT(); } public virtual async TaskT? GetByIdAsync(int id) { return await _dbSet.FindAsync(id); } public virtual async TaskIReadOnlyListT ListAllAsync() { return await _dbSet.ToListAsync(); } // 核心方法使用规约查询 public virtual async TaskIReadOnlyListT ListAsync(ISpecificationT specification) { // 从 DbSet 开始 var query _dbSet.AsQueryable(); // 应用规约 query specification.Apply(query); // 执行查询 return await query.ToListAsync(); } public virtual async Taskint CountAsync(ISpecificationT? specification null) { var query _dbSet.AsQueryable(); if (specification ! null) { // 注意计数时通常忽略排序和分页但保留查询条件 // 我们可以创建一个只包含条件的简化规约或修改 Apply 方法逻辑。 // 这里简单应用整个规约但排序和分页对计数无影响。 query specification.Apply(query); } return await query.CountAsync(); } public virtual async Taskbool AnyAsync(ISpecificationT? specification null) { var query _dbSet.AsQueryable(); if (specification ! null) { query specification.Apply(query); } return await query.AnyAsync(); } public virtual async TaskT AddAsync(T entity) { await _dbSet.AddAsync(entity); await _dbContext.SaveChangesAsync(); return entity; } public virtual async Task UpdateAsync(T entity) { _dbContext.Entry(entity).State EntityState.Modified; await _dbContext.SaveChangesAsync(); } public virtual async Task DeleteAsync(T entity) { _dbSet.Remove(entity); await _dbContext.SaveChangesAsync(); } } }最后可以创建一个产品专用的仓储接口和实现它除了通用方法还可以包含一些高度复用或复杂的、基于规约组合的查询。// Data/IProductRepository.cs using EfCoreSpecificationDemo.Models; using EfCoreSpecificationDemo.Specifications; namespace EfCoreSpecificationDemo.Data { public interface IProductRepository : IRepositoryProduct { // 可以添加一些产品特有的查询方法它们内部使用规约 TaskIReadOnlyListProduct GetActiveProductsWithSupplierAsync(); // 或者直接返回 IQueryable 供服务层组合另一种灵活的方式 IQueryableProduct ApplySpecification(ISpecificationProduct specification); } } // Data/ProductRepository.cs using EfCoreSpecificationDemo.Models; using EfCoreSpecificationDemo.Specifications; using Microsoft.EntityFrameworkCore; namespace EfCoreSpecificationDemo.Data { public class ProductRepository : RepositoryProduct, IProductRepository { public ProductRepository(AppDbContext dbContext) : base(dbContext) { } public async TaskIReadOnlyListProduct GetActiveProductsWithSupplierAsync() { var spec new ActiveProductSpecification(); // 在现有规约基础上动态添加 Include // 这里演示了规约的灵活性你也可以创建一个新的复合规约 var query ApplySpecification(spec); query query.Include(p p.Supplier); // 额外包含 return await query.ToListAsync(); } public IQueryableProduct ApplySpecification(ISpecificationProduct specification) { var query _dbSet.AsQueryable(); return specification.Apply(query); } } }在Program.cs中注册这些服务builder.Services.AddScoped(typeof(IRepository), typeof(Repository)); builder.Services.AddScopedIProductRepository, ProductRepository();4. 在服务层与控制器中使用规约现在我们可以在应用的其他部分使用规约了。创建一个服务类来处理产品相关的业务逻辑。// Services/ProductService.cs using EfCoreSpecificationDemo.Data; using EfCoreSpecificationDemo.Models; using EfCoreSpecificationDemo.Specifications.ProductSpecs; namespace EfCoreSpecificationDemo.Services { public class ProductService { private readonly IProductRepository _productRepository; public ProductService(IProductRepository productRepository) { _productRepository productRepository; } public async TaskIReadOnlyListProduct GetActiveProductsAsync() { var spec new ActiveProductSpecification(); return await _productRepository.ListAsync(spec); } public async TaskIReadOnlyListProduct SearchProductsAsync(string? keyword, bool? isActive, int? categoryId, int pageNumber 1, int pageSize 20) { // 创建搜索规约 var searchSpec new ProductSearchSpecification(keyword, isActive, categoryId); // 创建分页规约注意排序已在 searchSpec 中定义 // 我们需要组合两个规约。一种方式是创建一个新的组合规约或者修改 BaseSpecification 以支持组合。 // 简单起见这里我们先应用搜索规约得到 IQueryable再手动分页。 var query _productRepository.ApplySpecification(searchSpec); // 手动应用分页因为排序已在 searchSpec 中 var pagedQuery query.Skip((pageNumber - 1) * pageSize).Take(pageSize); return await pagedQuery.ToListAsync(); } public async Task(IReadOnlyListProduct Items, int TotalCount) SearchProductsWithCountAsync(string? keyword, bool? isActive, int? categoryId, int pageNumber 1, int pageSize 20) { var searchSpec new ProductSearchSpecification(keyword, isActive, categoryId); var query _productRepository.ApplySpecification(searchSpec); var totalCount await query.CountAsync(); var items await query.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync(); return (items, totalCount); } public async TaskIReadOnlyListProduct GetExpensiveProductsInCategoryAsync(int categoryId, decimal minPrice) { var spec new ProductsByCategoryAndPriceSpecification(categoryId, minPrice); return await _productRepository.ListAsync(spec); } } }注册服务builder.Services.AddScopedProductService();最后在一个 API 控制器中调用服务// Controllers/ProductsController.cs using Microsoft.AspNetCore.Mvc; using EfCoreSpecificationDemo.Services; namespace EfCoreSpecificationDemo.Controllers { [ApiController] [Route(api/[controller])] public class ProductsController : ControllerBase { private readonly ProductService _productService; public ProductsController(ProductService productService) { _productService productService; } [HttpGet(active)] public async TaskIActionResult GetActiveProducts() { var products await _productService.GetActiveProductsAsync(); return Ok(products); } [HttpGet(search)] public async TaskIActionResult SearchProducts( [FromQuery] string? keyword, [FromQuery] bool? isActive, [FromQuery] int? categoryId, [FromQuery] int page 1, [FromQuery] int pageSize 20) { var result await _productService.SearchProductsWithCountAsync(keyword, isActive, categoryId, page, pageSize); // 通常会将结果包装在一个包含分页元数据的对象中 return Ok(new { Data result.Items, TotalCount result.TotalCount, Page page, PageSize pageSize, TotalPages (int)Math.Ceiling(result.TotalCount / (double)pageSize) }); } [HttpGet(category/{categoryId}/expensive)] public async TaskIActionResult GetExpensiveProducts(int categoryId, [FromQuery] decimal minPrice 100) { var products await _productService.GetExpensiveProductsInCategoryAsync(categoryId, minPrice); return Ok(products); } } }运行项目并访问相应的 API 端点如/api/products/active你将看到规约模式在起作用。数据库查询被整洁地封装在规约类中控制器和服务层的代码变得非常清晰只关注业务意图而非数据访问细节。5. 高级主题与生产环境优化基础的规约模式已经能解决大部分问题但在生产环境中我们还需要考虑更多。5.1 动态组合查询条件如前所述简单的条件叠加会破坏IQueryable。推荐使用LinqKit库的PredicateBuilder或自己实现表达式组合。以下是使用LinqKit的示例首先安装包dotnet add package LinqKit修改BaseSpecification或创建一个新的构建器类来支持动态 AND/OR 组合// Specifications/SpecificationBuilder.cs (示例) using System.Linq.Expressions; using LinqKit; namespace EfCoreSpecificationDemo.Specifications { public class SpecificationBuilderT { private ExpressionFuncT, bool? _predicate; public SpecificationBuilderT And(ExpressionFuncT, bool expression) { _predicate _predicate null ? expression : _predicate.And(expression.Expand()); return this; } public SpecificationBuilderT Or(ExpressionFuncT, bool expression) { _predicate _predicate null ? expression : _predicate.Or(expression.Expand()); return this; } public ExpressionFuncT, bool? Build() _predicate; } }然后在动态规约中使用var builder new SpecificationBuilderProduct(); if (!string.IsNullOrWhiteSpace(searchTerm)) { builder.And(p p.Name.Contains(searchTerm)); } if (isActive.HasValue) { builder.And(p p.IsActive isActive.Value); } // ... 最终将 builder.Build() 赋值给规约的 Criteria5.2 规约的组合与复用有时我们需要复用多个规约的逻辑。我们可以通过让规约支持“组合”来实现。例如一个AndSpecification可以组合两个规约的条件。// Specifications/CompositeSpecification.cs public class AndSpecificationT : BaseSpecificationT { public AndSpecification(ISpecificationT left, ISpecificationT right) { // 使用 PredicateBuilder 组合条件 var leftCriteria left.Criteria; var rightCriteria right.Criteria; if (leftCriteria ! null rightCriteria ! null) { Criteria leftCriteria.And(rightCriteria); } else { Criteria leftCriteria ?? rightCriteria; } // 合并 Includes, OrderBys 等需要更复杂的合并逻辑 IncludesInternal.AddRange(left.Includes); IncludesInternal.AddRange(right.Includes); // 注意排序合并需要谨慎定义优先级 } }5.3 性能考量AsNoTracking 与查询优化AsNoTracking对于只读查询务必使用AsNoTracking。这可以显著提升查询性能因为 EF Core 不需要为实体创建变更跟踪信息。我们在ProductSearchSpecification和PaginatedProductSpecification中已经使用了它。选择性包含Include规约中的Include要谨慎。过度包含会导致 SQL 查询产生巨大的笛卡尔积严重影响性能。应根据实际返回的数据需求来决定包含哪些导航属性。投影Select如果只需要实体的部分字段应在规约或查询的最后使用Select进行投影而不是返回整个实体。这可以减少数据传输量和内存占用。可以在ISpecification接口中增加Selector属性来实现。5.4 分页与总数查询的优化在SearchProductsWithCountAsync方法中我们执行了两次数据库查询一次Count一次Skip/Take。对于大数据集这是标准的做法。确保Count查询没有不必要的Include和OrderBy我们的Apply方法在应用规约时Count查询会忽略排序和分页但Include可能会被转换如LEFT JOIN影响性能。一个更精细的实现是为计数创建一个只包含Criteria的规约。6. 常见问题排查与最佳实践6.1 常见问题排查表问题现象可能原因检查与解决方式查询结果不符合预期缺少数据规约中的Criteria条件过于严格或逻辑错误。1. 检查规约类构造函数的参数是否正确传递。2. 在Apply方法开始处使用调试器或日志输出生成的 SQL 语句query.ToQueryString()。3. 检查动态条件组合逻辑AND/OR是否正确。查询包含Include的导航属性为null规约中的Includes列表未正确添加或使用了错误的表达式。1. 确认AddInclude方法被调用且表达式正确如p p.Supplier。2. 对于多级包含如p p.Supplier.Address确保使用正确的表达式或字符串包含。3. 检查数据库中外键关系和数据是否存在。分页顺序不稳定或重复分页规约未指定排序或排序字段不唯一导致不同页数据重叠。强制要求任何分页查询必须在规约中至少指定一个排序条件最好是唯一或高区分度的字段如Id。修改PaginatedProductSpecification确保有AddOrderBy。查询性能缓慢1. 未使用AsNoTracking。2. 包含了过多或不必要的Include。3. 查询条件未命中索引。1. 对只读查询启用IsNoTracking。2. 审查规约中的Includes按需加载。3. 检查生成的 SQL使用 EF Core 日志分析执行计划确保关键字段如CategoryId,IsActive有数据库索引。动态条件组合导致异常或客户端评估使用了Compile()和委托组合条件破坏了表达式树。使用LinqKit的PredicateBuilder或System.Linq.Expressions.Expression类的方法来动态构建表达式树确保最终条件是一个ExpressionFuncT, bool。Skip/Take在 SQL Server 2008 或更低版本报错EF Core 对旧版本 SQL Server 分页的支持问题。确保数据库版本支持OFFSET FETCHSQL Server 2012。对于旧版本需使用不同的分页策略如基于键的分页。6.2 最佳实践清单一规约一职责每个规约类应代表一个明确的、可复用的查询意图。避免创建“万能”规约。命名清晰规约类名应明确表达其意图如ActiveProductsWithSupplierSpecification。强制分页排序在创建分页规约时必须在构造函数中强制添加排序条件以避免不可预测的结果。默认只读查询不跟踪在规约基类或具体规约中对于明显是只读列表查询的场景默认设置IsNoTracking true。谨慎使用Include只在规约中添加当前业务场景必需的Include。考虑使用投影Select来替代加载完整实体图。单元测试规约规约是纯逻辑类不依赖DbContext非常适合单元测试。可以测试其Criteria、Includes等属性是否正确设置。仓储层保持精简通用仓储RepositoryT应只提供最基础的 CRUD 和按规约查询的方法。复杂的业务查询组合应在服务层通过规约组合或调用多个规约来完成。考虑规约的序列化如果你需要在 API 层直接接收规约例如前端传递复杂的过滤条件需要设计一个 DTO 并将其映射到规约而不是直接序列化/反序列化规约对象因为表达式树不易序列化。日志记录在开发环境记录规约应用后生成的 SQL 语句这对于调试复杂查询至关重要。7. 总结与扩展方向通过实现和应用规范模式我们成功地将 EF Core 查询逻辑从分散的、难以维护的仓储方法中解放出来封装成一个个独立、可组合、可测试的规约对象。这不仅使代码更加清晰也极大地提升了复杂查询逻辑的复用性。回顾一下我们构建的核心组件ISpecificationT接口定义了规约的契约。BaseSpecificationT基类提供了默认实现和流畅的构建 API。具体规约类如ActiveProductSpecification封装了具体的业务查询逻辑。通用仓储RepositoryT提供按规约查询的通用能力。服务层协调规约和仓储实现业务用例。要进一步完善这个模式你可以探索以下方向规约求值器将Apply逻辑抽离到一个单独的SpecificationEvaluator类中使BaseSpecification只负责存储状态更符合单一职责原则。规约的序列化/反序列化设计一套机制将前端的过滤、排序、分页参数自动转换为规约对象。与 CQRS 结合在命令查询职责分离架构中规约模式非常适合在查询端Query Side构建复杂的只读查询。更强大的组合实现OrSpecification、NotSpecification等并提供运算符重载如spec1 spec2来组合规约。将规范模式引入你的 EF Core 项目初期可能会增加一些类但长远来看它为数据访问层带来的结构清晰度、可测试性和可维护性收益是巨大的。开始清理你的查询代码吧从一个具体的业务查询开始尝试将其重构成一个规约类你会立刻感受到它的威力。