代码也是非常简单、容易理解的:GetByIdAsync方法根据给定的聚合根类型以及ID值,从后台存储中读取所有属于该聚合的领域事件,并在聚合上进行回放,以便将聚合恢复到存储前的状态;SaveAsync方法则从聚合根上获得所有未被提交的领域事件,将这些事件保存到后台存储,然后设置聚合的“已保存版本”,最后清空未提交事件的缓存。剩下的就是如何实现LoadDomainEventsAsync以及PersistDomainEventsAsync两个方法了。而这两个方法,原本就应该是事件存储对象的职责范围了。
Note:你也许会问:如果某个聚合从开始到现在,已经发生了大量的领域事件了,那么这样一条条地将事件回放到聚合上,岂不是性能非常低下?没错,这个问题我们可以通过快照来解决。在后续文章中我会介绍。你还会问:日积月累,事件存储系统中的事件数量岂不是会越来越多吗?需要删除吗?答案是:不删!不过可以对数据进行归档,或者依赖一些第三方框架来处理这个问题,但是,从领域驱动设计的角度,领域事件代表着整个领域模型系统中发生过的所有事情,事情既然已经发生,就无法再被抹去,因此,删除事件存储系统中的事件是不合理的。那数据量越来越大怎么办?答案是:或许,存储硬件设备要比业务数据更便宜。
仓储的实现我们暂且探索到这一步,目前我们只需要有一个正确的聚合保存、读取(通过领域事件重塑)的逻辑就可以了,并不需要关心事件本身是如何被读取被保存的。接下来,我们在.NET Core的测试项目中,借助Moq框架,通过Mock一个假想的仓储,来验证整个系统从聚合、聚合根的实现到仓储设计的正确性。
使用Moq框架,通过单元测试验证聚合、聚合根以及仓储设计的正确性Moq是一个很好的Mock框架,简单轻量,而且支持.NET Core,在单元测试的项目中使用Moq是一种很好的实践。Moq上手非常简单,只需要在单元测试项目上添加Moq的NuGet依赖包就可以开始着手编写测试用例了。为了测试我们的聚合根以及仓储对聚合根保存、读取的设计,首先我们定义一个简单的聚合:
public class Book : AggregateRootWithEventSourcing { public void ChangeTitle(string newTitle) { this.Raise(new BookTitleChangedEvent(newTitle)); } public string Title { get; private set; } [HandlesInline] private void OnTitleChanged(BookTitleChangedEvent @event) { this.Title = @event.NewTitle; } public override string ToString() { return Title; } }Book类是一个聚合根,它继承AggregateRootWithEventSourcing抽象类,同时它有一个属性,Title,表示书的名称,而ChangeTitle方法(业务方法)会直接产生一个BookTitleChangedEvent领域事件,之后,OnTitleChanged成员函数会负责将领域事件中的NewTitle的值设置到Book聚合根的Title状态上,完成书本标题的更新。与之相关的BookTitleChangedEvent的定义如下:
public class BookTitleChangedEvent : DomainEvent { public BookTitleChangedEvent(string newTitle) { this.NewTitle = newTitle; } public string NewTitle { get; set; } public override string ToString() { return $"{Sequence} - {NewTitle}"; } }首先,下面两个测试用例用于测试Book聚合本身产生领域事件的过程是否正确,如果正确,那么当Book本身本构造时,会产生一个AggregateCreatedEvent,如果更改书本的标题,则又会产生一个BookTitleChangedEvent,所以,第一个测试中,book的版本应该为1,而第二个则为2:
[Fact] public void CreateBookTest() { // Arrange & Act var book = new Book(); // Assert Assert.NotEqual(Guid.Empty, book.Id); Assert.Equal(1, book.Version); } [Fact] public void ChangeBookTitleEventTest() { // Arrange var book = new Book(); // Act book.ChangeTitle("Hit Refresh"); // Assert Assert.Equal("Hit Refresh", book.Title); Assert.Equal(2, book.UncommittedEvents.Count()); Assert.Equal(2, book.Version); }接下来,测试仓储保存Book聚合的正确性,因为我们没有实现一个有效的仓储实例,因此,这里借助Moq帮我们动态生成。在下面的代码中,让Moq对仓储抽象类的PersisDomainEventsAsync受保护成员进行动态生成,指定当它被任何IEnumerable<IDomainEvent>作为参数调用时,都将这些事件保存到一个本地的List中,于是,最后只需要检查List中的领域事件是否符合我们的要求就可以了。代码如下:
[Fact] public async Task PersistBookTest() { // Arrange var domainEventsList = new List<IDomainEvent>(); var mockRepository = new Mock<Repository>(); mockRepository.Protected().Setup<Task>("PersistDomainEventsAsync", ItExpr.IsAny<IEnumerable<IDomainEvent>>()) .Callback<IEnumerable<IDomainEvent>>(evnts => domainEventsList.AddRange(evnts)) .Returns(Task.CompletedTask); var book = new Book(); // Act book.ChangeTitle("Hit Refresh"); await mockRepository.Object.SaveAsync(book); // Assert Assert.Equal(2, domainEventsList.Count); Assert.Empty(book.UncommittedEvents); Assert.Equal(2, book.Version); }