在.NET生态中,鲜有话题能像**仓储模式(Repository Pattern)**这般引发激烈争论。有些开发者奉其为整洁架构的基石,另一些人则认为它是被Entity Framework Core(EF Core)淘汰的冗余设计。
那么,仓储模式真的消亡了吗?还是说它正在进化?本文将剖析正反双方观点,探讨其适用场景,并回答是否应在你的下一个.NET项目中采用它。
仓储模式是一种抽象数据访问的设计模式,旨在简化管理和测试。通过创建仓储类作为中间层,业务逻辑不再直接调用数据库。
典型的.NET仓储模式示例:
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
}
基于Entity Framework Core的实现:
public class Repository<T> : IRepository<T> where T : class
{
protected readonly DbContext _context;
protected readonly DbSet<T> _dbSet;
public Repository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public async Task<T?> GetByIdAsync(int id) => await _dbSet.FindAsync(id);
public async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.ToListAsync();
public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity);
public async Task UpdateAsync(T entity) => _dbSet.Update(entity);
public async Task DeleteAsync(T entity) => _dbSet.Remove(entity);
}
其核心目标?关注点分离、提升可测试性、解耦业务逻辑与数据库细节。
随着Entity Framework Core的普及,许多开发者认为仓储模式已冗余,原因如下:
Update()
DbContext
处理数据库交互如果你的仓储只是简单包装DbContext
,无异于重复造轮子。
DbContext
,意义何在?反面教材——无用的仓储类:
public class UserRepository
{
private readonly AppDbContext _context;
public UserRepository(AppDbContext context) => _context = context;
public async Task<User?> GetUserByIdAsync(int id)
{
return await _context.Users.FindAsync(id);
}
}
直接注入AppDbContext
至服务层更简洁:
public class UserService
{
private readonly AppDbContext _context;
public UserService(AppDbContext context) => _context = context;
public async Task<User?> GetUserByIdAsync(int id)
{
return await _context.Users.FindAsync(id);
}
}
为何要添加冗余层?
Include()
、AsNoTracking()
和原生SQL等高级功能,可能在强制通过预定义仓储方法时失去灵活性。尽管争议不断,仓储模式在特定场景下仍不可替代。
// 接口
public interface IUserRepository
{
Task<User?> GetByIdAsync(int id);
}
// EF Core实现
public class EfUserRepository : IUserRepository
{
private readonly AppDbContext _context;
public EfUserRepository(AppDbContext context) => _context = context;
public async Task<User?> GetByIdAsync(int id) => await _context.Users.FindAsync(id);
}
// MongoDB实现
public class MongoUserRepository : IUserRepository
{
private readonly IMongoCollection<User> _users;
public MongoUserRepository(IMongoClient client)
{
var database = client.GetDatabase("MyDatabase");
_users = database.GetCollection<User>("Users");
}
public async Task<User?> GetByIdAsync(int id)
{
return await _users.Find(u => u.Id == id).FirstOrDefaultAsync();
}
}
切换数据库变得轻而易举。
DbContext
会因EF Core与内存数据库的兼容性问题导致单元测试困难。通过仓储接口,可轻松模拟数据库层:
var mockRepo = new Mock<IUserRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(1)).ReturnsAsync(new User { Id = 1, Name = "Ganesh Gurav" });
var userService = new UserService(mockRepo.Object);
var user = await userService.GetUserByIdAsync(1);
Assert.Equal("Ganesh Gurav", user?.Name);
若无仓储,则需依赖更复杂、低效的集成测试。
public class UserRepository : IUserRepository
{
private readonly AppDbContext _context;
public UserRepository(AppDbContext context) => _context = context;
public async Task<IEnumerable<User>> GetUsersWithOrdersAsync()
{
return await _context.Users.Include(u => u.Orders).ToListAsync();
}
}
避免LINQ查询散落各处,降低维护成本。
来源:Microsoft文档
使用自定义仓储 vs 直接使用EF DbContext
若项目足够简单,直接使用DbContext
更高效;若需解耦或切换数据源,自定义仓储仍具价值。
以下情况可跳过:
• 仅使用EF Core且无换库计划
• 仓储仅包装DbContext
而无实际价值
• 需完全访问EF Core高级功能
以下情况建议使用:
• 未来可能切换数据库
• 需要更好的单元测试支持
• 需集中管理复杂查询
仓储模式并未消亡,但滥用会导致冗余复杂度。若仅用于包装DbContext
,则得不偿失;若需数据库灵活性、测试支持或查询抽象,它仍是现代.NET应用的利器。