CodeSharp.EventSourcing框架介绍-开篇
去年一次组内的技术分享会上,和几个同事讨论了Event Sourcing模式和CQRS(命令与查询分离)架构,当时觉得应该很有价值对这两个东西进行实践。所以在今年年初,我开始设计一个用于实现Event Sourcing模式和CQRS架构的框架。到目前为止框架基本稳定了,所以在园子里分享给大家。
如果大家还不清楚什么是Event Sourcing或CQRS,没关系,可以看一下以下两篇文章就大概了解了。
- http://www.cnblogs.com/netfocus/archive/2012/02/12/2347911.html
- http://www.cnblogs.com/netfocus/archive/2012/02/12/2347910.html
框架概况:
- 项目名称:CodeSharp.EventSourcing
- 开源地址:https://github.com/tangxuehua/eventsourcing
- nuget package Id:CodeSharp.EventSourcing
注:编译调试源码前请务必先用记事本打开阅读README.cd
框架特色:
- 代码干净整洁,风格一致,可读性强;
- 高度灵活可配置且可扩展,完全面向接口编程,所有组件全部可替换,包括容器本身;
- 编程模型单一清晰,与DDD经典架构无缝集成,为DDD设计开发提供很多支持;
- 支持事件的同步和异步订阅,同步订阅时可确保事件持久化与显示表持久化在一个事务中完成;
- 例子丰富,容易入门;
使用该框架的应用的标准架构:
- UI层命令端->应用层->领域层->事件订阅者(持久化显示表的数据)
- UI层查询端->直接调用框架提供的接口查询所需数据
快速入门的例子:
首先看看UI层的代码:
1 static void Main(string[] args) 2 { 3 var assembly = Assembly.GetExecutingAssembly(); 4 Configuration.Config("EventSourcing.Sample.SyncEventBus", assembly, assembly); 5 6 var noteService = ObjectContainer.Resolve<INoteService>(); 7 var note = noteService.CreateNote("Sample Note"); 8 noteService.ChangeTitle(note.Id, "Updated Note Title"); 9 10 Console.Write("Press Enter to exit..."); 11 Console.ReadLine(); 12 }
说明:首先调用Configuration.Config方法初始化框架,然后从容器获取一个应用层服务接口,然后调用服务接口的相关方法完成指定操作;
接下来是应用层的代码:
public interface INoteService { Note CreateNote(string title); void ChangeTitle(Guid id, string title); } public class NoteService : INoteService { private IContextManager _contextManager; public NoteService(IContextManager contextManager) { _contextManager = contextManager; } public Note CreateNote(string title) { using (var context = _contextManager.GetContext()) { var note = new Note(title); context.Add(note); context.SaveChanges(); return note; } } public void ChangeTitle(Guid id, string title) { using (var context = _contextManager.GetContext()) { var note = context.Load<Note>(id); note.ChangeTitle(title); context.SaveChanges(); } } }
所有的应用层方法总是先获取一个操作的上下文,然后调用context的Load或Add来加载某个聚合根或将某个聚合根添加到context,整个操作完成后总是需要调用SaveChanges明确告诉框架保存当前context内发生的所有变化。context.SaveChanges方法主要完成的事情是:1)持久化事件;2)publish事件给外部订阅者,支持同步和异步两种方式;注意:我们每次使用完context对象后总是应该释放该对象,否则context对象的生命周期会显得混乱;
接下来是领域层的代码:
public class Note : AggregateRoot<Guid> { public string Title { get; private set; } public DateTime CreatedTime { get; private set; } public DateTime UpdatedTime { get; private set; } public Note() { } public Note(string title) : base(Guid.NewGuid()) { var currentTime = DateTime.Now; OnEventHappened(new NoteCreated(Id, title, currentTime, currentTime)); } public void ChangeTitle(string title) { OnEventHappened(new NoteTitleChanged(Id, title, DateTime.Now)); } } [SourcableEvent] public class NoteCreated { public Guid Id { get; private set; } public string Title { get; private set; } public DateTime CreatedTime { get; private set; } public DateTime UpdatedTime { get; private set; } public NoteCreated(Guid id, string title, DateTime createdTime, DateTime updatedTime) { Id = id; Title = title; CreatedTime = createdTime; UpdatedTime = updatedTime; } } [SourcableEvent] public class NoteTitleChanged { public Guid Id { get; private set; } public string Title { get; private set; } public DateTime UpdatedTime { get; private set; } public NoteTitleChanged(Guid id, string title, DateTime updatedTime) { Id = id; Title = title; UpdatedTime = updatedTime; } }
Note是一个聚合根,每个聚合根需要继承自框架的聚合根基类AggregateRoot<TAggregateRootKey>,NoteCreated,NoteTitleChanged是两个可溯源事件,可溯源事件会在context.SaveChanges的时候被持久化。EventSourcing模式的一个核心特征就是,一个对象总是通过事件来驱动状态的修改,也就是说,这里一个聚合根如果要修改自己的状态,那必须先实例化并触发一个事件,然后聚合根内部响应该事件作出状态更新;当然,这里大家没看到聚合根更新自己状态的代码,因为框架默认已经做掉了,只要基于约定,程序员不需要根据事件去更新聚合根的状态;聚合根基类提供了几个关键的方法确保程序员能够方便的实现领域模型:
- OnEventHappened(object evnt); //通知框架某个事件发生了,这个方法最常用
- OnAggregateRootCreated(AggregateRoot instance); //通知框架某个聚合根创建了,当我们在一个聚合根的方法中创建另一个聚合根时需要调用该方法
- WakeupAggregateRoot<T>(object id); //通知框架唤醒某个指定的聚合根,如果一个聚合根对另外一个聚合根只有一个ID关联,但是现在想调用那个聚合根的方法,则可以通过此方法唤醒该聚合根
接下来就是事件订阅者了:
public class NoteEventSubscriber { private IDbConnection _connection; private IDbTransaction _transaction; public NoteEventSubscriber(ICurrentDbTransactionProvider transactionProvider) { _transaction = transactionProvider.CurrentTransaction; _connection = _transaction.Connection; } [SyncEventHandler] public void Handle(NoteCreated evnt) { _connection.Insert(evnt, "EventSourcing_Sample_Note", _transaction); } [SyncEventHandler] public void Handle(NoteTitleChanged evnt) { _connection.Update( new { Title = evnt.Title, UpdatedTime = evnt.UpdatedTime }, new { Id = evnt.Id }, "EventSourcing_Sample_Note", _transaction); } }
事件订阅者很简单,就是响应聚合根所发出来的事件。SyncEventHandler表示以同步的方式响应事件,也可以通过异步的方式响应,用AsyncEventHandler特性即可。当然,前面说过,所有上面用到的特性你都可以不用,你完全可以使用你自己的方法来定义怎样才是一个事件,怎样才是一个事件订阅者。我接下来还会写大量文章介绍如何扩展和自定义该框架;上吗的Insert,Update方法非常简单,就是往显示表中插入数据或更新数据,框架默认集成了dapper,一个非常轻量级且高效的ORM,我对他进行了一些扩展并修复了一个Bug。确保我们用起来非常方便,这里只要我们按照基于约定的思路,加上.net dynamic对象的支持,让我们可以非常方便的实现各种数据持久化或查询。这使得我再也不想去用其他任何ORM,之前我一直用NHibernate来做数据持久化,而NHibernate与完全基于约定的dapper比起来,简直太麻烦了。在Event Sourcing以及CQRS的架构下,查询端的数据持久化以及查询逻辑应该用最简单灵活高效的方式来实现,经过我的不断摸索,如果数据是持久化在数据库表里,那框架目前实现的方式应该是最简单的。
简单说明一下Insert方法的实现:接受一个对象,该对象只要属性名称与数据库字段名一致,那我们无需映射,就可以直接将对象包含的数据插入到表,第二个参数就是告诉框架应该把数据插入到哪个表;第三个参数是一个当前的事务。context.SaveChanges方法被调用时,框架会先将事件持久化到数据库,持久化之前会先启动一个事务,事件持久化好之后,会通过事件总线publish这些事件,此时上面的同步事件订阅者就会响应这些事件,因为我们要确保这些同步的事件订阅者也在同一个事务内,所以需要将当前的事务对象也传递给Insert方法。
Update方法:第一个参数表示要更新哪几个字段,我们可以非常方便的定义一个匿名对象来告诉框架该更新哪几个字段,匿名对象的属性名与实际数据库字段名一致即可;第二个参数告诉框架要更新的记录的条件信息,同样也是基于约定;
就这么简单,数据就自动新增或更新到数据库了,我们无需映射,也无需定义任何Entity。另外,我们呀查询数据时,也无需定义实体,因为查询出来的数据如果不指定类型,则默认就是dynamic类型,然后我们可以再controller中对该dynamic对象json序列化,然后传递给前台页面使用。这个过程中,我们无需定义任何实体,感觉太爽了。尤其是如果前段页面使用类似AngularJS这样的js MVVM框架的话,那可以直接将json和html元素进行双向绑定,简直妙不可言!
今天先大概介绍一下该框架以及简单常用的使用方法。接下来开始根据各个不同的已经准备好的例子来具体介绍如何使用框架的方方面面。下图是框架源码的结构图: