由于事件溯源(Event Sourcing)的需要,领域事件需要被保存到外部的存储系统中。由于事件本身描述了在特定对象上所发生的事情,因此,为了能够跟踪对象状态的变化过程以获得Event Audit的能力,我们总是将事件的数据保存在存储系统中,而从来不去删除它们。或许你会认为,这样做有点极端,时间长了,存储系统中的数据量将变得非常庞大。遇到这种情况,你需要引入备份和归档策略,而不是直接将过期的数据删除,因为,存储成本是便宜的,但数据却是有价值的。
对于一些生命周期比较长的领域对象而言,发生在它们身上的事件数量会随着时间的推移而增大,甚至会变得巨大。于是,通过使用这大量的事件数据来重建领域模型将变得非常耗时。因此,我们需要引入“快照”的概念。当领域对象满足一个特定的条件时(快照策略),系统就会为之生成一个“快照”。比如,系统架构师可以设置:当有n个事件发生在某个领域对象上时,将会为这个对象产生“快照”。下次当系统需要重建这个对象时,只需要从“快照”存储中获得最近一次的快照数据,通过快照数据产生对象,进而再逐个地将发生在“快照”之后的事件逐个地“重现”在这个对象上,于是,对象就可以被快速地还原到最后一个事件发生时的状态。
就Apworks框架而言,当前所支持的快照策略非常简单。它由Apworks.Events.Storage.IDomainEventStorage.CanCreateOrUpdateSnapshot方法定义,而Apworks.Events.Storage.SqlDomainEventStorage类则用这样一种快照策略实现了这个接口:每当第1000个事件发生时,系统就会为相应的领域对象做一次快照。因此,如果你打算在你的应用程序中继续采用SQL Server作为事件溯源的存储系统,并且打算定义自己的快照策略的话,你可以创建一个继承于Apworks.Events.Storage.SqlDomainEventStorage的类,并重写CanCreateOrUpdateSnapshot方法。如果你打算选用其它的存储系统(比如MySQL,Oracle,NoSQL方案或者内存数据库等),那么你就需要创建一个实现Apworks.Events.Storage.IDomainEventStorage接口的类,然后实现CanCreateOrUpdateSnapshot方法。
现在让我们返回到TinyLibraryCQRS解决方案,在前面的章节中,我们创建了两个聚合根:Reader和Book,在这两个聚合根里,有两个未实现的方法。现在我们来实现这两个方法以便让我们的应用程序支持快照功能。
- 右键单击TinyLibrary.Domain项目,单击 Add | New Folder 菜单,将 New Folder 重命名为 Snapshots
-
用下面的方法创建一个BookSnapshot类
1: using System;
2: using Apworks.Snapshots;
3:
4: namespace TinyLibrary.Domain.Snapshots
5: {
6: [Serializable]
7: public class BookSnapshot : Snapshot
8: {
9: public string Title { get; set; }
10: public string Publisher { get; set; }
11: public DateTime PubDate { get; set; }
12: public string ISBN { get; set; }
13: public int Pages { get; set; }
14: public bool Lent { get; set; }
15: }
16: }
注意我们使用System.SerializableAttribute特性标注了上面的类,因为我们需要将快照数据保存到存储系统中。你也会注意到,这个类的结构跟Book实体的结构很像,这是因为快照本身的作用就是反映并存储与之对应的实体在某个时间点的状态。 -
同理,添加ReaderSnapshot类
1: using System;
2: using System.Collections.Generic;
3: using Apworks.Snapshots;
4:
5: namespace TinyLibrary.Domain.Snapshots
6: {
7: [Serializable]
8: public class ReaderSnapshot : Snapshot
9: {
10: public string LoginName { get; set; }
11: public string Name { get; set; }
12:
13: public List<BookSnapshot> Books { get; set; }
14: }
15: }
-
回到Book类,用下面的方式实现DoBuildFromSnapshot和DoCreateSnapshot方法
1: protected override void DoBuildFromSnapshot(ISnapshot snapshot)
2: {
3: BookSnapshot bs = (BookSnapshot)snapshot;
4: this.Title = bs.Title;
5: this.Publisher = bs.Publisher;
6: this.PubDate = bs.PubDate;
7: this.ISBN = bs.ISBN;
8: this.Pages = bs.Pages;
9: this.Lent = bs.Lent;
10: }
11:
12: protected override ISnapshot DoCreateSnapshot()
13: {
14: BookSnapshot bs = new BookSnapshot();
15: bs.ISBN = this.ISBN;
16: bs.Lent = this.Lent;
17: bs.Pages = this.Pages;
18: bs.PubDate = this.PubDate;
19: bs.Publisher = this.Publisher;
20: bs.Title = this.Title;
21: return bs;
22: }
-
回到Reader类,用下面的方式实现DoBuildFromSnapshot和DoCreateSnapshot方法
1: protected override void DoBuildFromSnapshot(ISnapshot snapshot)
2: {
3: ReaderSnapshot rs = (ReaderSnapshot)snapshot;
4: this.Books.Clear();
5: foreach (var bk in rs.Books)
6: {
7: this.Books.Add(Book.Create(bk.AggregateRootId,
8: bk.Title,
9: bk.Publisher,
10: bk.PubDate,
11: bk.ISBN,
12: bk.Pages,
13: bk.Lent));
14: }
15: this.LoginName = rs.LoginName;
16: this.Name = rs.Name;
17: }
18:
19: protected override ISnapshot DoCreateSnapshot()
20: {
21: ReaderSnapshot rs = new ReaderSnapshot();
22: rs.Books = new List<BookSnapshot>();
23: foreach (var bk in this.Books)
24: {
25: rs.Books.Add((BookSnapshot)bk.CreateSnapshot());
26: }
27: rs.LoginName = this.LoginName;
28: rs.Name = this.Name;
29: return rs;
30: }
-
在完成了上面的所有步骤后,我们的Reader和Book类定义如下:
1: using System;
2: using Apworks;
3: using Apworks.Snapshots;
4:
5: namespace TinyLibrary.Domain
6: {
7: public class Book : SourcedAggregateRoot
8: {
9: public string Title { get; private set; }
10: public string Publisher { get; private set; }
11: public DateTime PubDate { get; private set; }
12: public string ISBN { get; private set; }
13: public int Pages { get; private set; }