Appearance
仓储模式(Repository)是否需要?
概述
仓储模式(Repository Pattern)是 EF Core 项目中最具争议的话题之一。有人认为它是必要的抽象层,有人则认为它是不必要的过度设计。本文将深入分析仓储模式的优缺点,并提供实用的决策指南。
什么是仓储模式?
仓储模式在领域层和数据映射层之间充当集合-like 的接口,它:
- 提供对象集合的抽象
- 封装数据访问逻辑
- 解耦业务逻辑与数据访问技术
方案对比
方案 A: 使用仓储模式
csharp
// 定义接口
public interface IRepository<T> where T : BaseEntity
{
Task<T?> GetByIdAsync(int id);
Task<List<T>> GetAllAsync();
Task AddAsync(T entity);
void Update(T entity);
void Delete(T entity);
Task SaveChangesAsync();
}
// 实现
public class ProductRepository : IRepository<Product>
{
private readonly AppDbContext _context;
public ProductRepository(AppDbContext context)
{
_context = context;
}
public async Task<Product?> GetByIdAsync(int id)
{
return await _context.Products.FindAsync(id);
}
// ... 其他方法
}
// 使用
public class ProductService
{
private readonly IRepository<Product> _repository;
public ProductService(IRepository<Product> repository)
{
_repository = repository;
}
public async Task<ProductDto?> GetProduct(int id)
{
var product = await _repository.GetByIdAsync(id);
return product != null ? MapToDto(product) : null;
}
}优势:
- ✅ 可测试性: 轻松 Mock 仓储进行单元测试
- ✅ 可替换性: 可以轻松切换数据库实现
- ✅ 关注点分离: 业务逻辑与数据访问分离
- ✅ DRY 原则: 避免重复的数据访问代码
劣势:
- ❌ 额外复杂度: 增加了抽象层
- ❌ 功能受限: 无法充分利用 EF Core 的所有功能
- ❌ 维护成本: 需要维护额外的接口和实现
- ❌ 可能泄漏抽象: DbContext 仍可能暴露
方案 B: 直接使用 DbContext
csharp
// 直接使用 DbContext
public class ProductService
{
private readonly AppDbContext _context;
public ProductService(AppDbContext context)
{
_context = context;
}
public async Task<ProductDto?> GetProduct(int id)
{
var product = await _context.Products.FindAsync(id);
return product != null ? MapToDto(product) : null;
}
public async Task<List<ProductDto>> GetProductsByCategory(int categoryId)
{
return await _context.Products
.Include(p => p.Category)
.Where(p => p.CategoryId == categoryId)
.Select(p => MapToDto(p))
.ToListAsync();
}
}优势:
- ✅ 简单直接: 减少抽象层
- ✅ 功能完整: 充分利用 EF Core 能力
- ✅ 性能透明: 更容易优化查询
- ✅ 学习曲线低: 新开发者容易理解
劣势:
- ❌ 耦合度高: 业务逻辑依赖 EF Core
- ❌ 测试复杂: 需要使用 InMemory 或 SQLite
- ❌ 难以替换: 切换到其他 ORM 困难
- ❌ 可能重复: 相似的查询逻辑可能重复
决策矩阵
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 小型项目 (< 10 实体) | 直接使用 DbContext | 简单高效,无需过度设计 |
| 中型项目 (10-30 实体) | 可选仓储模式 | 根据团队偏好决定 |
| 大型项目 (> 30 实体) | 仓储模式 + UoW | 需要良好的架构设计 |
| 多数据库支持 | 仓储模式 | 需要抽象不同数据库实现 |
| 团队有多个项目组 | 仓储模式 | 便于并行开发和测试 |
| 快速原型开发 | 直接使用 DbContext | 减少前期设计成本 |
| 长期维护项目 | 仓储模式 | 提高代码可维护性 |
| DDD 项目 | 仓储模式 | DDD 要求领域层独立 |
| CRUD 为主 | 直接使用 DbContext | 仓储带来的价值有限 |
| 复杂业务逻辑 | 仓储模式 | 更好地隔离和测试 |
折中方案: 规范模式(Specification)
如果你不确定是否使用仓储模式,可以考虑规范模式作为折中方案:
csharp
// 规范模式: 封装查询逻辑
public class ProductSpecifications
{
public static Expression<Func<Product, bool>> IsActive()
{
return p => !p.IsDeleted && p.Stock > 0;
}
public static Expression<Func<Product, bool>> InCategory(int categoryId)
{
return p => p.CategoryId == categoryId;
}
public static IQueryable<Product> WithIncludes(this DbSet<Product> products)
{
return products.Include(p => p.Category)
.Include(p => p.Reviews);
}
}
// 使用
public class ProductService
{
private readonly AppDbContext _context;
public ProductService(AppDbContext context)
{
_context = context;
}
public async Task<List<ProductDto>> GetActiveProductsInCategory(int categoryId)
{
var products = await _context.Products
.WithIncludes()
.Where(ProductSpecifications.IsActive())
.Where(ProductSpecifications.InCategory(categoryId))
.ToListAsync();
return products.Select(MapToDto).ToList();
}
}优势:
- ✅ 保留查询灵活性
- ✅ 封装常用查询逻辑
- ✅ 不增加过多复杂度
- ✅ 易于单元测试(可以单独测试规范)
实际案例研究
案例 1: 电商系统(推荐使用仓储)
csharp
// 复杂的业务规则和多数据库需求
public interface IOrderRepository : IRepository<Order>
{
Task<Order?> GetByIdWithItemsAsync(int orderId);
Task<List<Order>> GetOrdersByCustomerAsync(int customerId, DateTime? startDate = null);
Task<decimal> GetTotalSalesAsync(DateTime startDate, DateTime endDate);
Task UpdateStatusAsync(int orderId, OrderStatus status);
}
public class OrderRepository : BaseRepository<Order>, IOrderRepository
{
public OrderRepository(AppDbContext context) : base(context) { }
public async Task<Order?> GetByIdWithItemsAsync(int orderId)
{
return await _context.Orders
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.FirstOrDefaultAsync(o => o.Id == orderId);
}
public async Task<List<Order>> GetOrdersByCustomerAsync(
int customerId,
DateTime? startDate = null)
{
var query = _context.Orders
.Include(o => o.OrderItems)
.Where(o => o.CustomerId == customerId);
if (startDate.HasValue)
query = query.Where(o => o.OrderDate >= startDate.Value);
return await query.OrderByDescending(o => o.OrderDate).ToListAsync();
}
public async Task<decimal> GetTotalSalesAsync(DateTime startDate, DateTime endDate)
{
return await _context.Orders
.Where(o => o.OrderDate >= startDate && o.OrderDate <= endDate)
.Where(o => o.Status == OrderStatus.Completed)
.SumAsync(o => o.TotalAmount);
}
public async Task UpdateStatusAsync(int orderId, OrderStatus status)
{
await _context.Database.ExecuteSqlInterpolatedAsync(
$"UPDATE Orders SET Status = {status} WHERE Id = {orderId}");
}
}为什么使用仓储?
- 复杂的查询逻辑需要封装
- 多个服务共享相同的查询模式
- 需要单元测试业务逻辑
- 可能需要切换到不同的数据库
案例 2: 博客系统(不推荐仓储)
csharp
// 简单的 CRUD 操作
public class BlogService
{
private readonly AppDbContext _context;
public BlogService(AppDbContext context)
{
_context = context;
}
public async Task<List<PostDto>> GetRecentPostsAsync(int count = 10)
{
return await _context.Posts
.Include(p => p.Author)
.Include(p => p.Comments)
.OrderByDescending(p => p.PublishedAt)
.Take(count)
.Select(p => new PostDto(
p.Id,
p.Title,
p.Content,
p.Author.Name,
p.Comments.Count,
p.PublishedAt))
.ToListAsync();
}
public async Task CreatePostAsync(CreatePostRequest request)
{
var post = new Post
{
Title = request.Title,
Content = request.Content,
AuthorId = request.AuthorId,
PublishedAt = DateTime.UtcNow
};
_context.Posts.Add(post);
await _context.SaveChangesAsync();
}
}为什么不使用仓储?
- 查询相对简单,直接使用 LINQ 更清晰
- 没有复杂的业务逻辑需要隔离
- 不太可能需要更换数据库
- 仓储会增加不必要的复杂度
常见反模式
❌ 反模式 1: 泄露的抽象
csharp
// 错误: 仓储暴露了 DbContext
public class ProductRepository
{
public AppDbContext Context { get; } // 不要这样做!
public IQueryable<Product> Products => Context.Products; // 泄漏抽象
}
// 正确: 只暴露必要的方法
public interface IProductRepository
{
Task<List<Product>> GetActiveProductsAsync();
// 不暴露 IQueryable 或 DbContext
}❌ 反模式 2: 通用仓储过度泛化
csharp
// 错误: 试图创建一个万能仓储
public interface IGenericRepository<T> where T : class
{
Task<T> GetByIdAsync<TKey>(TKey id);
Task<List<T>> FindAsync(Expression<Func<T, bool>> predicate);
Task<List<T>> GetAllAsync();
Task AddAsync(T entity);
Task AddRangeAsync(IEnumerable<T> entities);
void Update(T entity);
void UpdateRange(IEnumerable<T> entities);
void Delete(T entity);
void DeleteRange(IEnumerable<T> entities);
Task<int> CountAsync(Expression<Func<T, bool>> predicate);
Task<bool> ExistsAsync(Expression<Func<T, bool>> predicate);
IQueryable<T> Query(); // 又回到了泄漏抽象的问题
}
// 正确: 为每个实体定义特定接口
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id);
Task<List<Product>> GetByCategoryAsync(int categoryId);
Task<List<Product>> GetLowStockProductsAsync();
// 具体的方法名,清晰表达意图
}❌ 反模式 3: 仓储变成事务脚本
csharp
// 错误: 在仓储中编写业务逻辑
public class OrderRepository
{
public async Task ProcessOrderAsync(int orderId)
{
var order = await GetByIdAsync(orderId);
// 业务逻辑不应该在仓储中
if (order.TotalAmount > 1000)
{
order.ApplyDiscount(0.1m);
}
// 发送通知等副作用
await _emailService.SendNotification(order);
}
}
// 正确: 仓储只负责数据访问
public class OrderService
{
public async Task ProcessOrderAsync(int orderId)
{
var order = await _orderRepository.GetByIdAsync(orderId);
// 业务逻辑在服务层
if (order.TotalAmount > 1000)
{
order.ApplyDiscount(0.1m);
}
_orderRepository.Update(order);
await _orderRepository.SaveChangesAsync();
}
}现代替代方案
1. CQRS + MediatR
csharp
// 查询直接返回 DTO,不需要仓储
public class GetProductQueryHandler
: IRequestHandler<GetProductQuery, ProductDto>
{
private readonly AppDbContext _context;
public GetProductQueryHandler(AppDbContext context)
{
_context = context;
}
public async Task<ProductDto> Handle(GetProductQuery request, CancellationToken ct)
{
return await _context.Products
.Where(p => p.Id == request.Id)
.Select(p => new ProductDto(p.Id, p.Name, p.Price))
.FirstOrDefaultAsync(ct);
}
}
// 命令使用轻量级仓储或直接操作
public class CreateProductCommandHandler
: IRequestHandler<CreateProductCommand, int>
{
private readonly AppDbContext _context;
public async Task<int> Handle(CreateProductCommand request, CancellationToken ct)
{
var product = new Product
{
Name = request.Name,
Price = request.Price
};
_context.Products.Add(product);
await _context.SaveChangesAsync(ct);
return product.Id;
}
}2. 活动记录模式(Active Record)
csharp
// 实体自己负责持久化(适合简单场景)
public class Product : BaseEntity
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
// 静态方法创建
public static async Task<Product> CreateAsync(string name, decimal price)
{
var product = new Product { Name = name, Price = price };
await using var context = ServiceLocator.Get<AppDbContext>();
context.Products.Add(product);
await context.SaveChangesAsync();
return product;
}
// 实例方法更新
public async Task UpdatePriceAsync(decimal newPrice)
{
Price = newPrice;
await using var context = ServiceLocator.Get<AppDbContext>();
context.Products.Update(this);
await context.SaveChangesAsync();
}
}我的建议
✅ 使用仓储模式的情况
- 企业级应用: 需要清晰的架构分层
- 团队协作: 多个开发者并行工作
- 复杂查询: 需要封装和复用查询逻辑
- 测试驱动: 需要大量单元测试
- 多数据源: 可能需要切换数据库实现
❌ 不使用仓储模式的情况
- 小型项目: CRUD 为主,无需复杂架构
- 快速原型: 需要快速迭代验证想法
- 简单 API: 主要是基本的增删改查
- 单体应用: 不太可能更换技术栈
- 初创项目: 过早优化会增加负担
🎯 我的决策
对于大多数现代 .NET 应用,我推荐:
小型项目 → 直接使用 DbContext + 规范模式
中型项目 → 选择性使用仓储(仅对复杂聚合)
大型项目 → 仓储模式 + 工作单元 + CQRS关键原则:
- 务实: 根据实际需求选择,不要盲目追随最佳实践
- 渐进: 从简单开始,随着复杂度增长再引入抽象
- 一致: 一旦选择,整个项目保持一致
- 文档化: 记录你的选择及原因
记住:好的架构是演进出来的,不是设计出来的!