仓储与工作单元模式
仓储与工作单元模式
简介
仓储(Repository)封装数据访问逻辑,提供类似集合的接口。工作单元(Unit of Work)维护受业务事务影响的对象列表,协调变更的写入。理解这两种模式,有助于实现领域层与数据层的解耦。
仓储的概念最早由 Martin Fowler 和 Eric Evans 在 DDD(领域驱动设计)中提出。Eric Evans 在《领域驱动设计》中将仓储定义为:"一种封装了获取持久化对象并将其生命周期托付给持久化层的机制"。工作单元则来自 Martin Fowler 的《企业应用架构模式》,它记录了在业务事务中对对象所做的所有变更,并在事务结束时一次性提交。
在 .NET 生态中,EF Core 的 DbContext 本身就是工作单元和仓储的结合体,但在复杂领域模型中,仍然需要自定义仓储来提供更丰富的查询能力和更好的测试性。
特点
结构分析
UML 类图
+--------------------+
| IRepository<T> | <-- 仓储接口
+--------------------+
| +GetByIdAsync() |
| +GetAllAsync() |
| +FindAsync() |
| +AddAsync() |
| +Update() |
| +Remove() |
| +CountAsync() |
+--------------------+
^
|
+--------------------+
| EfRepository<T> | <-- EF Core 实现
+--------------------+
| -_context: DbContext|
| -Set: DbSet<T> |
+--------------------+
+--------------------+
| IUnitOfWork | <-- 工作单元接口
+--------------------+
| +Users: IUserRepo |
| +Orders: IOrderRepo|
| +SaveChangesAsync()|
| +BeginTransaction()|
| +CommitAsync() |
| +RollbackAsync() |
| +Dispose() |
+--------------------+
^
|
+--------------------+
| UnitOfWork | <-- 具体实现
+--------------------+
| -_context: AppDbCtx|
| -_transaction |
+--------------------+组件协作流程
+----------+ +----------+ +--------------+ +--------+
| Service |---->| IUnitOf |---->| EfRepository |---->| DbSet |
| Layer | | Work | | (仓储) | | (EF) |
+----------+ +----------+ +--------------+ +--------+
| | | |
| SaveChanges | 跟踪变更 | 查询/写入 |
|---------------->| | |
| |----------------->| |
| | 提交事务 |---------------->|
| | | |--> DB实现
泛型仓储
// 仓储接口
public interface IRepository<T> where T : class, IEntity
{
Task<T?> GetByIdAsync(Guid id, CancellationToken ct);
Task<IReadOnlyList<T>> GetAllAsync(CancellationToken ct);
Task<IReadOnlyList<T>> FindAsync(Expression<Func<T, bool>> predicate, CancellationToken ct);
Task AddAsync(T entity, CancellationToken ct);
void Update(T entity);
void Remove(T entity);
Task<int> CountAsync(Expression<Func<T, bool>> predicate, CancellationToken ct);
}
public interface IEntity { Guid Id { get; } }
// EF Core 实现
public class EfRepository<T> : IRepository<T> where T : class, IEntity
{
protected readonly DbContext _context;
protected DbSet<T> Set => _context.Set<T>();
public EfRepository(DbContext context) => _context = context;
public async Task<T?> GetByIdAsync(Guid id, CancellationToken ct) => await Set.FindAsync(new object[] { id }, ct);
public async Task<IReadOnlyList<T>> GetAllAsync(CancellationToken ct) => await Set.AsNoTracking().ToListAsync(ct);
public async Task<IReadOnlyList<T>> FindAsync(Expression<Func<T, bool>> predicate, CancellationToken ct)
=> await Set.Where(predicate).ToListAsync(ct);
public async Task AddAsync(T entity, CancellationToken ct) => await Set.AddAsync(entity, ct);
public void Update(T entity) => Set.Update(entity);
public void Remove(T entity) => Set.Remove(entity);
public async Task<int> CountAsync(Expression<Func<T, bool>> predicate, CancellationToken ct)
=> await Set.CountAsync(predicate, ct);
}
// 规约扩展
public interface ISpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
Expression<Func<T, object>>? OrderBy { get; }
int? Take { get; }
int? Skip { get; }
}
public class SpecificationEvaluator<T> where T : class, IEntity
{
public static IQueryable<T> Apply(IQueryable<T> query, ISpecification<T> spec)
{
query = query.Where(spec.Criteria);
foreach (var include in spec.Includes) query = query.Include(include);
if (spec.OrderBy != null) query = query.OrderBy(spec.OrderBy);
if (spec.Skip.HasValue) query = query.Skip(spec.Skip.Value);
if (spec.Take.HasValue) query = query.Take(spec.Take.Value);
return query;
}
}工作单元
public interface IUnitOfWork : IDisposable
{
IUserRepository Users { get; }
IOrderRepository Orders { get; }
Task<int> SaveChangesAsync(CancellationToken ct);
Task BeginTransactionAsync(CancellationToken ct);
Task CommitAsync(CancellationToken ct);
Task RollbackAsync(CancellationToken ct);
}
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
private IDbContextTransaction? _transaction;
public IUserRepository Users { get; }
public IOrderRepository Orders { get; }
public UnitOfWork(AppDbContext context)
{
_context = context;
Users = new UserRepository(context);
Orders = new OrderRepository(context);
}
public async Task BeginTransactionAsync(CancellationToken ct)
=> _transaction = await _context.Database.BeginTransactionAsync(ct);
public async Task<int> SaveChangesAsync(CancellationToken ct) => await _context.SaveChangesAsync(ct);
public async Task CommitAsync(CancellationToken ct)
{
await _context.SaveChangesAsync(ct);
if (_transaction != null) await _transaction.CommitAsync(ct);
}
public async Task RollbackAsync(CancellationToken ct)
{
if (_transaction != null) await _transaction.RollbackAsync(ct);
}
public void Dispose() { _transaction?.Dispose(); _context.Dispose(); }
}
// 使用 — 转账场景
public class TransferService
{
private readonly IUnitOfWork _uow;
public async Task TransferAsync(Guid fromId, Guid toId, decimal amount)
{
await _uow.BeginTransactionAsync(CancellationToken.None);
try
{
var from = await _uow.Users.GetByIdAsync(fromId, CancellationToken.None);
var to = await _uow.Users.GetByIdAsync(toId, CancellationToken.None);
from!.Balance -= amount;
to!.Balance += amount;
await _uow.CommitAsync(CancellationToken.None);
}
catch { await _uow.RollbackAsync(CancellationToken.None); throw; }
}
}
// DI 注册
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddDbContext<AppDbContext>(o => o.UseSqlServer(connStr));内存仓储(测试用)
public class InMemoryRepository<T> : IRepository<T> where T : class, IEntity
{
private readonly ConcurrentDictionary<Guid, T> _store = new();
public Task<T?> GetByIdAsync(Guid id, CancellationToken ct) => Task.FromResult(_store.GetValueOrDefault(id));
public Task<IReadOnlyList<T>> GetAllAsync(CancellationToken ct) => Task.FromResult<IReadOnlyList<T>>(_store.Values.ToList());
public Task<IReadOnlyList<T>> FindAsync(Expression<Func<T, bool>> predicate, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<T>>(_store.Values.AsQueryable().Where(predicate).ToList());
public Task AddAsync(T entity, CancellationToken ct) { _store[entity.Id] = entity; return Task.CompletedTask; }
public void Update(T entity) => _store[entity.Id] = entity;
public void Remove(T entity) => _store.TryRemove(entity.Id, out _);
public Task<int> CountAsync(Expression<Func<T, bool>> predicate, CancellationToken ct)
=> Task.FromResult(_store.Values.AsQueryable().Count(predicate));
}实战:按聚合根定义专用仓储
DDD 中推荐按聚合根定义专用仓储,而非对所有实体都使用泛型仓储。以下是一个订单聚合根的专用仓储实现。
// 订单聚合根
public class Order : IEntity
{
public Guid Id { get; private set; } = Guid.NewGuid();
public Guid CustomerId { get; private set; }
public decimal TotalAmount { get; private set; }
public OrderStatus Status { get; private set; }
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
private readonly List<OrderLine> _lines = new();
public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
public void AddLine(Product product, int quantity)
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("只能为草稿订单添加商品");
var line = new OrderLine(product.Id, product.Name, product.Price, quantity);
_lines.Add(line);
TotalAmount = _lines.Sum(l => l.Amount);
}
public void Confirm()
{
if (_lines.Count == 0) throw new InvalidOperationException("空订单不能确认");
Status = OrderStatus.Confirmed;
}
}
public class OrderLine
{
public Guid ProductId { get; }
public string ProductName { get; }
public decimal UnitPrice { get; }
public int Quantity { get; }
public decimal Amount => UnitPrice * Quantity;
public OrderLine(Guid productId, string productName, decimal unitPrice, int quantity)
{
ProductId = productId; ProductName = productName;
UnitPrice = unitPrice; Quantity = quantity;
}
}
public enum OrderStatus { Draft, Confirmed, Shipped, Completed, Cancelled }
// 专用仓储接口
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id, CancellationToken ct);
Task<IReadOnlyList<Order>> GetByCustomerAsync(Guid customerId, CancellationToken ct);
Task<IReadOnlyList<Order>> GetPendingOrdersAsync(int page, int size, CancellationToken ct);
void Add(Order order);
void Remove(Order order);
}
// EF Core 专用仓储实现
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
private readonly DbSet<Order> _orders;
public OrderRepository(AppDbContext context)
{
_context = context;
_orders = context.Set<Order>();
}
public async Task<Order?> GetByIdAsync(Guid id, CancellationToken ct)
=> await _orders
.Include(o => o.Lines) // 聚合根必须完整加载
.FirstOrDefaultAsync(o => o.Id == id, ct);
public async Task<IReadOnlyList<Order>> GetByCustomerAsync(Guid customerId, CancellationToken ct)
=> await _orders
.Include(o => o.Lines)
.Where(o => o.CustomerId == customerId)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync(ct);
public async Task<IReadOnlyList<Order>> GetPendingOrdersAsync(int page, int size, CancellationToken ct)
=> await _orders
.Where(o => o.Status == OrderStatus.Conirmed)
.OrderBy(o => o.CreatedAt)
.Skip((page - 1) * size)
.Take(size)
.ToListAsync(ct);
public void Add(Order order) => _orders.Add(order);
public void Remove(Order order) => _orders.Remove(order);
}实战:带领域事件的仓储
在 DDD 中,聚合根在状态变更时会发布领域事件。仓储在持久化时需要收集并发布这些事件。
// 领域事件接口
public interface IDomainEvent
{
DateTime OccurredAt { get; }
}
public record OrderCreatedEvent(Guid OrderId, Guid CustomerId, decimal Amount) : IDomainEvent
{
public DateTime OccurredAt { get; } = DateTime.UtcNow;
}
// 领域事件调度器
public interface IDomainEventDispatcher
{
Task DispatchAsync(IEnumerable<IDomainEvent> events);
}
// 具有领域事件能力的聚合根基类
public abstract class AggregateRoot : IEntity
{
public Guid Id { get; protected set; } = Guid.NewGuid();
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void AddDomainEvent(IDomainEvent @event) => _domainEvents.Add(@event);
public void ClearDomainEvents() => _domainEvents.Clear();
}
// 带事件发布的仓储装饰器
public class EventPublishingRepository<T> : IRepository<T> where T : class, IEntity
{
private readonly IRepository<T> _inner;
private readonly AppDbContext _context;
private readonly IDomainEventDispatcher _dispatcher;
public EventPublishingRepository(IRepository<T> inner, AppDbContext context, IDomainEventDispatcher dispatcher)
{
_inner = inner;
_context = context;
_dispatcher = dispatcher;
}
public async Task AddAsync(T entity, CancellationToken ct)
{
await _inner.AddAsync(entity, ct);
if (entity is AggregateRoot aggregate && aggregate.DomainEvents.Count > 0)
{
await _dispatcher.DispatchAsync(aggregate.DomainEvents);
aggregate.ClearDomainEvents();
}
}
// 其他方法委托给内部仓储
public Task<T?> GetByIdAsync(Guid id, CancellationToken ct) => _inner.GetByIdAsync(id, ct);
public Task<IReadOnlyList<T>> GetAllAsync(CancellationToken ct) => _inner.GetAllAsync(ct);
public Task<IReadOnlyList<T>> FindAsync(Expression<Func<T, bool>> predicate, CancellationToken ct) => _inner.FindAsync(predicate, ct);
public void Update(T entity) => _inner.Update(entity);
public void Remove(T entity) => _inner.Remove(entity);
public Task<int> CountAsync(Expression<Func<T, bool>> predicate, CancellationToken ct) => _inner.CountAsync(predicate, ct);
}实战:CQRS 读写分离
在现代应用中,经常将读写操作分离。写仓储处理命令,读仓储处理查询,两者使用不同的数据模型。
// 读模型
public record OrderSummaryDto(Guid OrderId, string CustomerName, decimal TotalAmount, string Status, DateTime CreatedAt);
// 写仓储(命令端)
public interface IOrderWriteRepository
{
Task AddAsync(Order order, CancellationToken ct);
Task UpdateAsync(Order order, CancellationToken ct);
}
// 读仓储(查询端)
public interface IOrderReadRepository
{
Task<OrderSummaryDto?> GetByIdAsync(Guid id, CancellationToken ct);
Task<IReadOnlyList<OrderSummaryDto>> GetByCustomerAsync(Guid customerId, CancellationToken ct);
Task<IReadOnlyList<OrderSummaryDto>> SearchAsync(string? keyword, int page, int size, CancellationToken ct);
}
// 读仓储实现 — 可直接查询 SQL 视图或读库
public class OrderReadRepository : IOrderReadRepository
{
private readonly DbContext _readContext;
public OrderReadRepository(DbContext readContext) => _readContext = readContext;
public async Task<OrderSummaryDto?> GetByIdAsync(Guid id, CancellationToken ct)
{
return await _readContext.Set<OrderSummaryDto>()
.FirstOrDefaultAsync(o => o.OrderId == id, ct);
}
public async Task<IReadOnlyList<OrderSummaryDto>> SearchAsync(string? keyword, int page, int size, CancellationToken ct)
{
var query = _readContext.Set<OrderSummaryDto>().AsQueryable();
if (!string.IsNullOrWhiteSpace(keyword))
query = query.Where(o => o.CustomerName.Contains(keyword));
return await query
.OrderByDescending(o => o.CreatedAt)
.Skip((page - 1) * size)
.Take(size)
.ToListAsync(ct);
}
}仓储 vs 直接使用 EF Core
仓储模式 直接使用 EF Core
+------------------+ +------------------+
| 领域层不依赖 EF | | 领域层依赖 EF |
| 可替换 ORM | | 绑定 EF Core |
| 查询封装 | | 查询分散 |
| 易于测试 | | 测试需 InMemory |
| 多一层间接 | | 直接高效 |
+------------------+ +------------------+
建议:
- 简单 CRUD 项目 -> 直接用 EF Core
- 复杂领域模型 -> 使用仓储 + 规约
- 需要多数据源 -> 使用仓储抽象工作单元的生命周期管理
// 选项 1: Scoped(每个 HTTP 请求一个工作单元)— 最常用
services.AddScoped<IUnitOfWork, UnitOfWork>();
// 选项 2: 手动管理(长事务场景)
public class OrderWorkflow
{
private readonly IUnitOfWorkFactory _factory;
public async Task ExecuteAsync()
{
await using var uow = await _factory.CreateAsync();
try
{
// 业务逻辑
await uow.CommitAsync();
}
catch
{
await uow.RollbackAsync();
throw;
}
}
}
// 工作单元工厂
public interface IUnitOfWorkFactory
{
Task<IUnitOfWork> CreateAsync();
}最佳实践
- 按聚合根定义仓储:不要为每个实体创建仓储,只为聚合根创建。子实体通过聚合根访问。
- 仓储接口放在领域层:仓储接口属于领域层,实现放在基础设施层,符合依赖倒置原则。
- 避免暴露 IQueryable:仓储返回具体类型(
IReadOnlyList<T>),不返回IQueryable<T>,防止查询逻辑泄漏到领域层。 - 工作单元与请求同生命周期:通常使用 Scoped 依赖注入,每个 HTTP 请求一个工作单元。
- 读写分离:在需要高性能读取时,将读操作分离到独立的查询服务或读库。
优点
缺点
总结
仓储模式提供集合语义的数据访问接口,工作单元模式协调事务提交。EF Core 的 DbContext 本身就是工作单元,DbSet 是仓储。在简单场景中可直接使用 EF Core,复杂领域逻辑建议封装仓储接口以解耦和测试。规约模式封装复杂查询条件。建议按聚合根定义专用仓储接口,而非滥用泛型仓储。
仓储与工作单元的本质价值在于:在领域层和数据层之间建立一道清晰的边界,让领域逻辑只关心"业务规则",不关心"数据如何存储"。当你的项目从简单 CRUD 演进为复杂领域模型时,这道边界会让你受益匪浅。
关键知识点
- 模式不是目标,降低耦合和控制变化才是目标。
- 先找变化点、稳定点和协作边界,再决定是否引入模式。
- 同一个模式在不同规模下的收益和代价差异很大。
项目落地视角
- 优先画出参与对象、依赖方向和调用链,再落到代码。
- 把模式放到一个真实场景里,比如支付、规则引擎、工作流或插件扩展。
- 配合单元测试或契约测试,保证重构后的行为没有漂移。
常见误区
- 为了看起来"高级"而套模式。
- 把简单问题拆成过多抽象层,导致阅读和排障都变难。
- 只会背 UML,不会解释为什么这里需要这个模式。
进阶路线
- 继续关注模式之间的组合用法,而不是孤立记忆。
- 从业务建模、演进策略和团队协作角度看模式的适用性。
- 把模式结论沉淀为项目模板、基类或约束文档。
适用场景
- 当你准备把《仓储与工作单元模式》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在业务规则频繁变化、分支增多或对象协作复杂时引入。
- 当你希望提高扩展性,但又不想把系统拆得过度抽象时,这类主题很有参考价值。
落地建议
- 先识别变化点,再决定是否引入模式,而不是反过来套模板。
- 优先为模式的边界、依赖和调用路径画出简单结构图。
- 把模式落到一个明确场景,例如支付、规则计算、插件扩展或工作流。
排错清单
- 检查抽象层是否过多,导致调用路径和责任不清晰。
- 确认引入模式后是否真的减少了条件分支和重复代码。
- 警惕"为了模式而模式",尤其是在简单业务里。
复盘问题
- 如果把《仓储与工作单元模式》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《仓储与工作单元模式》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《仓储与工作单元模式》最大的收益和代价分别是什么?
