基于DDD实现.NET 8 WebAPI座位预订超时自动持久化
DDD下座位超时自动释放的正确实现方案
在DDD和垂直切片架构的约束下,核心原则是领域模型只聚焦业务规则,持久化与外部触发逻辑完全剥离。针对你提出的座位超时自动从Pending转为Vacant的需求,以下是几种符合DDD理念的实现方案,按贴合领域驱动的优先级排序:
方案一:领域事件 + 延迟任务调度
这是最符合DDD思想的方案——通过领域事件传递业务意图,由外部组件处理超时逻辑,完全不污染领域模型。
步骤1:在领域模型中发布事件
当座位进入Pending状态时,领域对象发布一个领域事件,表达“该座位需要在指定时间后检查并释放”的业务意图。修改Seat的Reserve方法:
public Result Reserve(ushort pendingTimeoutInSeconds) { if (Occupancy != OccupancyState.Vacant) return SeatError.InvalidAction; Occupancy = OccupancyState.Pending; OccupancyStateExpiration = DateTime.UtcNow.AddSeconds(pendingTimeoutInSeconds); // 发布领域事件(用内存事件集合,避免领域模型依赖外部服务) DomainEvents.Add(new SeatPendingExpiredEvent(Id, OccupancyStateExpiration.Value)); return Result.Success(); } // 领域事件定义(放在领域层) public record SeatPendingExpiredEvent(Guid SeatId, DateTime ExpirationTime); // 内存事件发布器(领域层工具类) public static class DomainEvents { public static List<object> Events { get; } = new(); public static void Add(object eventObj) => Events.Add(eventObj); public static void Clear() => Events.Clear(); }
步骤2:应用层订阅并调度延迟任务
在保存领域对象到数据库后,应用层取出领域事件,通过调度组件(如Hangfire、Quartz,或.NET 8自带的BackgroundService结合延迟队列)创建延迟任务,到超时时间触发处理逻辑:
// 应用层服务:处理座位预订命令 public class ReserveSeatCommandHandler { private readonly ISeatRepository _seatRepository; private readonly IUnitOfWork _unitOfWork; private readonly IBackgroundJobClient _backgroundJobClient; public ReserveSeatCommandHandler(ISeatRepository seatRepository, IUnitOfWork unitOfWork, IBackgroundJobClient backgroundJobClient) { _seatRepository = seatRepository; _unitOfWork = unitOfWork; _backgroundJobClient = backgroundJobClient; } public async Task<Result> Handle(Guid seatId, ushort pendingTimeoutInSeconds) { var seat = await _seatRepository.GetByIdAsync(seatId); if (seat == null) return SeatError.NotFound; var result = seat.Reserve(pendingTimeoutInSeconds); if (!result.IsSuccess) return result; await _unitOfWork.SaveChangesAsync(); // 处理领域事件,调度延迟任务 foreach (var domainEvent in DomainEvents.Events) { if (domainEvent is SeatPendingExpiredEvent expiredEvent) { _backgroundJobClient.Schedule<SeatExpirationProcessor>( p => p.ProcessExpiredSeat(expiredEvent.SeatId), expiredEvent.ExpirationTime - DateTime.UtcNow); } } DomainEvents.Clear(); return result; } } // 超时处理处理器 public class SeatExpirationProcessor { private readonly ISeatRepository _seatRepository; private readonly IUnitOfWork _unitOfWork; public SeatExpirationProcessor(ISeatRepository seatRepository, IUnitOfWork unitOfWork) { _seatRepository = seatRepository; _unitOfWork = unitOfWork; } public async Task ProcessExpiredSeat(Guid seatId) { var seat = await _seatRepository.GetByIdAsync(seatId); if (seat == null) return; // 双重检查:避免并发操作导致的状态不一致 if (seat.Occupancy == Seat.OccupancyState.Pending && seat.OccupancyStateExpiration <= DateTime.UtcNow) { seat.Release(); await _unitOfWork.SaveChangesAsync(); } } }
方案二:定时批量扫描后台服务
如果不想引入延迟任务调度组件,可采用定期扫描数据库的方案,适合小型系统或对延迟容忍度较高的场景。
实现代码
创建一个后台服务,定期查询数据库中已过期的Pending座位,调用领域模型的Release()方法更新状态:
public class ExpiredSeatCleanupService : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; // 扫描间隔可配置,比如1分钟 private readonly TimeSpan _scanInterval = TimeSpan.FromMinutes(1); public ExpiredSeatCleanupService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { using var scope = _scopeFactory.CreateScope(); var seatRepository = scope.ServiceProvider.GetRequiredService<ISeatRepository>(); var unitOfWork = scope.ServiceProvider.GetRequiredService<IUnitOfWork>(); // 查询所有已过期的Pending座位 var expiredSeats = await seatRepository.GetExpiredPendingSeatsAsync(DateTime.UtcNow); foreach (var seat in expiredSeats) { seat.Release(); } await unitOfWork.SaveChangesAsync(); await Task.Delay(_scanInterval, stoppingToken); } } } // 仓储层扩展方法 public interface ISeatRepository { Task<List<Seat>> GetExpiredPendingSeatsAsync(DateTime currentTime); // 其他仓储方法... } // EF Core仓储实现 public class EfSeatRepository : ISeatRepository { private readonly CinemaDbContext _dbContext; public EfSeatRepository(CinemaDbContext dbContext) { _dbContext = dbContext; } public async Task<List<Seat>> GetExpiredPendingSeatsAsync(DateTime currentTime) { return await _dbContext.Seats .Where(s => s.Occupancy == Seat.OccupancyState.Pending && s.OccupancyStateExpiration <= currentTime) .ToListAsync(); } }
方案三:业务操作时的即时补偿
作为前两种方案的补充,在任何涉及座位的业务操作(如预订、确认、查询)之前,先检查并处理过期状态,避免用户看到不一致的状态:
public class SeatService { private readonly ISeatRepository _seatRepository; private readonly IUnitOfWork _unitOfWork; public SeatService(ISeatRepository seatRepository, IUnitOfWork unitOfWork) { _seatRepository = seatRepository; _unitOfWork = unitOfWork; } public async Task<Result> ReserveSeat(Guid seatId, ushort pendingTimeoutInSeconds) { var seat = await _seatRepository.GetByIdAsync(seatId); if (seat == null) return SeatError.NotFound; // 先处理过期状态 await ProcessExpiredSeatIfNeeded(seat); return seat.Reserve(pendingTimeoutInSeconds); } public async Task<Seat> GetSeatById(Guid seatId) { var seat = await _seatRepository.GetByIdAsync(seatId); if (seat == null) return null; await ProcessExpiredSeatIfNeeded(seat); return seat; } private async Task ProcessExpiredSeatIfNeeded(Seat seat) { if (seat.Occupancy == Seat.OccupancyState.Pending && seat.OccupancyStateExpiration <= DateTime.UtcNow) { seat.Release(); await _unitOfWork.SaveChangesAsync(); } } }
核心原则总结
- 领域模型只负责业务规则:所有状态修改必须通过
Seat的Release()方法,而非直接更新数据库字段,确保业务逻辑的一致性。 - 外部逻辑与领域分离:超时触发、持久化操作均放在应用层或基础设施层,领域模型不依赖任何外部框架或服务。
- 状态一致性保障:无论采用哪种方案,都必须做双重检查(确认座位当前状态为
Pending且已过期),避免并发操作导致的错误。
内容的提问来源于stack exchange,提问作者user23421787
相关产品推荐
相关产品推荐

