这篇文章继续挑战 .NET 项目里常见的 Repository Pattern。它的观点和前面那篇“删除 Repository Pattern”相近,但角度更直接:EF Core 本身已经提供了很强的查询抽象,为什么还要再创建一层低配仓储接口?
作者并不是说所有 Repository 都错,而是在质疑一种常见做法:为每个实体创建仓储,然后写一堆 GetAll、GetById、GetWhereSomePropIs。这种做法通常不会让系统更清楚,反而会限制 LINQ 的表达力。
为什么团队喜欢 Repository很多团队选择 Repository 的理由包括:
• 便于 mock。• 隔离数据库。• 保持业务层不依赖 EF Core。• 未来可能替换 ORM。• 看起来符合分层架构。这些理由听起来都合理,但需要落到具体项目里检查。尤其是“未来可能替换 ORM”,在大多数业务系统中发生概率很低。为了一个低概率变化,让日常查询变得复杂,不一定值得。
典型问题:仓储接口很快膨胀看一个常见商品仓储。
public interface IProductRepository
{
Task GetByIdAsync(Guid id, CancellationToken cancellationToken);
Task> GetAllAsync(CancellationToken cancellationToken);
Task> GetByCategoryAsync(Guid categoryId, CancellationToken cancellationToken);
Task> GetActiveAsync(CancellationToken cancellationToken);
Task> GetActiveByCategoryAsync(Guid categoryId, CancellationToken cancellationToken);
Task> GetActiveByCategoryOrderByPriceAsync(Guid categoryId, CancellationToken cancellationToken);
}一开始只是几个方法,后来业务条件增加,接口就会不断膨胀。如果还要支持搜索、分页、排序、价格区间、库存状态,方法数量会快速失控。
这本质上是在用方法名重新发明 LINQ。
EF Core 查询本来就能表达组合条件使用 DbContext 时,可以按条件组合查询。
public sealed class ProductQueryService
{
private readonly AppDbContext _dbContext;
public ProductQueryService(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public Task> SearchAsync(
ProductSearchRequest request,
CancellationToken cancellationToken)
{
var query = _dbContext.Products
.AsNoTracking
.Where(product => product.IsActive);
if (request.CategoryId is not null)
{
query = query.Where(product => product.CategoryId == request.CategoryId);
}
if (!string.IsNullOrWhiteSpace(request.Keyword))
{
query = query.Where(product => product.Name.Contains(request.Keyword));
}
if (request.MinPrice is not null)
{
query = query.Where(product => product.Price >= request.MinPrice);
}
if (request.MaxPrice is not null)
{
query = query.Where(product => product.Price query.OrderBy(product => product.Price),
ProductSortBy.PriceDesc => query.OrderByDescending(product => product.Price),
_ => query.OrderBy(product => product.Name)
};
return query
.Skip((request.PageIndex - 1) * request.PageSize)
.Take(request.PageSize)
.Select(product => new ProductListItemDto
{
Id = product.Id,
Name = product.Name,
Price = product.Price,
Stock = product.Stock
})
.ToListAsync(cancellationToken);
}
}这段代码虽然长,但查询意图非常直接。你能看到每个条件如何参与 SQL 生成。如果把它拆成多个仓储方法,反而要在调用方和实现之间来回跳转。
IQueryable 到底能不能暴露文章中提到 IQueryable 的表达能力。这里需要谨慎。
直接从 Repository 暴露 IQueryable 有优点:调用方可以自由组合查询。但也有风险:查询逻辑可能散落到各层,甚至 Controller 里到处拼查询。
一种折中是:不做泛型 Repository,但也不把 IQueryable 随便传到 UI 层。把查询集中在应用层查询服务或查询对象中。
public sealed record ProductSearchRequest(
Guid? CategoryId,
string? Keyword,
decimal? MinPrice,
decimal? MaxPrice,
ProductSortBy SortBy,
int PageIndex,
int PageSize);
public enum ProductSortBy
{
Name,
PriceAsc,
PriceDesc
}这样查询仍由 EF Core 表达,但边界是清楚的。
测试:不要只 mock 仓储返回值Repository 常被认为便于测试,但它也容易制造假信心。
如果你 mock GetActiveByCategoryOrderByPriceAsync 返回一个列表,测试只能证明服务层处理了这个列表。它不能证明真实 SQL 是否正确,也不能证明排序、分页、包含关系是否正确。
EF Core 查询更适合使用集成测试覆盖。
public sealed class ProductQueryServiceTests
{
public async Task Search_Should_Filter_And_Sort_Products
{
await using var dbContext = TestDbContextFactory.Create;
var categoryId = Guid.NewGuid;
dbContext.Products.AddRange(
Product.Create("Keyboard", categoryId, 300m, stock: 10),
Product.Create("Mouse", categoryId, 120m, stock: 20),
Product.Create("Monitor", Guid.NewGuid, 900m, stock: 5));
await dbContext.SaveChangesAsync;
var service = new ProductQueryService(dbContext);
var result = await service.SearchAsync(
new ProductSearchRequest(
categoryId,
null,
null,
null,
ProductSortBy.PriceAsc,
1,
20),
CancellationToken.None);
if (result.Count != 2




