Skip to content

仓储模式(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();
    }
}

我的建议 ​

✅ 使用仓储模式的情况 ​

  1. 企业级应用: 需要清晰的架构分层
  2. 团队协作: 多个开发者并行工作
  3. 复杂查询: 需要封装和复用查询逻辑
  4. 测试驱动: 需要大量单元测试
  5. 多数据源: 可能需要切换数据库实现

❌ 不使用仓储模式的情况 ​

  1. 小型项目: CRUD 为主,无需复杂架构
  2. 快速原型: 需要快速迭代验证想法
  3. 简单 API: 主要是基本的增删改查
  4. 单体应用: 不太可能更换技术栈
  5. 初创项目: 过早优化会增加负担

🎯 我的决策 ​

对于大多数现代 .NET 应用,我推荐:

小型项目 → 直接使用 DbContext + 规范模式
中型项目 → 选择性使用仓储(仅对复杂聚合)
大型项目 → 仓储模式 + 工作单元 + CQRS

关键原则:

  • 务实: 根据实际需求选择,不要盲目追随最佳实践
  • 渐进: 从简单开始,随着复杂度增长再引入抽象
  • 一致: 一旦选择,整个项目保持一致
  • 文档化: 记录你的选择及原因

记住:好的架构是演进出来的,不是设计出来的!

基于 MIT 许可发布