使用Apworks开发基于CQRS架构的应用程序【领域事件】

本文详细介绍如何在CQRS架构中实现领域事件,包括定义领域事件、处理领域事件及创建事件处理器等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

根据wikipedia中关于“事件”的描述,“事件”可以被看成是“状态的一次变化”。例如:当一个客户购买了一台汽车,汽车的状态就从“待售”转变为“已售”。汽车销售系统则把这种状态的改变看成是一次事件的产生、发布、检测以及被更多其它应用程序所使用的过程。

对于CQRS架构的应用程序而言,事件产生于领域模型,并由领域模型发布事件同时由领域模型首次捕获并处理,因此,我们称之为领域事件(Domain Events)。在Apworks开发框架中,与领域事件相关的代码都被定义在Apworks.Events命名空间下。

请使用下面的步骤为TinyLibraryCQRS解决方案添加领域事件。

  1. Solution ExplorerTinyLibraryCQRS解决方案上,右键单击并选择Add | New Project…菜单,这将打开Add New Project对话框
  2. Installed Templates选项卡下,选择Visual C# | Windows,然后选择Class Library,确保所选的.NET Framework版本是.NET Framework 4,然后在Name文本框中,输入TinyLibrary.Events,并单击OK按钮
  3. Solution Explorer下,右键单击TinyLibrary.Events项目的References节点,然后选择Add Reference…菜单,这将打开Add Reference对话框
  4. .NET选项卡下,选择Apworks,然后单击OK按钮
  5. TinyLibrary.Events项目添加下面的类代码,以实现领域事件
       1: using System;
       2: using Apworks.Events;
       3: 
       4: namespace TinyLibrary.Events
       5: {
       6:     [Serializable]
       7:     public class BookBorrowedEvent : DomainEvent
       8:     {
       9:         public long BookId { get; set; }
      10:         public long ReaderId { get; set; }
      11:         public string ReaderName { get; set; }
      12:         public string ReaderLoginName { get; set; }
      13:         public string BookTitle { get; set; }
      14:         public string BookPublisher { get; set; }
      15:         public DateTime BookPubDate { get; set; }
      16:         public string BookISBN { get; set; }
      17:         public int BookPages { get; set; }
      18:         public bool BookLent { get; set; }
      19: 
      20:         public DateTime RegistrationDate { get; set; }
      21:         public DateTime DueDate { get; set; }
      22:     }
      23: 
      24:     [Serializable]
      25:     public class BookCreatedEvent : DomainEvent
      26:     {
      27:         public string Title { get; set; }
      28:         public string Publisher { get; set; }
      29:         public DateTime PubDate { get; set; }
      30:         public string ISBN { get; set; }
      31:         public int Pages { get; set; }
      32:         public bool Lent { get; set; }
      33:     }
      34: 
      35:     [Serializable]
      36:     public class BookGetReturnedEvent : DomainEvent
      37:     {
      38:         public long ReaderId { get; set; }
      39:         public DateTime ReturnedDate { get; set; }
      40:     }
      41: 
      42:     [Serializable]
      43:     public class BookLentEvent : DomainEvent
      44:     {
      45:         public long ReaderId { get; set; }
      46:         public DateTime LentDate { get; set; }
      47:     }
      48: 
      49:     [Serializable]
      50:     public class BookReturnedEvent : DomainEvent
      51:     {
      52:         public long ReaderId { get; set; }
      53:         public long BookId { get; set; }
      54:     }
      55: 
      56:     [Serializable]
      57:     public class ReaderCreatedEvent : DomainEvent
      58:     {
      59:         public string LoginName { get; set; }
      60:         public string Name { get; set; }
      61:     }
      62: }

从上面的代码我们可以看到,所有的领域事件都继承于Apworks.Events.DomainEvent类。同时,在每个领域事件上都是用了System.SerializableAttribute特性,以便系统能够序列化/反序列化这些领域事件,进而使其能够在网络上传输,或能够将其保存到存储系统中。

每当一次操作发生在我们的领域对象上(比如Book和Reader)时,这种操作有可能会改变对象的状态。例如:一个用来改变用户姓名的实例方法ChangeName会产生“用户姓名变更”的效果,于是,领域事件就由此产生了,最先被通知到该事件的就是领域对象本身,它会将这个事件记录下来,然后根据事件中所包含的数据来改变相应的状态。下面让我们看看,如何在Apworks框架的支持下,实现事件的产生和捕获。

现在让我们为之前已经定义好的Book和Reader实体添加一些业务逻辑。根据上面的分析我们不难得出,应用程序的用户可以创建读者和图书,不仅如此,读者可以从图书馆借书,也可以向图书馆还书。而对于书而言呢?它会被从图书馆借出,也会被归还回图书馆。于是,我们可以向Reader实体添加BorrowBook和ReturnBook方法,向Book实体添加LendTo和ReturnBy方法。

  1. 回到TinyLibrary.Domain项目,右键单击References节点,选择Add Reference…菜单,这将打开Add Reference对话框
  2. Projects选项卡下,选择TinyLibrary.Events,然后单击OK按钮
  3. Book类添加下面的代码
       1: public void LendTo(Reader reader)
       2: {
       3:     this.RaiseEvent<BookLentEvent>(new BookLentEvent
       4:     {
       5:         ReaderId = reader.Id,
       6:         LentDate = DateTime.Now
       7:     });
       8: }
       9: 
      10: public void ReturnBy(Reader reader)
      11: {
      12:     this.RaiseEvent<BookGetReturnedEvent>(new BookGetReturnedEvent
      13:     {
      14:         ReaderId = reader.Id,
      15:         ReturnedDate = DateTime.Now
      16:     });
      17: }
      18: 
      19: [Handles(typeof(BookLentEvent))]
      20: private void OnBookLent(BookLentEvent evnt)
      21: {
      22:     this.Lent = true;
      23: }
      24: 
      25: [Handles(typeof(BookGetReturnedEvent))]
      26: private void OnBookReturnedBack(BookGetReturnedEvent evnt)
      27: {
      28:     this.Lent = false;
      29: }
  4. Reader类添加下面的代码
       1: public void BorrowBook(Book book)
       2: {
       3:     if (book.Lent)
       4:         throw new DomainException();
       5:     book.LendTo(this);
       6:     this.RaiseEvent<BookBorrowedEvent>(new BookBorrowedEvent
       7:     {
       8:         BookId = book.Id,
       9:         ReaderId = Id,
      10:         ReaderLoginName = LoginName,
      11:         ReaderName = Name,
      12:         BookISBN = book.ISBN,
      13:         BookPages = book.Pages,
      14:         BookPubDate = book.PubDate,
      15:         BookPublisher = book.Publisher,
      16:         BookTitle = book.Title,
      17:         BookLent = book.Lent,
      18:         RegistrationDate = DateTime.Now,
      19:         DueDate = DateTime.Now.AddMonths(2)
      20:     });
      21: }
      22: 
      23: public void ReturnBook(Book book)
      24: {
      25:     if (!book.Lent)
      26:         throw new DomainException();
      27:     if (!HasBorrowed(book))
      28:         throw new DomainException();
      29:     book.ReturnBy(this);
      30:     this.RaiseEvent<BookReturnedEvent>(new BookReturnedEvent
      31:     {
      32:         ReaderId = this.Id,
      33:         BookId = book.Id
      34:     });
      35: }
      36: 
      37: private bool HasBorrowed(Book book)
      38: {
      39:     return Books.Any(p => p.Id.Equals(book.Id));
      40: }
      41: 
      42: [Handles(typeof(BookBorrowedEvent))]
      43: private void OnBookBorrowed(BookBorrowedEvent evnt)
      44: {
      45:     this.Books.Add(Book.Create(evnt.BookId, 
      46:         evnt.BookTitle, 
      47:         evnt.BookPublisher, 
      48:         evnt.BookPubDate, 
      49:         evnt.BookISBN, 
      50:         evnt.BookPages, 
      51:         evnt.BookLent));
      52: 
      53: }
      54: 
      55: [Handles(typeof(BookReturnedEvent))]
      56: private void OnBookReturned(BookReturnedEvent evnt)
      57: {
      58:     this.Books.RemoveAll(p => p.Id.Equals(evnt.BookId));
      59: }

在上面定义的业务方法里,用到了RaiseEvent泛型方法。这个方法会通知Apworks框架已经产生了一个新的事件。领域对象有其自己的方法来处理这些新产生的事件,这些处理方法都被冠以Apworks.Events.HandlesAttribute特性。在这些事件处理方法中,领域对象的状态得到了改变。

目前看来,在这些领域对象中到处充斥着被标以Apworks.Events.HandlesAttribute特性的事件处理方法,当前的Apworks框架仅支持这样的基于反射技术的“事件-处理器”映射。在Apworks的后续版本中,会提供更多的映射策略以供开发人员选择。

总之,领域对象通过方法来表述它们需要处理的业务逻辑,这些方法又通过领域事件来更新领域对象的状态。这就使得CQRS架构能够通过领域事件来跟踪对象状态发生变化的情况。每当RaiseEvent泛型方法被调用时,SourcedAggregateRoot基类会将事件记录到本地的存储中,然后调用相应的标有Apworks.Events.HandlesAttribute特性的事件处理函数。当领域仓储保存聚合时,这些保存在本地的事件将被发布到事件总线,以便于订阅者能够对事件做后续处理。

在Apworks框架中,领域事件是由领域仓储推送到事件总线的【注意:这只是在Apworks的Alpha版本中会这么做,由于这么做会导致TPC(或者称2PC,二次提交)问题,所以在后续版本中会解决这个问题】。而在另一方面,系统会根据事件发布策略,将事件总线中的事件发布到与之相应的事件处理器上。这个过程可以通过异步实现以提高系统的响应度。当事件处理器获得了事件之后,它们会与基础结构层服务打交道以实现更多的功能(比如邮件推送、数据同步等等)。在Apworks系统启动的时候,应用程序就会根据配置文件对事件处理器进行注册。在后续章节中,我将简单介绍Apworks Alpha版本中的配置文件。

就TinyLibraryCQRS而言,我们尽创建一些用于同步“查询数据库”的事件处理器。查询数据库是一个位于基础结构层的存储系统,它用来保存与“查询”相关的数据,比如用于生成报表的数据,或者是用于显示在屏幕上的数据等。在后面的讨论中,你将了解到,TinyLibraryCQRS的查询数据库完全是为了迎合客户端的显示需要而设计的:每张数据表对应的是一个表示层的视图模型(View Model)。你会觉得这种设计将造成大量的数据冗余,没错,的确数据冗余很大,但这样做减少了JOIN操作甚至是ORM的引入,它有助于性能的提高【注意:在实际项目中,还是应该根据具体情况来确定查询数据库的设计方案】。

在创建事件处理器之前,我们先讨论一下“查询对象”的概念。“查询对象”可以看成是一种DTO(Data Transfer Object),它是查询数据库中数据的一种表现形式,会来回在网络中传输并持久化到存储系统。在Apworks框架的应用中,“查询对象”需要实现Apworks.Queries.IQueryObject接口,并使用System.SerializableAttribute特性来标识这些对象。不仅如此,如果你使用Apworks.Queries.Storage.SqlQueryObjectStorage来访问你的查询数据库(实际上,如果你是在使用关系型数据库系统),那么你就需要事先准备一个XML映射文件。这个XML映射文件告知系统,查询对象将被映射到哪张数据表,以及查询对象的属性将被映射到数据表中的哪些字段。这个XML文件的Schema结构非常简单,在后续章节中会对其进行讨论。

现在,让我们创建几个查询对象。

  1. Solution Explorer下,右键单击TinyLibraryCQRS解决方案,然后单击Add | New Project…菜单,这将打开Add New Project对话框
  2. Installed Templates选项卡下,选择Visual C# | Windows,然后选择Class Library,确保所选的.NET版本是.NET Framework 4,然后在Name文本框中,输入TinyLibrary.QueryObjects,然后单击OK按钮
  3. 右键单击TinyLibrary.QueryObjects项目的References节点,单击Add Reference…菜单,这将打开Add Reference对话框
  4. .NET选项卡下,选择Apworks然后单击OK按钮
  5. TinyLibrary.QueryObjects项目添加如下代码
    using System;
    using System.Runtime.Serialization;
    using Apworks.Queries;
     
        
    namespace TinyLibrary.QueryObjects
    {
        [Serializable]
        [DataContract]
        public class BookObject : IQueryObject
        {
            [DataMember]
            public long Id { get; set; }
     
        
            [DataMember]
            public string Title { get; set; }
     
        
            [DataMember]
            public string Publisher { get; set; }
     
        
            [DataMember]
            public DateTime PubDate { get; set; }
     
        
            [DataMember]
            public string ISBN { get; set; }
     
        
            [DataMember]
            public int Pages { get; set; }
     
        
            [DataMember]
            public bool Lent { get; set; }
     
        
            [DataMember]
     
        
            [DataMember]
            public string LendTo { get; set; }
        }
     
        
        [Serializable]
        [DataContract]
        public class ReaderObject : IQueryObject
        {
            [DataMember]
            public long Id { get; set; }
     
        
            [DataMember]
            public string LoginName { get; set; }
     
        
            [DataMember]
            public string Name { get; set; }
     
        
            [DataMember]
            public long AggregateRootId { get; set; }
        }
     
        
        [Serializable]
        [DataContract]
        public class RegistrationObject : IQueryObject
        {
            [DataMember]
            public long Id { get; set; }
     
        
            [DataMember]
            public long BookAggregateRootId { get; set; }
     
        
            [DataMember]
            public long ReaderAggregateRootId { get; set; }
     
        
            [DataMember]
            public string ReaderName { get; set; }
     
        
            [DataMember]
            public string ReaderLoginName { get; set; }
     
        
            [DataMember]
            public string BookTitle { get; set; }
     
        
            [DataMember]
            public string BookPublisher { get; set; }
     
        
            [DataMember]
            public DateTime BookPubDate { get; set; }
     
        
            [DataMember]
            public string BookISBN { get; set; }
     
        
            [DataMember]
            public int BookPages { get; set; }
     
        
            [DataMember]
            public DateTime RegistrationDate { get; set; }
     
        
            [DataMember]
            public DateTime DueDate { get; set; }
     
        
            [DataMember]
            public DateTime ReturnedDate { get; set; }
     
        
            [DataMember]
            public bool Returned { get; set; }
        }
    }

现在可以开始创建事件处理器了。事件处理器会使用捕获的事件来创建查询对象,然后将其保存到查询数据库中。

  1. Solution Explorer中,右键单击TinyLibraryCQRS解决方案,然后单击Add | New Project…菜单,这将打开Add New Project对话框
  2. Installed Templates选项卡下,选择Visual C# | Windows,然后选择Class Library,确保所选的.NET版本是.NET Framework 4,在Name文本框中,输入TinyLibrary.EventHandlers,然后单击OK按钮
  3. 右键单击TinyLibrary.EventHandlers项目的References节点,然后单击Add Reference…菜单,这将打开Add Reference对话框
  4. .NET选项卡下,选择Apworks然后单击OK按钮
  5. 右键单击TinyLibrary.EventHandlers项目的References节点,然后单击Add Reference…菜单,这将打开Add Reference对话框
  6. Projects选项卡下,选择TinyLibrary.EventsTinyLibrary.QueryObjects,然后单击OK按钮
  7. TinyLibrary.EventHandlers项目加入如下代码:
    1. using TinyLibrary.QueryObjects;
       
            
      namespace TinyLibrary.EventHandlers
      {
          public class BookBorrowedEventHandler : Apworks.Events.EventHandler<BookBorrowedEvent>
          {
              public override bool Handle(BookBorrowedEvent target)
              {
                  using (IQueryObjectStorage storage = this.GetQueryObjectStorage())
                  {
                      RegistrationObject registrationObject = new RegistrationObject
                      {
                          BookAggregateRootId = target.BookId,
                          BookISBN = target.BookISBN,
                          BookPages = target.BookPages,
                          BookPubDate = target.BookPubDate,
                          BookPublisher = target.BookPublisher,
                          BookTitle = target.BookTitle,
                          ReaderAggregateRootId = target.ReaderId,
                          ReaderLoginName = target.ReaderLoginName,
                          ReaderName = target.ReaderName,
                          DueDate = target.DueDate,
                          RegistrationDate = target.RegistrationDate,
                          Returned = false,
                          ReturnedDate = (DateTime)SqlDateTime.MinValue
                      };
                      storage.Insert<RegistrationObject>(new Apworks.Storage.PropertyBag(registrationObject));
       
            
                      PropertyBag pbUpdateFields = new PropertyBag();
                      pbUpdateFields.Add("Lent", true);
                      pbUpdateFields.Add("LendTo", target.ReaderName);
       
            
                      PropertyBag pbCriteria = new PropertyBag();
                      pbCriteria.Add("AggregateRootId", target.BookId);
       
            
                      storage.Update<BookObject>(pbUpdateFields, pbCriteria);
                  }
                  return true;
              }
          }
       
            
          public class BookCreatedEventHandler : Apworks.Events.EventHandler<BookCreatedEvent>
          {
              public override bool Handle(BookCreatedEvent target)
              {
                  using (IQueryObjectStorage storage = this.GetQueryObjectStorage())
                  {
                      BookObject bookData = new BookObject
                      {
                          AggregateRootId = target.AggregateRootId,
                          ISBN = target.ISBN,
                          Lent = target.Lent,
                          Pages = target.Pages,
                          PubDate = target.PubDate,
                          Publisher = target.Publisher,
                          Title = target.Title,
                          LendTo = string.Empty
                      };
                      storage.Insert<BookObject>(new Apworks.Storage.PropertyBag(bookData));
                  }
                  return true;
              }
          }
       
            
          public class BookReturnedEventHandler : Apworks.Events.EventHandler<BookReturnedEvent>
          {
              public override bool Handle(BookReturnedEvent target)
              {
                  using (IQueryObjectStorage queryStorage = this.GetQueryObjectStorage())
                  {
                      PropertyBag pbCriteria = new PropertyBag();
                      pbCriteria.Add("BookAggregateRootId", target.BookId);
                      pbCriteria.Add("ReaderAggregateRootId", target.ReaderId);
                      PropertyBag pbUpdateFields = new PropertyBag();
                      pbUpdateFields.Add("ReturnedDate", DateTime.Now);
                      pbUpdateFields.Add("Returned", true);
                      queryStorage.Update<RegistrationObject>(pbUpdateFields, pbCriteria);
       
            
                      PropertyBag pbUpdateFieldsBooks = new PropertyBag();
                      pbUpdateFieldsBooks.Add("Lent", false);
                      pbUpdateFieldsBooks.Add("LendTo", string.Empty);
       
            
                      PropertyBag pbCriteriaBooks = new PropertyBag();
                      pbCriteriaBooks.Add("AggregateRootId", target.BookId);
       
            
                      queryStorage.Update<BookObject>(pbUpdateFieldsBooks, pbCriteriaBooks);
                  }
                  return true;
              }
          }
       
            
          public class ReaderCreatedEventHandler : Apworks.Events.EventHandler<ReaderCreatedEvent>
          {
              public override bool Handle(ReaderCreatedEvent target)
              {
                  using (IQueryObjectStorage storage = this.GetQueryObjectStorage())
                  {
                      ReaderObject readerObject = new ReaderObject 
                      { AggregateRootId = target.AggregateRootId, LoginName = target.LoginName, Name = target.Name };
                      storage.Insert<ReaderObject>(new PropertyBag(readerObject));
                  }
                  return true;
              }
          }
      }

    事件处理器需要继承Apworks.Events.EventHandler类,并实现Handle方法。在Handle方法中,事件处理器将首先通过GetQueryObjectStorage方法获得查询数据库存储对象,然后通过存储对象来保存查询对象。

    至此,我们已经创建了聚合、快照、查询对象、领域事件以及事件处理器。在下一步操作中,我们将会创建一些“业务外观对象”来协调业务处理过程,以便更上层的应用组件能够方便地使用我们的领域模型。在CQRS架构中,这种“业务外观对象”就是命令与命令处理器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值