在《当我们在讨论CQRS时,我们在讨论些神马》中,我们讨论了当使用CQRS的过程中,需要关心的一些问题。其中与CQRS关联最为紧密的模式莫过于Event Sourcing了,CQRS与ES的结合,为我们构造高性能、可扩展系统提供了基本思路。本文将介绍

Kanasz Robert在《Introduction to CQRS》中的示例项目Diary.CQRS。

获取Diary.CQRS项目

该项目为Kanasz Robert为了介绍CQRS模式而写的一个测试项目,原始项目可以通过访问《Introduction to CQRS》来获取,由于项目版本比较旧,没有使用nuget管理程序包等,导致下载以后并不能正常运行,我下载了这个项目,升级到Visual Studio 2017,重新引用了StructMap框架(使用nuget),移除了Web层报错的代码,并上传到博客园,可以从这里下载:Diary.CQRS.rar

Diary.CQRS项目简介

Diary.CQRS项目的场景为日记本管理,提供了新增、编辑、删除、列表等功能,整个解决方案分为三个项目:

  • Diary.CQRS:核心项目,完成了EventBus、CommandBus、Domain、Storage等功能,也是我们分析的重点。
  • Diary.CQRS.Configuration:服务配置,通过ServiceLocator类进行依赖注入、服务查找功能。
  • Diary.CQRS.Web:用户界面,MVC项目。

这是一个很好的入门项目,功能简单、结构清晰,概念覆盖全面。如果CQRS是一个城堡,那么Diary.CQRS则是打开第一重门的钥匙,接下来让我们一起推开这扇门吧。

Diary.CQRS.Web

运行项目,最先看到的是一个Web页面,如下图:

很简单,只有一个Add按钮,当我们点击以后,会进入添加的页面:

我们填上一些内容,然后点击Save按钮,就会返回到列表页,我们可以看到已添加的条目:

然后我们进行编辑操作,点击列表中的Edit按钮,跳转到编辑页面:

虽然页面中显示的是Add,但确实是Edit页面。我们编辑以后点击Save按钮,然后返回列表页即可看到编辑后的内容。

在列表页中,如果我们点击Delete按钮,则会删除改条目。

到此为止,我们已经看到了这个项目的所有页面,一个简单的CURD操作。我们继续看它的代码(在HomeController中)。

Index:列表页面

  1. public ActionResult Index()
  2. {
  3. ViewBag.Model = ServiceLocator.ReportDatabase.GetItems();
  4. return View();
  5. }

通过ServiceLocator定位ReportDatabase,并从ReportDatabase中获取所有条目。

Add:新增页面

  1. public ActionResult Add()
  2. {
  3. return View();
  4. }
  5. [HttpPost]
  6. public ActionResult Add(DiaryItemDto item)
  7. {
  8. ServiceLocator.CommandBus.Send(new CreateItemCommand(Guid.NewGuid(), item.Title, item.Description, -1, item.From, item.To));
  9. return RedirectToAction("Index");
  10. }

两个方法:

  • Add()方法,处理Get请求,返回新增视图;
  • Add(DiaryItemDto item)方法,接收DiaryItemDto参数,处理Post请求,创建并发送CreateItemCommand命令,然后返回到Index页面

Edit:编辑页面

  1. public ActionResult Edit(Guid id)
  2. {
  3. var item = ServiceLocator.ReportDatabase.GetById(id);
  4. var model = new DiaryItemDto()
  5. {
  6. Description = item.Description,
  7. From = item.From,
  8. Id = item.Id,
  9. Title = item.Title,
  10. To = item.To,
  11. Version = item.Version
  12. };
  13. return View(model);
  14. }
  15. [HttpPost]
  16. public ActionResult Edit(DiaryItemDto item)
  17. {
  18. ServiceLocator.CommandBus.Send(new ChangeItemCommand(item.Id, item.Title, item.Description, item.From, item.To, item.Version));
  19. return RedirectToAction("Index");
  20. }

仍然是两个方法:

  • Edit(Guid id)方法,接收Guid作为参数,并从ReportDatabase中获取数据,构建dto对象返回给页面
  • Edit(DiaryItemDto item)方法,接收DiaryItemDto对象,处理Post请求,接收到请求以后根据dto对象创建ChangeItemCommand命令,然后返回到Index页面

Delete:删除操作

  1. public ActionResult Delete(Guid id)
  2. {
  3. var item = ServiceLocator.ReportDatabase.GetById(id);
  4. ServiceLocator.CommandBus.Send(new DeleteItemCommand(item.Id, item.Version));
  5. return RedirectToAction("Index");
  6. }

对于删除操作来说,它没有视图页面,接收到请求以后,先获取该记录,创建并发送DeleteImteCommand命令,然后返回到Index页面

题外话:对于改变数据状态的操作,使用Get请求是不可取的,可能存在安全隐患

通过上面的代码,你会发现所有的操作都是从ServiceLocator发起的,通过它我们能够定位到CommandBus和ReportDatabase,从而进行相应的操作,我们在接下来会介绍ServiceLocator类。

Diary.CQRS.Configuration

Diary.CQRS.Configuration 项目中定义了ServiceLocator类,这个类的作用是完成IoC容器的服务注册、服务定位功能。例如我们可以通过ServiceLocator获取到CommandBus实例、获取ReportDatabase实例。

服务注册

ServiceLocator使用StructureMap作为依赖注入框架,提供了服务注册、服务导航的功能。ServiceLocator类通过静态构造函数完成对服务注册和服务实例化工作:

  1. static ServiceLocator()
  2. {
  3. if (!_isInitialized)
  4. {
  5. lock (_lockThis)
  6. {
  7. ContainerBootstrapper.BootstrapStructureMap();
  8. _commandBus = ObjectFactory.GetInstance<ICommandBus>();
  9. _reportDatabase = ObjectFactory.GetInstance<IReportDatabase>();
  10. _isInitialized = true;
  11. }
  12. }
  13. }

首先调用ContainerBootstrapper.BootstrapStructureMap()方法,这个方法里面包含了对将服务添加到容器的代码;然后使用容器创建CommandBus和ReportDatabase的实例。

  • CommandBus:命令总线,对应Command操作,用来发送命令,程序中需要定义相应的命令处理器,从而完成具体的操作。
  • ReportDatabase:报表数据库,对应Query操作,用来获取数据。

ServiceLocator的重要之处在于对外暴露了两个至关重要的实例,分别处理CQRS中的Command和Query。

为什么没有Event相关操作呢?到目前为止我们还没有涉及到,因为对于UI层来说,用户的意图都是通过Command表示的,而数据的状态变化才会触发Event。

Diary.CQRS

在ServiceLocator中定义了获取CommandBus和ReportDatabase的方法,我们顺着这两个对象继续分析。

CommandBus

在基于消息的系统设计中,我们常会看到总线的身影,Command也是一种消息,所以使用总线是再合适不过的了。CommandBus就是我们在Diary.CQRS项目中用到的一种消息总线。

在Diary.CQRS中,它被定义在Messaging目录,在这个目录下面,还有与Event相关的EventBus,我们稍后再进行介绍。

CommandBus实现ICommandBus接口,ICommandBus接口的定义如下:

  1. public interface ICommandBus
  2. {
  3. void Send<T>(T command) where T : Command;
  4. }

它只包含了Send方法,用来将命令发送到对应的处理程序。

CommandBus是ICommand的实现,具体代码如下:

  1. public class CommandBus:ICommandBus
  2. {
  3. private readonly ICommandHandlerFactory _commandHandlerFactory;
  4. public CommandBus(ICommandHandlerFactory commandHandlerFactory)
  5. {
  6. _commandHandlerFactory = commandHandlerFactory;
  7. }
  8. public void Send<T>(T command) where T : Command
  9. {
  10. var handler = _commandHandlerFactory.GetHandler<T>();
  11. if (handler!=null)
  12. {
  13. handler.Execute(command);
  14. }
  15. else
  16. {
  17. throw new Exception();
  18. }
  19. }
  20. }

在CommandBus中,显式依赖ICommandHandlerFactory类,通过构造函数进行注入。那么 _commandHandlerFactory 的作用是什么呢?我们在Send方法中可以看到,通过 _commandHandlerFactory 可以获取到与Command对应的CommandHandler(命令处理程序),在程序的设计上,每一个Command都会有一个对应的CommandHandler,而手工判断类型、实例化处理程序显然不符合使用习惯,此处采用工厂模式来获取命令处理程序。

当获取到与Command对应的CommandHandler后,调用handler的Execute方法,执行该命令。

截止目前为止,我们又接触了三个概念:CommandHandlerFactory、CommandHandler、Command:

  • CommandHandlerFactory:命令处理程序工厂,通过GetHandler方法获取到与命令对应的处理程序
  • CommandHandler:命令处理程序,用于执行对应的命令
  • Command:命令,描述用户的意图、并包含与意图相关的数据

CommandHandlerFactory

使用简单工厂模式,用来获取与命令对应的处理程序。它的代码在Utils文件夹中,它的作用是提供一种获取Handler的方式,所以它只能作为工具存在。

接口定义如下:

  1. public interface ICommandHandlerFactory
  2. {
  3. ICommandHandler<T> GetHandler<T>() where T : Command;
  4. }

只有GetHandler一个方法,它的实现是 StructureMapCommandHandlerFactory,即通过StructureMap作为依赖注入框架来实现的,代码也比较简单,这里不再贴出来了。

Command和CommandHandler

命令是代表用户的意图、并包含与意图相关的数据,比如用户想要添加一条数据,这便是一个意图,于是就有了CreateItemCommand,用户要在界面上填写添加操作必须的数据,于是就有了命令的属性。

关于命令的定义如下:

  1. public interface ICommand
  2. {
  3. Guid Id { get; }
  4. }
  5. public class Command : ICommand
  6. {
  7. public Guid Id { get; private set; }
  8. public int Version { get; set; }
  9. public Command(Guid id, int version)
  10. {
  11. Id = id;
  12. Version = version;
  13. }
  14. }
  • ICommand接口:包含Id属性,这个Id表示Command对应聚合的Id。聚合是领域驱动开发(DDD)的概念,表示一组强关联的领域对象,而对聚合中状态的变更,只能通过聚合根(AggregateRoot)来完成。
  • Command类:实现了ICommand接口,并增加了Version属性,用来标记当前操作对应的聚合跟的版本。

为什么要有版本的概念的?因为当使用ES模式的时候,数据库中的数据都是事件产生的数据镜像,保存了某个时间点的数据快照,如果要获取到最新的数据,则需要通过加载该聚合根对应的所有Event来回放到最新状态。如果引入版本的概念,每一个Event对应一个版本,而景象中的数据也有一个版本,在进行回放的时候,可以仅加载高版本的Event进行回放,节省了系统资源,并提高了运行效率。

命令处理程序,它的作用是处理与它相对应的命令,处理CQRS的核心,接口定义如下:

  1. public interface ICommandHandler<TCommand> where TCommand : Command
  2. {
  3. void Execute(TCommand command);
  4. }

它接收command作为参数,执行该命令的处理逻辑。每一个命令都有一个与之对应的处理程序。

我们再重新梳理一下流程,首先用户要新增一个数据,点击保存按钮后,生成CreateItemCommand命令,随后这个命令被发送到CommandBus中,CommandBus通过CommandHandlerFactory找到该Command的处理程序,此时在CommandBus的Send方法中,我们有一个Command和CommandHandler,然后调用CommandHandler的Execute方法,即完成了该方法的处理。至此,Command的处理流程完结。

CreateItemCommand和CreateItemCommandHandler

我们来看一下CreateItemCommand的代码:

  1. public class CreateItemCommand : Command
  2. {
  3. public string Title { get; internal set; }
  4. public string Description { get; internal set; }
  5. public DateTime From { get; internal set; }
  6. public DateTime To { get; internal set; }
  7. public CreateItemCommand(Guid aggregateId, string title,
  8. string description, int version, DateTime from, DateTime to)
  9. : base(aggregateId, version)
  10. {
  11. Title = title;
  12. Description = description;
  13. From = from;
  14. To = to;
  15. }
  16. }

它继承自Command基类,继承后即拥有了Id和Version属性,然后又定义了几个其它的属性。它只包含数据,与该命令对应的处理程序叫做CreateItemCommandHandler,代码如下:

  1. public class CreateItemCommandHandler : ICommandHandler<CreateItemCommand>
  2. {
  3. private IRepository<DiaryItem> _repository;
  4. public CreateItemCommandHandler(IRepository<DiaryItem> repository)
  5. {
  6. _repository = repository;
  7. }
  8. public void Execute(CreateItemCommand command)
  9. {
  10. if (command == null)
  11. {
  12. throw new Exception();
  13. }
  14. if (_repository == null)
  15. {
  16. throw new Exception();
  17. }
  18. var aggregate = new DiaryItem(command.Id, command.Title, command.Description, command.From, command.To);
  19. aggregate.Version = -1;
  20. _repository.Save(aggregate, aggregate.Version);
  21. }
  22. }

这才是我们要分析的核心,在Handler中,我们看到了Repository,看到了DiaryItem聚合:

  • IRepository:仓储类,代表数据的储存方式,通过仓储能够进行数据操作
  • DiaryItem:领域对象,聚合根,所有数据状态的变更只能通过聚合根来修改

在上面的代码中,由于是新增,所以聚合的版本为-1,然后调用仓储的Save方法进行保存。我们继续往下扒,看看仓储和聚合的实现。

Repository

对于Repository的定义,仍然先看一下接口中的定义,代码如下:

  1. public interface IRepository<T> where T : AggregateRoot, new()
  2. {
  3. void Save(AggregateRoot aggregate, int expectedVersion);
  4. T GetById(Guid id);
  5. }

在仓储中只有两个方法:

  • Save(AggregateRoot aggregate, int expectedVersion):保存期望版本的聚合根
  • GetById(Guid id):根据聚合根Id获取聚合根

关于IRepository的实现,代码在Repository.cs中,我们拆开来进行介绍:

  1. private readonly IEventStorage _eventStorage;
  2. private static object _lock = new object();
  3. public Repository(IEventStorage eventStorage)
  4. {
  5. _eventStorage = eventStorage;
  6. }

首先是它的构造函数,强依赖IEventStorage,通过构造函数注入。EventStorage是事件的储存仓库,有个更为熟知的名字EventStore,我们稍后进行介绍。

  1. public T GetById(Guid id)
  2. {
  3. IEnumerable<Event> events;
  4. var memento = _eventStorage.GetMemento<BaseMemento>(id);
  5. if (memento != null)
  6. {
  7. events = _eventStorage.GetEvents(id).Where(e => e.Version >= memento.Version);
  8. }
  9. else
  10. {
  11. events = _eventStorage.GetEvents(id);
  12. }
  13. var obj = new T();
  14. if (memento != null)
  15. {
  16. ((IOriginator)obj).SetMemento(memento);
  17. }
  18. obj.LoadsFromHistory(events);
  19. return obj;
  20. }

GetById(Guid id)方法通过Id获取一个聚合对象,获取一个聚合对象有以下几个步骤:

  • 首先会从EventStorage中获取到该聚合的快照(memento的翻译为记忆碎片、纪念品、备忘录,用来聚合对象的快照)。
  • 加载Event列表,加载到的事件列表将用来做事件回放。

如果获取到快照的话,则加载版本高于该快照版本的事件列表,如果没有获取到快照,则加载全部事件列表。此处在上面已经介绍过,通过快照的方式保存聚合对象,在获取数据时可以减少重放事件的数量,起到提高加载速度的作用。

  • 实例化聚合根,对应代码中的var obj = new T();
  • 从快照中设置聚合根的状态。在获取到快照以后,如果快照不为空,则调用聚合根的SetMemento方法设置为快照中的状态,SetMemento方法定义在IOriginator接口中,聚合根需要实现该接口。
  • 加载历史事件,完成重放。完成这个步骤以后,聚合根将更新到最新状态。

通过这几个步骤以后,我们得到了一个最新状态的聚合根对象。

  1. public void Save(AggregateRoot aggregate, int expectedVersion)
  2. {
  3. if (aggregate.GetUncommittedChanges().Any())
  4. {
  5. lock (_lock)
  6. {
  7. var item = new T();
  8. if (expectedVersion != -1)
  9. {
  10. item = GetById(aggregate.Id);
  11. if (item.Version != expectedVersion)
  12. {
  13. throw new Exception();
  14. }
  15. }
  16. _eventStorage.Save(aggregate);
  17. }
  18. }
  19. }

Save方法,用来保存一个聚合根对象。在这个方法中,参数expectedVersion表示期望的版本,这里约定-1为新增的聚合根,当聚合根为新增的时候,会直接调用EventStorage中的Save方法。

关于expectedVersion参数,我们可以理解为对并发的控制,只有当expectedVersion与GetById获取到的聚合根对象的版本相同时才能进行保存操作。

在介绍Repository类的时候,我们接触了两个新的概念:EventStorage和AggregateRoot,接下来我们分别进行介绍。

AggregateRoot

AggregateRoot是聚合根,他表示一组强关联的领域对象,所有对象的状态变更只能通过聚合根来完成,这样可以保证数据的一致性,以及减少并发冲突。应用到EventSourcing模式中,聚合根的好处也是很明显的,我们所有对数据状态的变更都通过聚合根完成,而每次变更,聚合根都会生成相应的事件,在进行事件回放的时候,又通过聚合根来完成历史事件的加载。由此我们可以看到,聚合根对象应该具备生成事件、重放事件的能力。

我们来看看聚合根基类的定义,在Domain文件夹中:

  1. public abstract class AggregateRoot : IEventProvider{
  2. // ......
  3. }

首先这是一个抽象类,实现了IEventProvider接口,该接口的定义如下:

  1. public interface IEventProvider
  2. {
  3. void LoadsFromHistory(IEnumerable<Event> history);
  4. IEnumerable<Event> GetUncommittedChanges();
  5. }

它定义了两个方法,我们分别进行说明:

  • LoadsFromHistory()方法:加载历史事件,还原聚合根的最新状态,我们在Repository中已经用过这个方法。
  • GetUncommittedChanges()方法:获取未提交的事件。一个命令可能造成聚合根发生多次更改,每次更改都会产生一个事件,这些事件被暂时的保存在聚合根对象中,通过该方法可以获取到未提交的事件列表。

为了实现这个接口,聚合根中定义了 List<Event> _changes对象,用来临时存储所有未提交的事件,该对象在构造函数中进行初始化。

AggregateRoot中对于该事件的实现如下:

  1. public void LoadsFromHistory(IEnumerable<Event> history)
  2. {
  3. foreach (var e in history)
  4. {
  5. ApplyChange(e, false);
  6. }
  7. Version = history.Last().Version;
  8. EventVersion = Version;
  9. }
  10. public IEnumerable<Event> GetUncommittedChanges()
  11. {
  12. return _changes;
  13. }

LoadsFromHistory方法遍历历史事件,并调用ApplyChange方法更新聚合根的状态,在完成更新后设置版本号为最后一个事件的版本。GetUncommittedChanges方法比较简单,返回对象的_changes事件列表。

接下来我们看看ApplyChange方法,该方法有两个实现,代码如下:

  1. protected void ApplyChange(Event @event)
  2. {
  3. ApplyChange(@event, true);
  4. }
  5. protected void ApplyChange(Event @event, bool isNew)
  6. {
  7. dynamic d = this;
  8. d.Handle(Converter.ChangeTo(@event, @event.GetType()));
  9. if (isNew)
  10. {
  11. _changes.Add(@event);
  12. }
  13. }

这两个方法定义为protected,只能被子类访问。我们可以理解为,ApplyChange(Event @event)方法为简化操作,对第二个参数进行了默认为true的操作,然后调用ApplyChange(Event @event, bool isNew)方法。

在ApplyChange(Event @event, bool isNew)方法中,调用了聚合根的Handle方法,用来处理事件。如果isNew参数为true,则将事件添加到change列表中,如果为false,则认为是在进行事件回放,所以不进行事件的添加。

需要注意的是,聚合根的Handle方法,与EventHandler不同,当Event产生以后,首先由它对应的聚合根进行处理,因此聚合根要具备处理该事件的能力,如何具备呢?聚合根要实现IHandle接口,该接口的定义如下:

  1. public interface IHandle<TEvent> where TEvent:Event
  2. {
  3. void Handle(TEvent e);
  4. }

这里可以看出,IHandle接口是泛型的,它只对一个具体的Event类型生效,在代码上的体现如下:

  1. public class DiaryItem : AggregateRoot,
  2. IHandle<ItemCreatedEvent>,
  3. IHandle<ItemRenamedEvent>,
  4. IHandle<ItemFromChangedEvent>,
  5. IHandle<ItemToChangedEvent>,
  6. IHandle<ItemDescriptionChangedEvent>,
  7. IOriginator
  8. {
  9. //......
  10. }

最后,聚合根还定义了清除所有事件的方法,代码如下:

  1. public void MarkChangesAsCommitted()
  2. {
  3. _changes.Clear();
  4. }

MarkChangesAsCommitted()方法用来清空事件列表。

Event

终于到我们今天的另外一个核心内容了,Event是ES中的一等公民,所有的状态变更最终都以Event的形式进行存储,当我们要查看聚合根最新状态的时候,可以通过事件回放来获取。我们来看看Event的定义:

  1. public interface IEvent
  2. {
  3. Guid Id { get; }
  4. }

IEvent接口定义了一个事件必须拥有唯一的Id进行标识。然后Event实现了IEvent接口:

  1. public class Event:IEvent
  2. {
  3. public int Version;
  4. public Guid AggregateId { get; set; }
  5. public Guid Id { get; private set; }
  6. }

可以看到,除了Id属性外,还添加了两个字段Version和AggregateId。AggregateId表示该事件关联的聚合根Id,通过该Id可以获取到唯一的聚合根对象;Version表示事件发生时该事件的版本,每次产生新的事件,Version都会进行累加。

从而可以知道,在EventStorage中,聚合根Id对应的所有Event中的Version是顺序累加的,按照Version进行排序可以得到事件发生的先后顺序。

EventStorage

顾名思义,EventStorage是用来存储Event的地方。在Diary.CQRS中,EventStorage的定义如下:

  1. public interface IEventStorage
  2. {
  3. IEnumerable<Event> GetEvents(Guid aggregateId);
  4. void Save(AggregateRoot aggregate);
  5. T GetMemento<T>(Guid aggregateId) where T : BaseMemento;
  6. void SaveMemento(BaseMemento memento);
  7. }
  • GetEvents(Guid aggregateId):根据聚合根Id获取该聚合根的所有事件
  • Save(AggregateRoot aggregate):保存方法,入参为聚合根对象,在实现上则是获取聚合根中所有未提交的事件,随后对这些事件进行处理
  • GetMemento():获取快照
  • SaveMemento():存储快照

Diary.CQRS中使用InMemory的方式实现了EventStorage,属性和构造函数如下:

  1. private List<Event> _events;
  2. private List<BaseMemento> _mementoes;
  3. private readonly IEventBus _eventBus;
  4. public InMemoryEventStorage(IEventBus eventBus)
  5. {
  6. _events = new List<Event>();
  7. _mementoes = new List<BaseMemento>();
  8. _eventBus = eventBus;
  9. }
  • _events:事件列表,内存中存储事件的位置,所有事件最终都会存储在该列表中
  • _mementoes:快照列表,用于存储聚合根的某个事件版本的状态
  • _eventBus:事件总线,用于发布任务

当Event生成后,它并没有马上存入EventStorage,而是在Repository显示调用Save方法时,仓储将存储权交给了EventStorage,EventStorage是事件仓库,事件仓储在存储时进行了如下操作:

  • 获取聚合根中所有未提交的Event,同时获取到聚合根当前的版本号
  • 遍历未提交Event列表,根据聚合根版本号自动为Event生成版本号,保持自增长的特性;
  • 生成聚合根快照。示例中每3个版本生成一次,并保持到事件仓储中。
  • 将任务添加到事件仓库中。
  • 再次遍历未提交Event列表,此时将进行任务发布,调用事件总线的Publish方法进行发布。

Save方法的代码如下:

  1. public void Save(AggregateRoot aggregate)
  2. {
  3. var uncommittedChanges = aggregate.GetUncommittedChanges();
  4. var version = aggregate.Version;
  5. foreach (var @event in uncommittedChanges)
  6. {
  7. version++;
  8. if (version > 2)
  9. {
  10. if (version % 3 == 0)
  11. {
  12. var originator = (IOriginator)aggregate;
  13. var memento = originator.GetMemento();
  14. memento.Version = version;
  15. SaveMemento(memento);
  16. }
  17. }
  18. @event.Version = version;
  19. _events.Add(@event);
  20. }
  21. foreach (var @event in uncommittedChanges)
  22. {
  23. var desEvent = Converter.ChangeTo(@event, @event.GetType());
  24. _eventBus.Publish(desEvent);
  25. }
  26. }

至此Event的处理流程就算完结了。此时所有的操作都是在主库完成的,当事件被发布以后,订阅了该事件的所有Handler都将会被触发。

在Diary.CQRS项目中,EventHandler都被用来处理ReportDatabase了。

ReportDatabase

当你使用ES模式时,都存在一个严重问题,那就是数据查询的问题。当用户进行数据检索是,必然会使用各种查询条件,然而无论那种事件仓库都很难满足复杂查询。为了解决此问题,ReportDatabase就显得格外重要。

ReportDatabase的作用被定义为获取数据、应对数据查询、生成报表等,它的结构与主库不同,可以根据不同的业务场景进行定义。

ReportDatabase的数据不是通过业务逻辑进行更新的,它通过订阅Event进行更新。在本示例中ReportDatabase实现的很简单,接口定义如下:

  1. public interface IReportDatabase
  2. {
  3. DiaryItemDto GetById(Guid id);
  4. void Add(DiaryItemDto item);
  5. void Delete(Guid id);
  6. List<DiaryItemDto> GetItems();
  7. }

实现上,通过内存中维护一个列表,每次接收到事件以后,都对相应数据进行更新,此处不在贴出。

EventHandler、EventHandlerFactory和EventBus

在上文中已经介绍过Event,而针对Event的处理,实现逻辑上与Command非常相似,唯一的区别是,命令只可以有一个对应的处理程序,而事件则可以有多个处理程序。所以在EventHandlerFactory中获取处理程序的方法返回了EventHandler列表,代码如下:

  1. public IEnumerable<IEventHandler<T>> GetHandlers<T>() where T : Event
  2. {
  3. var handlers = GetHandlerType<T>();
  4. var lstHandlers = handlers.Select(handler => (IEventHandler<T>)ObjectFactory.GetInstance(handler)).ToList();
  5. return lstHandlers;
  6. }

在EventBus中,如果一个事件没有处理程序也不会引发错误,如果有一个或多个处理程序,则会以此调用他们的Handle方法,代码如下:

  1. public void Publish<T>(T @event) where T : Event
  2. {
  3. var handlers = _eventHandlerFactory.GetHandlers<T>();
  4. foreach (var eventHandler in handlers)
  5. {
  6. eventHandler.Handle(@event);
  7. }
  8. }

总结

Diary.CQRS是一个典型的CQRS+ES演示项目,通过对该项目的分析,我们能了解到Command、AggregateRoot、Event、EventStorage、ReportDatabase的基础知识,了解他们相互关系,尤其是如何进行事件存储、如何进行事件回放的内容。

另外,我们发现在使用CQRS+ES的过程中,项目的复杂度增加了很多,我们不可避免的要使用EventStore、Messaging等架构,从而影响那些不了解CQRS的团队成员的加入,因此在应用到实际项目的时候,要适可而止,慎重选择,避免过度设计。

由于这是一个示例,项目代码中存在很多不够严谨的地方,大家在学习的过程中应进行甄别。

由于本人的知识有限,如果内容中存在不准确或错误的地方,还请不吝赐教!

CQRS+ES项目解析-Diary.CQRS的更多相关文章

  1. CQRS+ES项目解析-Equinox

    今天我们来分析另一个开源的CQRS+ES项目:Equinox.该项目可以在github上下载并直接本地运行,项目地址:https://github.com/EduardoPires/EquinoxPr ...

  2. CQRS\ES架构介绍

    大家好,我叫汤雪华.我平时工作使用Java,业余时间喜欢用C#做点开源项目,如ENode, EQueue.我个人对DDD领域驱动设计.CQRS架构.事件溯源(Event Sourcing,简称ES). ...

  3. 用CQRS+ES实现DDD

    用CQRS+ES实现DDD 这篇文章应该算是对前三篇的一个补充,在写之前说个题外话,有园友评论这是在用三层架构在写DDD,我的个人理解DDD是一种设计思想,跟具体用什么架构应该没有什么关系,DDD也需 ...

  4. CQRS/ES框架调研

    1.Enode一个C#写的CQRS/ES框架,由汤雪华设计及实现,github上有相关源码,其个人博客上有详细的孵化.设计思路.版本迭代及最新的完善: 2.axon framwork,java编写,网 ...

  5. .net架构设计读书笔记--第三章 第10节 命令职责分离(CQRS)简介(Introducing CQRS)

    一.分离查询命令 Separating commands from queries     早期的面向DDD设计方法的难点是如何设计一个类,这个类要包含域的方方面面.通常来说,任务软件系统方法调用可以 ...

  6. Android开发周报:Flyme OS开源、经典开源项目解析

    Android开发周报:Flyme OS开源.经典开源项目解析 新闻 <魅族Flyme OS源码上线Github> :近日魅族正式发布了MX5,并且在发布会上,魅族还宣布Flyme OS开 ...

  7. renren-fast开源项目解析日志—1、项目的部署

    renren_fast项目解析日志 一.环境搭建 1.后端部署 (1)下载源码 按照步骤,从码云上down了fast,zip的(引maven项目)项目包. (2)安装lombok插件 安装lombok ...

  8. 分享一个CQRS/ES架构中基于写文件的EventStore的设计思路

    最近打算用C#实现一个基于文件的EventStore. 什么是EventStore 关于什么是EventStore,如果还不清楚的朋友可以去了解下CQRS/Event Sourcing这种架构,我博客 ...

  9. 一款不错的 Go Server/API boilerplate,使用 K8S+DDD+CQRS+ES+gRPC 最佳实践构建

    Golang API Starter Kit 该项目的主要目的是使用最佳实践.DDD.CQRS.ES.gRPC 提供样板项目设置. 为开发和生产环境提供 kubernetes 配置.允许与反映生产的 ...

随机推荐

  1. salesforce lightning零基础学习(十五) 公用组件之 获取表字段的Picklist(多语言)

    此篇参考:salesforce 零基础学习(六十二)获取sObject中类型为Picklist的field values(含record type) 我们在lightning中在前台会经常碰到获取pi ...

  2. 2019-11-26:密码学基础知识,csrf防御

    信息安全的基础是数学--->密码算法--->安全协议(ssl VPN)-->应用(证书 PKI)密码学入门密码编码学:研究加解密算法的学科密码分析学:研究破译密码算法的学科 加解密分 ...

  3. 2019-10-2,html作业,简历源码

    <html> <head> <title>简历作业</title> </head> <body bgcolor=#cccccc> ...

  4. day 31 网络基础的补充

    一.网络基础 1.端口 - 端口,是什么?为什么要有? 端口是为了将同一个电脑上的不同程序进行隔离. IP是找电脑 端口是找电脑上的程序 示例: MySQL是一个软件,软件帮助我们在硬盘上进行文件操作 ...

  5. cookies与session简介

    一.session和cookie 简单来讲cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案. 同时我们也看到,由于采用服务器端保持状态的方案在客户端 ...

  6. c#-EntitySet<TEntity>

    MSDN 解释: https://msdn.microsoft.com/zh-cn/library/bb341748.aspx 为 LINQ to SQL 应用程序中的一对多关系和一对一关系的集合方提 ...

  7. oralce迁移Mysql问题总结

    最近从oracle数据库迁移到Mysql, 总结了一些不兼容和需要注意的地方,持久层用的Mybatis 1 guid尽量用代码生成 现象:sys_guid()  mysql报错,mysql对应的为UU ...

  8. JavaScript-----2初识

    1.介绍 JavaScript是一种运行在客户端(自己的电脑上)的脚本语言不是在服务器上 脚本语言:不需要编译,运行过程由JS解释器(js引擎)逐行进行解释并执行 JavaScript不仅可以做前端编 ...

  9. Mac SourceTree配置Beyond Compare

    一   首先下载正版的Beyond Compare 地址:https://www.scootersoftware.com/download.php 二   如果bin文件夹下没有bcomp,打开终端命 ...

  10. Spring Boot中@ConditionalOnProperty使用详解

    在Spring Boot的自动配置中经常看到@ConditionalOnProperty注解的使用,本篇文章带大家来了解一下该注解的功能. Spring Boot中的使用 在Spring Boot的源 ...