重新整理 .net core 實踐篇—————工作單元模式[二十六]

不問前世發表於2021-06-23

前言

簡單整理一下工作單元模式。

正文

工作單元模式有3個特性,也算是其功能:

  1. 使用同一上下文

  2. 跟蹤實體的狀態

  3. 保障事務一致性

工作單元模式 主要關注事務,所以重點在事務上。

在共享層的基礎建設類庫中加入:

/// <summary>
/// 工作單元介面
/// </summary>
public interface IUnitOfWork : IDisposable
{
	/// <summary>
	/// 儲存變更
	/// </summary>
	/// <param name="cancellationToken"></param>
	/// <returns>返回受影響的資料條數</returns>
	Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);

	/// <summary>
	/// 儲存變更
	/// </summary>
	/// <param name="cancellationToken"></param>
	/// <returns>返回儲存是否成功</returns>
	Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default);
}

SaveChangesAsync 事務第一個影響多少條數

SaveEntitiesAsync 事務是否成功

同樣加入事務介面:

interface ITransaction
{
	IDbContextTransaction GetCurrentTransaction();

	bool HasActiveTransaction { get; }

	Task<IDbContextTransaction> BeginTransactionAsync();

	Task CommitTransactionAsync(IDbContextTransaction transaction);

	void RollbackTransaction();
}

然後EFContext 實現它們:

/// <summary>
/// EF上下文
/// 注:在處理事務的邏輯部分,需要嵌入CAP的程式碼,建構函式引數 ICapPublisher
/// </summary>
public class EFContext : DbContext, IUnitOfWork, ITransaction
{
	protected IMediator _mediator;

	ICapPublisher _capBus;

	public EFContext(DbContextOptions options, IMediator mediator, ICapPublisher capBus)
		: base(options)
	{
		_mediator = mediator;
		_capBus = capBus;
	}

	#region IUnitOfWork
	/// <summary>
	/// 儲存實體變更
	/// </summary>
	/// <param name="cancellationToken"></param>
	/// <returns></returns>
	public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
	{
		var result = await base.SaveChangesAsync(cancellationToken);

		// 執行傳送領域事件 
		await _mediator.DispatchDomainEventsAsync(this);

		return true;
	}

	///// <summary>
	///// IUniOfWork中該方法的定義與DbContext中的SaveChangesAsync一致,所以此處無需再進行實現
	///// </summary>
	///// <param name="cancellationToken"></param>
	///// <returns></returns>
	//public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
	//{
	//    return base.SaveChangesAsync();
	//}
	#endregion

	#region ITransaction

	/// <summary>
	/// 當前事務
	/// </summary>
	private IDbContextTransaction _currentTransaction;

	/// <summary>
	/// 公開方法,返回當前私有事務物件
	/// </summary>
	/// <returns></returns>
	public IDbContextTransaction GetCurrentTransaction() => _currentTransaction;

	/// <summary>
	/// 當前事務是否開啟
	/// </summary>
	public bool HasActiveTransaction => _currentTransaction == null;

	/// <summary>
	/// 開啟事務
	/// </summary>
	/// <returns></returns>
	public Task<IDbContextTransaction> BeginTransactionAsync()
	{
		if (_currentTransaction != null)
		{
			return null;
		}
		// 該擴充套件方法是由CAP元件提供
		// 建立事務時,也要把 ICapPublisher 傳入
		// 核心作用是將我們要傳送事件邏輯與我們業務的儲存都放在同一個事務內部,從而保證事件與業務邏輯的存取都是一致的
		_currentTransaction = Database.BeginTransaction(_capBus, autoCommit: false);

		return Task.FromResult(_currentTransaction);
	}

	/// <summary>
	/// 提交事務
	/// </summary>
	/// <param name="transaction"></param>
	/// <returns></returns>
	public async Task CommitTransactionAsync(IDbContextTransaction transaction)
	{
		if (transaction == null)
		{
			throw new ArgumentNullException(nameof(transaction));
		}
		if (transaction != _currentTransaction)
		{
			throw new InvalidOperationException($"Transaction {transaction.TransactionId} is not current");
		}

		try
		{
			// 提交事務之前,安全起見還是要 SaveChanges 一下,儲存變更到資料庫
			await SaveChangesAsync();
			transaction.Commit();
		}
		catch (Exception ex)
		{
			RollbackTransaction();
			throw;
		}
		finally
		{
			if (_currentTransaction!=null)
			{
				_currentTransaction.Dispose();
				_currentTransaction = null;
			}
		}
	}

	/// <summary>
	/// 回滾事務
	/// </summary>
	public  void RollbackTransaction()
	{
		try
		{
			_currentTransaction?.Rollback();
		}
		finally
		{
			if (_currentTransaction!=null)
			{
				_currentTransaction.Dispose();
				_currentTransaction = null;
			}
		}
	}
	#endregion

}

前面這兩個實現了工作單元模式的事務的功能,那麼還有一個問題,如何實現管理我們的事務。

/// <summary>
/// 注入事務管理過程
/// </summary>
/// <typeparam name="TDbContext"></typeparam>
/// <typeparam name="TRequest"></typeparam>
/// <typeparam name="TResponse"></typeparam>
public class TransactionBehavior<TDbContext, TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TDbContext : EFContext
{
	ILogger _logger;
	TDbContext _dbContext;
	ICapPublisher _capBus;

	public TransactionBehavior(TDbContext dbContext, ICapPublisher capBus, ILogger logger)
	{
		_dbContext = dbContext ?? throw new ArgumentNullException();
		_capBus = capBus ?? throw new ArgumentNullException(nameof(capBus));
		_logger = logger ?? throw new ArgumentNullException(nameof(logger));
	}

	/// <summary>
	/// 事務執行
	/// </summary>
	/// <param name="request"></param>
	/// <param name="cancellationToken"></param>
	/// <param name="next"></param>
	/// <returns></returns>
	public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
	{
		var response = default(TResponse);
		var typeName = request.GetGenericTypeName();

		try
		{
			// 判斷當前是否有開啟事務,如果開啟就執行後續動作
			if (_dbContext.HasActiveTransaction)
			{
				return await next();
			}

			// 資料庫操作預設執行策略
			// 比如,可以嵌入重試邏輯
			var strategy = _dbContext.Database.CreateExecutionStrategy();

			await strategy.ExecuteAsync(async () =>
			{
				// 開啟事務
				Guid transactionId;
				using (var transaction = await _dbContext.BeginTransactionAsync())
				// 記錄開啟的事務
				using (_logger.BeginScope("TransactionContext:{TransactionId}", transaction.TransactionId))
				{
					_logger.LogInformation("----- 開始事務 {TransactionId} ({@Command})", transaction.TransactionId, typeName, request);

					// 類似中介軟體模式,後續邏輯執行完成後,提交事務
					response = await next();

					_logger.LogInformation("----- 提交事務 {TransactionId} ({CommandName})", transaction.TransactionId, typeName);

					// 提交事務
					await _dbContext.CommitTransactionAsync(transaction);

					transactionId = transaction.TransactionId;

				}
			});

			return response;
		}
		catch (Exception ex)
		{
			_logger.LogError(ex, "處理事務出錯 {CommandName} ({@Command})", typeName, request);
			throw;
		}

	}
}

這裡可能會有點疑問,這裡沒有rollback啊。

using (var transaction = await _dbContext.BeginTransactionAsync())

這一句是託管了,如果中間發生異常,那麼會自動呼叫rollback,using原理前面在c# 基礎篇中介紹了,本質就是try catch finnaly這樣的模式,這裡不詳細介紹了。

下一節倉儲層的具體實現。

相關文章