using System.Diagnostics; using Microsoft.EntityFrameworkCore; namespace MS.Microservice.Messaging.SelfManaged; /// One scoped context owns business data and messages. The instance must not be used concurrently. /// 复用业务 DbContext 是为了让业务与 Outbox 同事务;独立的消息 DbContext 无法保证这个不变量。 public sealed class SelfManagedUnitOfWork(TContext context, MessageContractRegistry contracts, TimeProvider timeProvider) : IUnitOfWork, IIntegrationEventPublisher where TContext : DbContext { private bool _active; private bool _rollbackOnly; private readonly Dictionary _pending = []; public ValueTask EnqueueAsync(IIntegrationEvent message, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); if (!_active) throw new InvalidOperationException("Enqueue requires an active unit of work."); var activity = Activity.Current; // 入队时冻结快照,避免调用者后续修改对象而改变本次事务将提交的事实。 var serialized = contracts.Serialize(message, new(message.Id, "", activity?.GetBaggageItem("correlationId"), activity?.Id, activity?.TraceStateString)); if (_pending.TryGetValue(message.Id, out var existing) && existing != serialized) throw new MessageContractException("A message Id cannot identify different payloads in one transaction."); _pending[message.Id] = serialized; return ValueTask.CompletedTask; } public async Task ExecuteAsync(Func> operation, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(operation); cancellationToken.ThrowIfCancellationRequested(); if (_active) { try { return await operation(cancellationToken); } // 内层失败即失去整体提交资格;即使外层捕获异常,也不能提交半个业务操作。 catch { _rollbackOnly = true; throw; } } if (context.Database.CurrentTransaction is not null) throw new InvalidOperationException("The messaging unit of work must own the outer transaction."); if (context.Model.FindEntityType(typeof(OutboxEntry)) is null) throw new InvalidOperationException("Call AddSelfManagedMessaging in the business context model."); await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken); _active = true; _rollbackOnly = false; try { var result = await operation(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); if (_rollbackOnly) throw new InvalidOperationException("A nested operation failed; this transaction cannot commit."); var now = timeProvider.GetUtcNow().UtcDateTime; foreach (var message in _pending.Values) context.Set().Add(OutboxEntry.From(message, now)); await context.SaveChangesAsync(cancellationToken); await transaction.CommitAsync(cancellationToken); return result; } catch { // Disposing the transaction rolls it back even when caller cancellation is already requested. context.ChangeTracker.Clear(); throw; } finally { _pending.Clear(); _active = false; } } }