重新整理 .net core 實踐篇—————領域事件[二十九]

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

前文

前面整理了倉儲層,工作單元模式,同時簡單介紹了一下mediator。

那麼就mediator在看下領域事件啟到了什麼作用吧。

正文

這裡先註冊一下MediatR服務:

// 註冊中間者:MediatR 
services.AddMediatRServices();

具體註冊:

/// <summary>
/// 註冊 ???
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddMediatRServices(this IServiceCollection services)
{
	// 註冊事務流程管理類
	services.AddTransient(typeof(IPipelineBehavior<,>), typeof(DomainContextTransactionBehavior<,>));

	// package: MediatR.Extensions.Microsoft.Dependency
	return services.AddMediatR(typeof(Order).Assembly, typeof(Program).Assembly);
}

前文提及在共享層的領域抽象類庫中,有下面幾個類:

用來標誌領域事件的介面:

/// <summary>
/// 領域事件介面
/// 用來標記我們某一個物件是否是領域事件
/// </summary>
public interface IDomainEvent : INotification
{
}

用來標誌領域處理的介面:

/// <summary>
/// 領域事件處理器介面
/// </summary>
public interface IDomainEventHandler<TDomainEvent> : INotificationHandler<TDomainEvent> where TDomainEvent : IDomainEvent
{
	//這裡我們使用了INotificationHandler的Handle方法來作為處理方法的定義,所以無需重新定義
	//Task Handle(TDomainEvent domainEvent, CancellationToken cancellationToken);
}

實際上就是在mediator的INotification和INotificationHandler 封裝一層。

那麼是否有必要封裝呢?其實我們大多數使用別人的庫,如果不是靜態呼叫最好封裝一層,這樣可以讓上層看來依賴於下層的IDomainEvent和IDomainEventHandler,而不是和某個框架耦合在一起。

比如說有一個框架是Mediator的升級版,相容了Mediator的功能,但是暴露出來的介面是INotificationPlusHandler,如果上層去耦合的話,改動的地方就有點多,風險也就越高,這不符合穩定性。

下面是實體抽象類:

/// <summary>
/// 實體抽象類(包含多個主鍵的實體介面)
/// </summary>
public abstract class Entity : IEntity
{
	public abstract object[] GetKeys();

	public override string ToString()
	{
		return $"[Entity:{GetType().Name}] Keys = {string.Join(",", GetKeys())}";
	}

	#region 領域事件定義處理 DomainEvents

	/// <summary>
	/// 領域事件集合
	/// </summary>
	private List<IDomainEvent> _domainEvents;

	/// <summary>
	/// 獲取當前實體物件領域事件集合(只讀)
	/// </summary>
	public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents?.AsReadOnly();

	/// <summary>
	/// 新增領域事件至當前實體物件領域事件結合中
	/// </summary>
	/// <param name="eventItem"></param>
	public void AddDomainEvent(IDomainEvent eventItem)
	{
		_domainEvents = _domainEvents ?? new List<IDomainEvent>();
		_domainEvents.Add(eventItem);
	}

	/// <summary>
	/// 移除指定領域事件
	/// </summary>
	/// <param name="eventItem"></param>
	public void RemoveDomainEvent(IDomainEvent eventItem)
	{
		_domainEvents?.Remove(eventItem);
	}

	/// <summary>
	/// 清空所有領域事件
	/// </summary>
	public void ClearDomainEvents()
	{
		_domainEvents?.Clear();
	}

	#endregion
}

/// <summary>
/// 實體抽象類(包含唯一主鍵Id的實體介面)
/// </summary>
/// <typeparam name="TKey">主鍵ID型別</typeparam>
public abstract class Entity<TKey> : Entity, IEntity<TKey>
{
	int? _requestedHasCode;
	public virtual TKey Id { get; protected set; }
	public override object[] GetKeys()
	{
		return new object[] { Id };
	}

	/// <summary>
	/// 物件是否想等
	/// </summary>
	/// <param name="obj"></param>
	/// <returns></returns>
	public override bool Equals(object obj)
	{
		if (obj == null || !(obj is Entity<TKey>))
		{
			return false;
		}

		if (Object.ReferenceEquals(this, obj))
		{
			return true;
		}

		Entity<TKey> item = (Entity<TKey>)obj;

		if (item.IsTransient() || this.IsTransient())
		{
			return false;
		}
		else
		{
			return item.Id.Equals(this.Id);
		}
	}

	public override int GetHashCode()
	{
		if (!IsTransient())
		{
			if (!_requestedHasCode.HasValue)
			{
				_requestedHasCode = this.Id.GetHashCode() ^ 31; // TODO
			}
			return _requestedHasCode.Value;
		}
		else
		{
			return base.GetHashCode();
		}
	}

	/// <summary>
	/// 物件是否為全新建立的,未持久化的
	/// </summary>
	/// <returns></returns>
	public bool IsTransient()
	{
		return EqualityComparer<TKey>.Default.Equals(Id, default);
	}
	public override string ToString()
	{
		return $"[Entity:{GetType().Name}] Id = {Id}";
	}

	/// <summary>
	/// == 操作符過載
	/// </summary>
	/// <param name="left"></param>
	/// <param name="right"></param>
	/// <returns></returns>
	public static bool operator ==(Entity<TKey> left,Entity<TKey> right)
	{
		if (Object.Equals(left,null))
		{
			return (Object.Equals(right, null)) ? true : false;
		}
		else
		{
			return left.Equals(right);
		}
	}

	/// <summary>
	/// != 操作符過載
	/// </summary>
	/// <param name="left"></param>
	/// <param name="right"></param>
	/// <returns></returns>
	public static bool operator !=(Entity<TKey> left, Entity<TKey> right)
	{
		return !(left == right);
	}
}

那麼我們在領域層中,我們的實體可以這樣定義:

/// <summary>
/// 訂單實體
/// </summary>
public class Order : Entity<long>, IAggregateRoot
{
	// 實體內欄位的 set 方法都是 private 的
	// 實體型別相關的資料操作,都應該是由我們實體來負責,而不是被外部的物件去操作
	// 這樣的好處是讓我們的領域模型符合封閉開放的原則

	public string UserId { get; private set; }
	public string UserName { get; private set; }
	public Address Address { get; private set; }
	public int ItemCount { get; set; }

	protected Order()
	{
	}

	public Order(string userId, string userName, int itemCount, Address address)
	{
		this.UserId = userId;
		this.UserName = userName;
		this.ItemCount = itemCount;
		this.Address = address;

		// 構造新的Order物件的時候,新增一個建立Order領域事件
		this.AddDomainEvent(new OrderCreatedDomainEvent(this));
	}

	/// <summary>
	/// 修改收貨地址
	/// </summary>
	/// <param name="address"></param>
	public void ChangeAddress(Address address)
	{
		this.Address = address;

		// 同樣的,在修改地址操作時,也該定義一個類似的修改地址領域事件
		//this.AddDomainEvent(new OrderAddressChangedDomainEvent(this));
	}
}

在例項化的時候將建立一個OrderCreatedDomainEvent事件,這個後面是用來處理訂單建立完成之後的事件。

那麼現在看下這個OrderCreatedDomainEvent是什麼:

/// <summary>
/// 建立Order領域事件
/// </summary>
public class OrderCreatedDomainEvent : IDomainEvent
{
	/// <summary>
	/// 寫入私有,讀取公開
	/// </summary>
	public Order Order { get; private set; }
	public OrderCreatedDomainEvent(Order order)
	{
		this.Order = order;
	}
}

那麼在我們的應用層可以定義一個訂單建立完成的事件處理類,比如說OrderCreatedDomainEventHandler:

/// <summary>
/// 建立Order領域事件處理
/// </summary>
public class OrderCreatedDomainEventHandler : IDomainEventHandler<OrderCreatedDomainEvent>
{
	ICapPublisher _capPublisher;
	public OrderCreatedDomainEventHandler(ICapPublisher capPublisher)
	{
		_capPublisher = capPublisher;
	}

	/// <summary>
	/// 領域事件處理
	/// </summary>
	/// <param name="notification"></param>
	/// <param name="cancellationToken"></param>
	/// <returns></returns>
	public async Task Handle(OrderCreatedDomainEvent notification, CancellationToken cancellationToken)
	{
		// 當建立新訂單時,向 EventBus 釋出一個事件
		await _capPublisher.PublishAsync("OrderCreated", new OrderCreatedIntegrationEvent(notification.Order.Id));
		//_capPublisher.Publish("OrderCreated", new OrderCreatedIntegrationEvent(notification.Order.Id));
		//return Task.CompletedTask;
	}
}

上面表示,當我們建立一個訂單後完成後的類,如果訂單建立完畢,會向 EventBus 釋出一個事件。

上面是訂單處理完成後的事件,那麼我們建立訂單是不是也應該寫一個請求呢?

為什麼我們寫請求,而不是寫事件呢?一個是因為請求與請求處理是1對1,事件與事件處理是一對多。第二個是跟符合意境,一切的源頭寫出請求更好,還有就是建立訂單的確在生活中像一個請求。

CreateOrderCommand 如下:

/// <summary>
/// 建立訂單 Command
/// </summary>
public class CreateOrderCommand : IRequest<long>
{
	public long ItemCount { get; private set; }

	// public CreateOrderCommand() { }
	public CreateOrderCommand(int itemCount)
	{
		ItemCount = itemCount;
	}
}

具體的訂單建立事件CreateOrderCommandHandler:

/// <summary>
/// 領域事件:訂單建立命令處理程式
/// 注:在建立完我們的領域模型並將其儲存之後才會觸發該處理程式
/// </summary>
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, long>
{
	IOrderRepository _orderRepository;
	ICapPublisher _capPublisher;

	public CreateOrderCommandHandler(IOrderRepository orderRepository, ICapPublisher capPublisher)
	{
		_orderRepository = orderRepository;
		_capPublisher = capPublisher;
	}

	/// <summary>
	/// 處理訂單建立命令
	/// </summary>
	/// <param name="request"></param>
	/// <param name="cancellationToken"></param>
	/// <returns></returns>
	public async Task<long> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
	{
		var address = new Address("wen san lu", "hangzhou", "310000");
		var order = new Order("xiaohong1999", "小紅", (int)request.ItemCount, address);

		_orderRepository.Add(order);
		await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
		return order.Id;
	}
}

那麼這個就會去處理相應的建立訂單。

那麼測試一下:

/// <summary>
/// 建立訂單
/// </summary>
/// <param name="cmd"></param>
/// <returns></returns>
[HttpPost]
public async Task<long> CreateOrder([FromBody] CreateOrderCommand cmd)
{
	// 中間者,傳送訂單建立命令
	return await _mediator.Send(cmd, HttpContext.RequestAborted);
}

第一步呼叫這裡:

第二步呼叫CreateOrderCommandHandler,去呼叫相應的訂單呼叫事件:

第三步,呼叫訂單建立完的中間處理,也就是訂單建立完的事件分發:

程式碼如下:

/// <summary>
/// 中間者,領域事件釋出擴充套件類
/// </summary>
public static class MediatorExtension
{
	/// <summary>
	/// 領域事件釋出,執行事件傳送
	/// </summary>
	/// <param name="mediator"></param>
	/// <param name="ctx"></param>
	/// <returns></returns>
	public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContext ctx)
	{
		// 1. 從當前要儲存的 EntityContext 裡面去跟蹤實體
		//    從跟蹤到的實體物件中,獲取到我們當前的 Event
		var domainEntities = ctx.ChangeTracker
								.Entries<Entity>()
								.Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any());

		// Events 型別轉換
		var domainEvents = domainEntities.SelectMany(x => x.Entity.DomainEvents).ToList();

		// 2. 將實體內的 Events 清除 
		domainEntities.ToList().ForEach(entity => entity.Entity.ClearDomainEvents());

		// 3. 將所有的 Event 通過中間者傳送出去
		//    發出後,並找到對應的 handle 進行處理
		foreach (var domainEvent in domainEvents)
		{
			await mediator.Publish(domainEvent);
		}
	}
}

第四步 呼叫建立訂單之後的回撥事件,也就是第三步中的分發:

這裡可能疑問DispatchDomainEventsAsync是怎麼觸發的?

這裡是在EFContext中的save中:

/// <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;
}

然後呼叫這個就會去找到相應事件,然後呼叫publish,找到事件處理類進行handle方法呼叫:

/// <summary>
/// 領域事件釋出,執行事件傳送
/// </summary>
/// <param name="mediator"></param>
/// <param name="ctx"></param>
/// <returns></returns>
public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContext ctx)
{
	// 1. 從當前要儲存的 EntityContext 裡面去跟蹤實體
	//    從跟蹤到的實體物件中,獲取到我們當前的 Event
	var domainEntities = ctx.ChangeTracker.Entries<Entity>().Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any());

	// Events 型別轉換
	var domainEvents = domainEntities.SelectMany(x => x.Entity.DomainEvents).ToList();

	// 2. 將實體內的 Events 清除 
	domainEntities.ToList().ForEach(entity => entity.Entity.ClearDomainEvents());

	// 3. 將所有的 Event 通過中間者傳送出去
	//    發出後,並找到對應的 handle 進行處理
	foreach (var domainEvent in domainEvents)
	{
		await mediator.Publish(domainEvent);
	}
}

下一節Mediator的介紹。

相關文章