求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
使用Apworks开发基于CQRS架构的应用程序
 

作者:陈晴阳 ,发布于2013-2-18,来源:博客园

 

目录:

使用Apworks开发基于CQRS架构的应用程序(一):前言

使用Apworks开发基于CQRS架构的应用程序(二):创建领域模型项目

使用Apworks开发基于CQRS架构的应用程序(三):创建快照

使用Apworks开发基于CQRS架构的应用程序(四):领域事件

使用Apworks开发基于CQRS架构的应用程序(五):命令

使用Apworks开发基于CQRS架构的应用程序(六):创建.NET WCF服务

使用Apworks开发基于CQRS架构的应用程序(七):配置数据库

使用Apworks开发基于CQRS架构的应用程序(八):应用程序的配置与编译

使用Apworks开发基于CQRS架构的应用程序(九):运行应用程序

使用Apworks开发基于CQRS架构的应用程序(一):前言

在Apworks框架发布Alpha版本的时候,我已经针对其开发案例:Tiny Library CQRS写了Walkthrough文档,地址是:http://apworks.org/custom/documents/wlkthr_BuildAppsUsingApworks/。为了走国际化道路,这篇文章是全英文的,社区里有不少网友表示难以理解。趁着这几天不算很忙,我抽空将其又翻译回中文,并加入更多的批准内容,供爱好DDD、CQRS和架构设计的朋友阅读参考,也希望大家能够积极参加讨论。

简介

Apworks是一套应用程序开发框架,软件架构师和开发人员可以使用这套开发框架开发出面向领域(Domain-Centric)并基于CQRS架构风格的应用程序。在本系列文章中,我将向大家介绍,如何使用Apworks开发一套面向领域的、松耦合的分布式应用系统。

本系列文章所使用的案例,就是之前我介绍的Tiny Library CQRS应用程序。之前也有一个系列文章是介绍这个项目的,不过那些文章都是介绍Tiny Library CQRS的一些功能要点和实现思想,并不是一套完整的How To演练文档。到写这篇文章为止,那个系列文章还没有写完,不过在我完成这个系列后,我会继续将其更新完善。

本系列文章将向读者朋友介绍,如何白手起家,使用Apworks开发应用程序。

业务场景

之前了解过Tiny Library CQRS项目的朋友对业务场景应该会非常熟悉,可直接跳过此段。在接下来的几章中,我们将开发一个应用程序,通过这个应用程序,用户可以对“读者”和“图书”进行管理。“读者”可以从图书馆“借书”,在看完后,可以“归还”给图书馆。系统用户可以创建“读者”和“图书”的信息,并且可以查询“读者”的“借书”、“还书”情况以及“图书”的借出历史和归还状态。

先决条件

为了保证你能够顺利地按照本系列文章的介绍,逐步创建能够运行的应用程序,你的系统必须符合下面的先决条件:

  • Microsoft .NET Framework 3.5 SP1
  • Microsoft Visual Studio 2010
  • Microsoft Patterns & Practices Enterprise Library 5.0 (April 2010)
  • Microsoft SQL Server 2005 (Express) or above
  • Microsoft ASP.NET MVC
  • Apworks Development Framework

在Visual Studio中创建解决方案

现在,我们开始在Visual Studio 2010中创建解决方案。为了描述方便,我们给解决方案取个名字,称为“TinyLibraryCQRS”,所有项目的命名空间都会基于这个名称。为了能够更好地使用Apworks框架来开发应用程序,通常我们的解决方案会包含以下项目:

  • TinyLibrary.Domain(C# Library)- 该项目包含了领域模型
  • TinyLibrary.Events (C# Library)- 该项目包含了对所有领域事件的定义
  • TinyLibrary.EventHandlers(C# Library)- 该项目包含了所有领域事件的处理器定义
  • TinyLibrary.Commands(C# Library)- 该项目包含了所有客户端命令的定义
  • TinyLibrary.CommandHandlers(C# Library)- 该项目包含了所有处理客户端命令的命令处理器定义
  • TinyLibrary.QueryObjects(C# Library) - 该项目为整个应用程序提供了查询机制和查询对象
  • TinyLibrary.Services(C# WCF Service Application)- 一个C#的WCF Service项目,用来向客户端提供应用程序服务接口
  • TinyLibrary.WebApp(C# ASP.NET MVC应用程序)- 一个采用ASP.NET MVC开发的客户端程序

现在,让我们在Visual Studio 2010中创建TinyLibraryCQRS的解决方案:

  1. 打开Microsoft Visual Studio 2010
  2. 单击 File | New | Project 菜单,这将打开 New Project 对话框
  3. 在 Installed Templates 选项卡下,选择 Other Project Types | Visual Studio Solutions,然后选择 Blank Solution
  4. 确保在对话框上的Framework版本选择区内,选择了.NET Framework 4.0
  5. 在 Name 文本框中,输入 TinyLibraryCQRS 然后单击 OK 按钮

至此,Visual Studio的Solution Explorer中只显示了一个节点,就是我们刚刚新建的TinyLibraryCQRS解决方案,在后续的文章中,我们将了解到项目的整个开发过程。

额外说明

  1. 在本系列文章的操作步骤描述中,界面元素的名称还是英文的,这是因为工作需要,我的开发环境是英文的,为了不至于产生歧义,我没有将这些界面元素的名称翻译成中文写在这里,也没来得及搭建一个中文环境去比对,所以目前也只能把英文的名称原封不动地写下来,我想应该不会给读者造成太大的阅读障碍吧
  2. Apworks目前发布的是Alpha版本,下一个版本也正在开发中,所以文章的内容目前只适应于Alpha版本

使用Apworks开发基于CQRS架构的应用程序(二):创建领域模型项目

领域模型是应用程序的核心,它包括了领域对象、状态、方法、事件、业务逻辑以及对象之间的关系。现在,我们来为Tiny Library CQRS创建一个领域模型项目。

1.在 Solution Explorer 下,右键单击TinyLibraryCQRS项目,单击 Add | New Project… 菜单,这将打开 Add New Project 对话框

2.在 Installed Templates 选项卡下,选择 Visual C# | Windows,然后选择 Class Library ,确保所选的Framework版本是.NET Framework 4,在 Name 文本框中,输入TinyLibrary.Domain,然后单击OK按钮

3.在 Solution Explorer 下,右键单击 TinyLibrary.Domain 项目的 References 节点,单击 Add Reference… 菜单,这将打开 Add Reference 对话框

4.在 .NET 选项卡下,选择 Apworks 然后单击 OK 按钮

5.在 TinyLibrary.Domain 项目下,创建一个 Book 类,由于它是聚合根,因此使其继承于 SourcedAggregateRoot 类

    using System;    
    using Apworks;    
    using Apworks.Snapshots;    
    namespace TinyLibrary.Domain    
    {    
       public class Book : SourcedAggregateRoot    
       {   
            public string Title { get; private set; }   
            public string Publisher { get; private set; }   
            public DateTime PubDate { get; private set; }   
            public string ISBN { get; private set; }   
            public int Pages { get; private set; }   
            public bool Lent { get; private set; }   
        
            public Book() : base() {  }   
            public Book(long id) : base(id) {  }   
        
            protected override void DoBuildFromSnapshot(ISnapshot snapshot)   
            {   
                throw new NotImplementedException();   
            }   
            protected override ISnapshot DoCreateSnapshot()   
            {   
                throw new NotImplementedException();   
            }   
        }   
    }

6.同理,在 TinyLibrary.Domain 项目下,创建一个 Reader 类

   using System;    
   using System.Collections.Generic;    
   using Apworks;    
   using Apworks.Snapshots;    
        6: namespace TinyLibrary.Domain    
   {    
       public class Reader : SourcedAggregateRoot    
       {   
          public string LoginName { get; private set; }   
          public string Name { get; private set; }  
          public List<Book> Books { get; private set; }   
          public Reader() : base() { Books = new List<Book>(); }   
          public Reader(long id) : base(id) { Books = new List<Book>(); }   
          protected override void DoBuildFromSnapshot(ISnapshot snapshot)   
          {   
               throw new NotImplementedException();   
          }   
          protected override ISnapshot DoCreateSnapshot()   
          {   
               throw new NotImplementedException();   
          }   
      }   
  }

从上面两段代码可以看出,有两个protected的方法我们还没有实现,这两个方法是定义在SourcedAggregateRoot基类中的,用来提供与“快照”相关的功能。“快照”是CQRS架构中非常重要的一部分,在下一章节中,我们将了解到,如何为我们的领域模型添加快照功能。

使用Apworks开发基于CQRS架构的应用程序(三):创建快照

由于事件溯源(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,在这两个聚合根里,有两个未实现的方法。现在我们来实现这两个方法以便让我们的应用程序支持快照功能。

1.右键单击TinyLibrary.Domain项目,单击 Add | New Folder 菜单,将 New Folder 重命名为 Snapshots

2.用下面的方法创建一个BookSnapshot类

    using System;    
    using Apworks.Snapshots;    
         
      namespace TinyLibrary.Domain.Snapshots    
        [Serializable]    
        public class BookSnapshot : Snapshot    
        {    
            public string Title { get; set; }   
           public string Publisher { get; set; }   
           public DateTime PubDate { get; set; }   
           public string ISBN { get; set; }   
           public int Pages { get; set; }   
           public bool Lent { get; set; }   
       }   
   }

注意我们使用System.SerializableAttribute特性标注了上面的类,因为我们需要将快照数据保存到存储系统中。你也会注意到,这个类的结构跟Book实体的结构很像,这是因为快照本身的作用就是反映并存储与之对应的实体在某个时间点的状态。

3.同理,添加ReaderSnapshot类

   using System;    
    using System.Collections.Generic;    
    using Apworks.Snapshots;    
    namespace TinyLibrary.Domain.Snapshots   
    {    
        [Serializable]    
        public class ReaderSnapshot : Snapshot    
        {   
           public string LoginName { get; set; }   
          public string Name { get; set; }   
       
           public List<booksnapshot> Books { get; set; }   
      }   
   }

4.回到Book类,用下面的方式实现DoBuildFromSnapshot和DoCreateSnapshot方法

   protected override void DoBuildFromSnapshot(ISnapshot snapshot)    
   {    
        BookSnapshot bs = (BookSnapshot)snapshot;    
        this.Title = bs.Title;    
        this.Publisher = bs.Publisher;    
        this.PubDate = bs.PubDate;    
        this.ISBN = bs.ISBN;    
        this.Pages = bs.Pages;    
        this.Lent = bs.Lent;   
    }   
        
    protected override ISnapshot DoCreateSnapshot()   
    {   
        BookSnapshot bs = new BookSnapshot();   
        bs.ISBN = this.ISBN;   
        bs.Lent = this.Lent;   
        bs.Pages = this.Pages;   
        bs.PubDate = this.PubDate;   
        bs.Publisher = this.Publisher;   
        bs.Title = this.Title;   
        return bs;   
    }

5.回到Reader类,用下面的方式实现DoBuildFromSnapshot和DoCreateSnapshot方法

   protected override void DoBuildFromSnapshot(ISnapshot snapshot)    
    {    
        ReaderSnapshot rs = (ReaderSnapshot)snapshot;    
        this.Books.Clear();    
        foreach (var bk in rs.Books)    
        {    
            this.Books.Add(Book.Create(bk.AggregateRootId,     
                bk.Title,     
                bk.Publisher,    
               bk.PubDate,    
               bk.ISBN,    
               bk.Pages,    
               bk.Lent));   
       }   
       this.LoginName = rs.LoginName;   
       this.Name = rs.Name;   
   }   
        protected override ISnapshot DoCreateSnapshot()   
   {   
       ReaderSnapshot rs = new ReaderSnapshot();   
       rs.Books = new List<BookSnapshot>();   
       foreach (var bk in this.Books)   
       {   
           rs.Books.Add((BookSnapshot)bk.CreateSnapshot());   
       }   
       rs.LoginName = this.LoginName;   
       rs.Name = this.Name;   
       return rs;   
   }

6.在完成了上面的所有步骤后,我们的Reader和Book类定义如下:

   using System;    
   using Apworks;    
   using Apworks.Snapshots;    
	namespace TinyLibrary.Domain    
	{    
	public class Book : SourcedAggregateRoot    
	{   
	public string Title { get; private set; }   
	public string Publisher { get; private set; }   
	public DateTime PubDate { get; private set; }   
	public string ISBN { get; private set; }   
	public int Pages { get; private set; }   
	public bool Lent { get; private set; }   
	public Book() : base() {  }   
	public Book(long id) : base(id) {  }   
	protected override void DoBuildFromSnapshot(ISnapshot snapshot)   
	{   
	    BookSnapshot bs = (BookSnapshot)snapshot;   
	    this.Title = bs.Title;   
		this.Publisher = bs.Publisher;   
		this.PubDate = bs.PubDate;   
		this.ISBN = bs.ISBN;   
		this.Pages = bs.Pages;   
		this.Lent = bs.Lent;   
	}   
	protected override ISnapshot DoCreateSnapshot()   
	{   
	    BookSnapshot bs = new BookSnapshot();   
		bs.ISBN = this.ISBN;   
		bs.Lent = this.Lent;   
		bs.Pages = this.Pages;   
		bs.PubDate = this.PubDate;   
		bs.Publisher = this.Publisher;   
		bs.Title = this.Title;   
		return bs;   
	}   
   }   
   public class Reader : SourcedAggregateRoot   
   {   
       public string LoginName { get; private set; }   
	   public string Name { get; private set; }   
	   public List<Book> Books { get; private set; }   
	   public Reader() : base() { Books = new List<Book>(); }   
	   public Reader(long id) : base(id) { Books = new List<Book>(); }   
	   protected override void DoBuildFromSnapshot(ISnapshot snapshot)   
	   { 
	       ReaderSnapshot rs = (ReaderSnapshot)snapshot;   
		   this.Books.Clear();   
		   foreach (var bk in rs.Books)   
		   {   
		       this.Books.Add(Book.Create(bk.AggregateRootId,   
			    bk.Title,    
				bk.Publisher,   
			    bk.PubDate,    
				bk.ISBN,    
				bk.Pages,    
				bk.Lent));  
			}  
			this.LoginName = rs.LoginName;   
			this.Name = rs.Name;   
		 }   
		 protected override ISnapshot DoCreateSnapshot()   
		 {   
		     ReaderSnapshot rs = new ReaderSnapshot();   
			 rs.Books = new List<BookSnapshot>();   
			 foreach (var bk in this.Books)   
			 {  
			     rs.Books.Add((BookSnapshot)bk.CreateSnapshot());   
			 }   
			 rs.LoginName = this.LoginName;   
			 rs.Name = this.Name;   
			 return rs;   
		}   
	 }   
   }

在下一篇文章中,我们将引入领域事件,并逐步向对象中加入领域逻辑。

使用Apworks开发基于CQRS架构的应用程序(四):领域事件

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

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

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

1.在 Solution Explorer 的 TinyLibraryCQRS 解决方案上,右键单击并选择 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 项目添加下面的类代码,以实现领域事件

    using System;    
    using Apworks.Events;    
         
    namespace TinyLibrary.Events    
	{    
	    [Serializable]    
		public class BookBorrowedEvent : DomainEvent    
		{   
		   public long BookId { get; set; }   
		   public long ReaderId { get; set; }   
		   public string ReaderName { get; set; }   
		   public string ReaderLoginName { get; set; }   
		   public string BookTitle { get; set; }   
		   public string BookPublisher { get; set; }   
		   public DateTime BookPubDate { get; set; }   
		   public string BookISBN { get; set; }   
		   public int BookPages { get; set; }   
		   public bool BookLent { get; set; }   
		   public DateTime RegistrationDate { get; set; }   
		   public DateTime DueDate { get; set; }   
		}   
		[Serializable]   
		public class BookCreatedEvent : DomainEvent   
		{   
	        public string Title { get; set; }   
			public string Publisher { get; set; }   
			public DateTime PubDate { get; set; }   
			public string ISBN { get; set; }   
			public int Pages { get; set; }   
			public bool Lent { get; set; }   
		}   
		[Serializable]   
		public class BookGetReturnedEvent : DomainEvent   
		{   
		    public long ReaderId { get; set; }  
			public DateTime ReturnedDate { get; set; }  
		}   
		[Serializable]   
		public class BookLentEvent : DomainEvent   
		{   
		    public long ReaderId { get; set; }   
			public DateTime LentDate { get; set; }   
		}   
		[Serializable]  
		public class BookReturnedEvent : DomainEvent   
		{   
	        public long ReaderId { get; set; }   
			public long BookId { get; set; }   
		}   
		[Serializable]   
		public class ReaderCreatedEvent : DomainEvent   
		{   
		     public string LoginName { get; set; }   
			 public string Name { get; set; }   
		 }   
	}

从上面的代码我们可以看到,所有的领域事件都继承于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类添加下面的代码

   public void LendTo(Reader reader)    
   {    
      this.RaiseEvent<BookLentEvent>(new BookLentEvent    
	  {   
		   ReaderId = reader.Id,    
		   LentDate = DateTime.Now    
	   });    
    }    
	public void ReturnBy(Reader reader)  
	{   
	   this.RaiseEvent<BookGetReturnedEvent>(new BookGetReturnedEvent   
	   {   
	       ReaderId = reader.Id,   
		   ReturnedDate = DateTime.Now   
		});  
	}   
	 [Handles(typeof(BookLentEvent))]   
	 private void OnBookLent(BookLentEvent evnt)  
	 {   
	    this.Lent = true;   
	 }   
	 [Handles(typeof(BookGetReturnedEvent))]   
	  private void OnBookReturnedBack(BookGetReturnedEvent evnt)   
	  {  
	      this.Lent = false;   
	  }

4.向Reader类添加下面的代码

   public void BorrowBook(Book book) 
   {   
	   if (book.Lent)   
	   throw new DomainException();    
	   book.LendTo(this);    
	   this.RaiseEvent<BookBorrowedEvent>(new BookBorrowedEvent    
	   {    
	       BookId = book.Id,    
		   ReaderId = Id,   
		   ReaderLoginName = LoginName,   
		   ReaderName = Name,   
		   BookISBN = book.ISBN,   
		   BookPages = book.Pages,   
		   BookPubDate = book.PubDate,   
		   BookPublisher = book.Publisher,   
		   BookTitle = book.Title,   
		   BookLent = book.Lent,   
		   RegistrationDate = DateTime.Now,   
		   DueDate = DateTime.Now.AddMonths(2) });   
		}  
		public void ReturnBook(Book book)   
		{   
		    if (!book.Lent)   
			throw new DomainException();  
			if (!HasBorrowed(book))   
			throw new DomainException();   
			book.ReturnBy(this);   
			this.RaiseEvent<BookReturnedEvent>(new BookReturnedEvent   
			{   
		        ReaderId = this.Id,   
				BookId = book.Id   
			});   
		}   
		private bool HasBorrowed(Book book)   
		{   
	       return Books.Any(p => p.Id.Equals(book.Id));   
		 }   
		 [Handles(typeof(BookBorrowedEvent))]   
		  private void OnBookBorrowed(BookBorrowedEvent evnt)   
		  {   
		     this.Books.Add(Book.Create(evnt.BookId,    
			 evnt.BookTitle,    
			 evnt.BookPublisher,    
			 evnt.BookPubDate,    
			 evnt.BookISBN,    
			 evnt.BookPages,    
			 evnt.BookLent));   
		 }   
		 [Handles(typeof(BookReturnedEvent))]   
		 private void OnBookReturned(BookReturnedEvent evnt)  
		 {   
		    this.Books.RemoveAll(p => p.Id.Equals(evnt.BookId));   
		 }

在上面定义的业务方法里,用到了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; }
      }
   }

在上面定义的业务方法里,用到了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.Events 和 TinyLibrary.QueryObjects,然后单击OK按钮

7.向 TinyLibrary.EventHandlers 项目加入如下代码:

  using System; 
  using System.Data.SqlTypes; 
  using Apworks.Queries.Storage; 
  using Apworks.Storage; 
  using TinyLibrary.Events; 
  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架构中,这种“业务外观对象”就是命令与命令处理器。

使用Apworks开发基于CQRS架构的应用程序(五):命令

客户端程序通过命令告知系统“应该做什么”。事实上,这是一种单向的交互过程,客户端程序仅仅向领域模型发送命令请求,它们并不会通过领域模型来查询某些数据信息。在CQRS架构的应用程序中,“查询”是另一部分的内容,这将在接下来的章节中单独讨论。当应用服务器端接收到来自客户端的命令请求后,就会将这些命令推送到命令总线。命令处理器会侦听命令总线,并相应地处理命令请求。现在,让我们在TinyLibraryCQRS解决方案中创建命令与命令解释器。

1.右键单击TinyLibraryCQRS解决方案,单击 Add | New Project… 菜单,这将打开Add New Project对话框

2.在Installed Templates选项卡下,选择Visual C# | Windows,然后选择Class Library,确保所选的.NET版本是.NET Framework 4,然后在Name文本框中,输入TinyLibrary.Commands,并单击OK按钮

3.在TinyLibrary.Commands项目上,右键单击References节点,单击Add Reference…菜单,这将打开Add Reference对话框

4.在.NET选项卡下,选择Apworks,然后单击OK按钮

5.添加下面的代码:

    using System;
    using Apworks.Commands;   
        
    namespace TinyLibrary.Commands   
    {   
        [Serializable]   
        public class BorrowBookCommand : Command   
        {   
            public long ReaderId { get; set; }  
           public long BookId { get; set; }  
           public BorrowBookCommand(long readerId, long bookId)  
           {  
               this.ReaderId = readerId;  
               this.BookId = bookId;  
           }  
       }  
       [Serializable]  
       public class CreateBookCommand : Command  
      {  
           public string Title { get; private set; }  
           public string Publisher { get; private set; }  
           public DateTime PubDate { get; private set; }  
           public string ISBN { get; private set; }  
           public int Pages { get; private set; }  
           public bool Lent { get; private set; }  
           public CreateBookCommand(string title,   
               string publisher,   
               DateTime pubDate,   
               string isbn, int pages, bool lent)  
           {  
               this.Title = title;  
               this.PubDate = pubDate;  
               this.Publisher = publisher;  
               this.ISBN = isbn;  
               this.Pages = pages;  
               this.Lent = lent;  
           }  
      
           public CreateBookCommand(long id,   
               string title,   
               string publisher,   
               DateTime pubDate,   
               string isbn, int pages, bool lent)  
               : base(id)  
           {  
               this.Title = title;  
               this.PubDate = pubDate;  
               this.Publisher = publisher;  
               this.ISBN = isbn;  
               this.Pages = pages;  
               this.Lent = lent;  
           }  
       }  
       [Serializable]  
       public class RegisterReaderCommand : Command  
       {  
           public string LoginName { get; private set; }  
           public string Name { get; private set; }  
           public RegisterReaderCommand(string loginName, string name)  
           {  
               this.LoginName = loginName;  
               this.Name = name;  
           }  
           public RegisterReaderCommand(long id, string loginName, string name):base(id)  
           {  
               this.LoginName = loginName;  
               this.Name = name;  
           }  
       }  
       [Serializable]  
       public class ReturnBookCommand : Command  
       {  
           public long ReaderId { get; set; }  
           public long BookId { get; set; }  
           public ReturnBookCommand(long readerId, long bookId)  
           {  
               this.ReaderId = readerId;  
               this.BookId = bookId;  
           }  
       }  
   }

看上去命令类与领域事件类的结构非常相似,的确如此,它们同样是继承于某个基类,同样都应用了System.SerializableAttribute特性。但事实上,命令与领域事件是两种完全不同的语义,虽然在某些情况下,两者结构相似,但这不是必然结果。

命令处理器用来处理已经定义的命令。当整个系统启动的时候,它会将Apworks配置文件里已经定义的命令处理器注册到系统中。有关这个配置文件的具体内容会在后续章节中描述。现在,我们创建一些命令处理器来处理上面已定义的命令。

1.在Solution Explorer中右键单击TinyLibraryCQRS解决方案,然后单击Add | New Project…菜单,这将打开Add New Project对话框

2.在Installed Templates选项卡下,选择Visual C# | Windows,然后选择Class Library,确保选择的.NET版本是.NET Framework 4,然后在Name文本框中,输入TinyLibrary.CommandHandlers,然后单击OK按钮

3.在Solution Explorer中,右键单击TinyLibrary.CommandHandlers项目的References节点,然后选择Add Reference…菜单,这将打开Add Reference对话框

4.在.NET选项卡下,选择Apworks然后单击OK按钮

5.在Solution Explorer中,右键单击TinyLibrary.CommandHandlers项目的References节点,然后选择Add Reference…菜单,这将打开Add Reference对话框

6.在Projects选项卡下,选择TinyLibrary.Commands和TinyLibrary.Domain项目,然后单击OK按钮

7.向该项目添加以下代码

    using Apworks.Commands;
    using Apworks.Repositories;   
    using TinyLibrary.Commands;   
    using TinyLibrary.Domain;   
    namespace TinyLibrary.CommandHandlers   
    {   
        public class BorrowBookCommandHandler : CommandHandler<BorrowBookCommand>   
       {  
           public override bool Handle(BorrowBookCommand command)  
           {  
               using (IDomainRepository repository = this.GetDomainRepository())  
               {  
                   Reader reader = repository.Get<Reader>(command.ReaderId);  
                   Book book = repository.Get<Book>(command.BookId);  
                   reader.BorrowBook(book);  
                   repository.Save(reader);  
                   repository.Save(book);  
               }  
               return true;  
           }  
       }  
      
       public class CreateBookCommandHandler : CommandHandler<CreateBookCommand>  
       {  
           public override bool Handle(CreateBookCommand command)  
           {  
               using (IDomainRepository repository = this.GetDomainRepository())  
               {  
                   Book book = Book.Create(command.Id,   
                       command.Title,   
                       command.Publisher,   
                       command.PubDate,   
                       command.ISBN,   
                       command.Pages,   
                       command.Lent);  
                   repository.Save<Book>(book);  
               }  
               return true;  
           }  
       }  
      
       public class RegisterReaderCommandHandler : CommandHandler<RegisterReaderCommand>  
       {  
           public override bool Handle(RegisterReaderCommand command)  
           {  
               using (IDomainRepository repository = this.GetDomainRepository())  
               {  
                   Reader reader = Reader.Create(command.Id, command.LoginName, command.Name);  
                   repository.Save(reader);  
               }  
               return true;  
           }  
       }  
       public class ReturnBookCommandHandler : CommandHandler<ReturnBookCommand>  
       {  
           public override bool Handle(ReturnBookCommand command)  
           {  
               using (IDomainRepository repository = this.GetDomainRepository())  
               {  
                   Reader reader = repository.Get<Reader>(command.ReaderId);  
                   Book book = repository.Get<Book>(command.BookId);  
                   reader.ReturnBook(book);  
                   repository.Save(reader);  
                   repository.Save(book);  
               }  
               return true;  
           }  
       }  
   }

从上面的代码我们可以看到,所有的命令处理器都从CommandHandler泛型抽象类继承而来,同时实现其Handle方法。通常,在Handle方法中,命令处理器会根据命令的具体内容,通过领域仓储来获得或更新聚合。要得到领域仓储的实例,可以使用定义在CommandHandler基类中的GetDomainRepository方法。建议在GetDomainRepository的调用端使用using子句以便及时释放资源。

就如我们上面讨论的那样,每当系统得到一个命令时,都将把它推送到命令总线中。当总线被提交的时候,已注册的命令处理器会处理与之对应的命令请求。现在,我们需要创建一个应用级的服务外观来接收客户端命令请求。在TinyLibraryCQRS解决方案中,这个应用服务外观被定义成了一个.NET WCF服务。下一讲将介绍这个.NET WCF服务的创建过程。

使用Apworks开发基于CQRS架构的应用程序(六):创建.NET WCF服务

在本节,我们将介绍.NET WCF服务的创建过程。

1.在Solution Explorer中,右键单击TinyLibraryCQRS,然后选择Add | New Project…菜单,这将打开Add New Project对话框

2.在Installed Templates 选项卡下,选择Visual C# | WCF,然后选择WCF Service Application,确保所选.NET版本为.NET Framework 4,在Name文本框中输入TinyLibrary.Services,然后单击OK按钮

3.右键单击TinyLibrary.Services的References节点,然后选择Add Reference…菜单,这将打开Add Reference对话框

4.在.NET选项卡下,选择Apworks,Apworks.Bus.DirectBus以及Apworks.ObjectContainers.Unity,然后单击OK按钮

5.右键单击TinyLibrary.Services的References节点,然后选择Add Reference…菜单,这将打开Add Reference对话框

6.在Projects选项卡下,选择TinyLibrary.Events,TinyLibrary.EventHandlers,TinyLibrary.Commands,TinyLibrary.CommandHandlers,TinyLibrary.Domain以及TinyLibrary.QueryObjects,然后单击OK按钮

7.将自动生成的Service1.svc文件删掉

8.右键单击TinyLibrary.Services项目,然后单击Add | New Item…菜单,这将打开Add New Item对话框

9.在Installed Templates选项卡下,选择Visual C# | Web,然后选择WCF Service,在Name文本框中,输入CommandService然后单击Add按钮

10.编辑CommandService.svc.cs文件,输入如下代码

    using System;
    using Apworks;    
	 using Apworks.Bus;    
	 using Apworks.Generators;    
	 using TinyLibrary.Commands;    
	 namespace TinyLibrary.Services    
	 {    
	     public class CommandService : ICommandService   
        {   
            private readonly ICommandBus bus = ObjectContainer.Instance.GetService<ICommandBus>();   
        
            public long CreateReader(string loginName, string name)   
            {   
                long id = (long)IdentityGenerator.Instance.Generate();   
                RegisterReaderCommand command = new RegisterReaderCommand(id, loginName, name);   
                bus.Publish(command);   
                bus.Commit();   
                return id;   
            }   
            public long CreateBook(string title, string publisher, DateTime pubDate, string isbn, int pages)   
            {   
                long id = (long)IdentityGenerator.Instance.Generate();   
                CreateBookCommand createBookCommand = new CreateBookCommand(id,    
                    title, publisher, pubDate, isbn, pages, false);   
                bus.Publish(createBookCommand);   
                bus.Commit();   
                return id;   
            }   
            public void BorrowBook(long readerId, long bookId)   
            {   
                BorrowBookCommand borrowBookCommand = new BorrowBookCommand(readerId, bookId);   
                bus.Publish(borrowBookCommand);   
                bus.Commit();   
            }   
            public void ReturnBook(long readerId, long bookId)   
            {   
                ReturnBookCommand returnBookCommand = new ReturnBookCommand(readerId, bookId);   
                bus.Publish(returnBookCommand);   
                bus.Commit();   
            }   
        }   
    }

11.用上面相同的方法创建一个QueryService.svc,然后编辑QueryService.svc.cs文件,输入如下代码

   using System;
   using System.Collections.Generic;    
	 using Apworks;    
	 using Apworks.Queries.Storage;    
	 using Apworks.Storage;    
	 using TinyLibrary.QueryObjects;    
	 namespace TinyLibrary.Services    
	 {   
       public class QueryService : IQueryService   
        {  
          private IQueryObjectStorage GetQueryStorage()   
            {   
                return ObjectContainer.Instance.GetService<IQueryObjectStorage>();   
            }   
            public BookObject GetBook(long id)   
            {   
                using (IQueryObjectStorage queryObjectStorage = this.GetQueryStorage())   
                {   
                    var pb = new PropertyBag();   
                    pb.Add("AggregateRootId", id);   
                    return queryObjectStorage.SelectFirstOnly<BookObject>(pb);   
                }   
            }   
        
            public ReaderObject GetReader(long id)   
            {   
                using (IQueryObjectStorage queryObjectStorage = this.GetQueryStorage())   
                {   
                    var pb = new PropertyBag();   
                    pb.Add("AggregateRootId", id);   
                    return queryObjectStorage.SelectFirstOnly<ReaderObject>(pb);   
                }   
            }   
            public ReaderObject GetReaderByLogin(string loginName)   
            {   
                using (IQueryObjectStorage queryObjectStorage = this.GetQueryStorage())   
                {   
                    var pb = new PropertyBag();   
                    pb.Add("LoginName", loginName);   
                    return queryObjectStorage.SelectFirstOnly<ReaderObject>(pb);   
                }   
            }   
        
            public IEnumerable<BookObject> GetAllBooks()   
            {   
                using (IQueryObjectStorage queryObjectStorage = this.GetQueryStorage())   
                {   
                    return queryObjectStorage.Select<BookObject>();   
                }   
            }   
        
            public IEnumerable<RegistrationObject> GetBookRegistrationHistory(long bookId)   
            {   
                using (IQueryObjectStorage qos = this.GetQueryStorage())   
                {   
                    PropertyBag pbCriteria = new PropertyBag();   
                    pbCriteria.Add("BookAggregateRootId", bookId);   
        
                    PropertyBag pbSort = new PropertyBag();   
                    pbSort.AddSort<DateTime>("RegistrationDate");   
                    return qos.Select<RegistrationObject>(pbCriteria, pbSort, Apworks.Storage.SortOrder.Descending); 
                }   
            }   
        
        
            public IEnumerable<RegistrationObject> GetReaderRegistrationHistory(long readerId)   
            {   
                using (IQueryObjectStorage qos = this.GetQueryStorage())   
                {   
                    PropertyBag pbCriteria = new PropertyBag();   
                    pbCriteria.Add("ReaderAggregateRootId", readerId);   
        
                    PropertyBag pbSort = new PropertyBag();   
                    pbSort.AddSort<DateTime>("RegistrationDate");   
                    return qos.Select<RegistrationObject>(pbCriteria, pbSort, Apworks.Storage.SortOrder.Descending);
                }   
            }   
        }   
    }

12.右键单击TinyLibrary.Services项目,然后选择Add | New Item…菜单,这将打开Add New Item对话框

13.在Installed Templates选项卡下,选择Visual C# | Web,然后选择Global Application Class,然后单击Add按钮

14.修改Global.asax.cs文件如下

    using System;
    using Apworks.Application;    
	 namespace TinyLibrary.Services    
	 {    
	     public class Global : System.Web.HttpApplication    
	     {    
	         private readonly IBootStrapper bootStrapper = BootStrapperFactory.Create();   
	     
            protected void Application_Start(object sender, EventArgs e)   
            {   
                bootStrapper.BootStrap();   
            }   
            // ... other methods omitted here.   
        }   
    }

Global.asax文件中定义了,当应用程序启动时,同时会启动Apworks系统。首先,Apworks.Application.BootStrapperFactory类创建了启动实例,然后使用BootStrap方法来初始化应用程序。这个过程是在Apworks的配置文件中定义的,将在后续文章中讨论。

<span class="image-wrap " style="position:relative; display:inline-block; background:url(image.jpg) no-repeat
 center center; width: 150px; height: 150px;">
    <img src="image.jpg" style="opacity: 0;">
</span>

使用Apworks开发基于CQRS架构的应用程序(七):配置数据库

到目前为止,我们还未涉及任何数据库的配置工作。本章节将简单介绍基于Apworks的应用程序的数据库配置。在SQL Server中(目前的Apworks版本仅支持SQL Server)创建两个数据库:TinyLibraryEventDB和TinyLibraryQueryDB,然后分别执行如下SQL脚本:

  • TinyLibraryEventDB
 USE TinyLibraryEventDB;
 /****** Object:  Table [dbo].[DomainEvents]    Script Date: 12/24/2010 20:27:20 ******/    
 IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[DomainEvents]') AND type in (N'U'))    
 DROP TABLE [dbo].[DomainEvents]    
 GO    
 /****** Object:  Table [dbo].[Snapshots]    Script Date: 12/24/2010 20:27:20 ******/    
 IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Snapshots]') AND type in (N'U'))    
 DROP TABLE [dbo].[Snapshots]    
 GO   
 /****** Object:  Table [dbo].[Snapshots]    Script Date: 12/24/2010 20:27:20 ******/   
 SET ANSI_NULLS ON   
 GO   
 SET QUOTED_IDENTIFIER ON   
 GO   
 IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Snapshots]') AND type in (N'U'))   
 BEGIN   
 CREATE TABLE [dbo].[Snapshots](   
  [Id] [bigint] IDENTITY(1,1) NOT NULL,   
  [Timestamp] [datetime] NOT NULL,   
  [SnapshotData] [varbinary](max) NOT NULL,   
  [AggregateRootId] [bigint] NOT NULL,   
  [AggregateRootType] [nvarchar](max) COLLATE Chinese_PRC_CI_AS NOT NULL,   
  [SnapshotType] [nvarchar](max) COLLATE Chinese_PRC_CI_AS NOT NULL,   
  [Version] [bigint] NOT NULL,   
  [Branch] [bigint] NOT NULL,   
  CONSTRAINT [PK_Snapshots] PRIMARY KEY CLUSTERED    
 (   
  [Id] ASC   
 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
 ALLOW_PAGE_LOCKS = ON)   
 )   
 END   
 GO   
 /****** Object:  Table [dbo].[DomainEvents]    Script Date: 12/24/2010 20:27:20 ******/   
 SET ANSI_NULLS ON   
 GO   
 SET QUOTED_IDENTIFIER ON   
 GO   
 IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[DomainEvents]') AND type in (N'U'))   
 BEGIN   
 CREATE TABLE [dbo].[DomainEvents](   
  [Id] [bigint] IDENTITY(1,1) NOT NULL,   
  [AggregateRootId] [bigint] NOT NULL,   
  [AggregateRootType] [nvarchar](max) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,   
  [Timestamp] [datetime] NOT NULL,   
  [Version] [bigint] NOT NULL,   
  [Branch] [bigint] NOT NULL,   
  [TargetType] [nvarchar](max) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,   
  [Data] [varbinary](max) NOT NULL,   
  CONSTRAINT [PK_DomainEvents] PRIMARY KEY CLUSTERED    
 (   
  [Id] ASC   
 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
 ALLOW_PAGE_LOCKS = ON)
 )   
 END   
 GO
  • TinyLibraryQueryDB
 USE TinyLibraryQueryDB;    
 /****** Object:  Table [dbo].[Books]    Script Date: 12/14/2010 14:12:31 ******/    
 IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Books]') AND type in (N'U'))    
 DROP TABLE [dbo].[Books]    
 GO    
 /****** Object:  Table [dbo].[Readers]    Script Date: 12/14/2010 14:12:31 ******/   
 IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Readers]') AND type in (N'U'))    
 DROP TABLE [dbo].[Readers]    
 GO   
 /****** Object:  Table [dbo].[Registrations]    Script Date: 12/14/2010 14:12:31 ******/   
 IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Registrations]') AND type in (N'U'))   
 DROP TABLE [dbo].[Registrations]   
 GO   
 /****** Object:  Table [dbo].[Registrations]    Script Date: 12/14/2010 14:12:31 ******/   
 SET ANSI_NULLS ON   
 GO   
 SET QUOTED_IDENTIFIER ON   
 GO   
 IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Registrations]') AND type in (N'U'))   
 BEGIN   
 CREATE TABLE [dbo].[Registrations](  
  [Id] [bigint] IDENTITY(1,1) NOT NULL,   
  [BookId] [bigint] NOT NULL,  
  [ReaderId] [bigint] NOT NULL,   
  [ReaderName] [nvarchar](max) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,  
  [ReaderLoginName] [nvarchar](max) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,   
  [BookTitle] [nvarchar](255) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,   
  [BookPublisher] [nvarchar](255) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,   
  [BookPubDate] [datetime] NOT NULL,   
  [BookISBN] [nvarchar](255) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,   
  [BookPages] [int] NOT NULL,   
  [RegistrationDate] [datetime] NULL,   
  [DueDate] [datetime] NULL,   
  [ReturnedDate] [datetime] NULL,   
  [Returned] [bit] NULL,   
  CONSTRAINT [PK_Registrations] PRIMARY KEY CLUSTERED    
 (   
  [Id] ASC   
 )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, 
ALLOW_PAGE_LOCKS  = ON)   
 )   
 END   
 GO   
 /****** Object:  Table [dbo].[Readers]    Script Date: 12/14/2010 14:12:31 ******/   
 SET ANSI_NULLS ON   
 GO   
 SET QUOTED_IDENTIFIER ON   
 GO   
 IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Readers]') AND type in (N'U'))   
 BEGIN   
 CREATE TABLE [dbo].[Readers](   
  [Id] [bigint] IDENTITY(1,1) NOT NULL,   
  [LoginName] [nvarchar](max) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,   
  [Name] [nvarchar](max) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,   
  [AggregateRootId] [bigint] NOT NULL,   
  CONSTRAINT [PK_Readers] PRIMARY KEY CLUSTERED    
 (   
  [Id] ASC   
 )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, 
ALLOW_PAGE_LOCKS  = ON)   
 )   
 END   
 GO   
 /****** Object:  Table [dbo].[Books]    Script Date: 12/14/2010 14:12:31 ******/   
 SET ANSI_NULLS ON   
 GO   
 SET QUOTED_IDENTIFIER ON   
 GO   
 IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Books]') AND type in (N'U'))   
 BEGIN   
 CREATE TABLE [dbo].[Books](   
  [Id] [bigint] IDENTITY(1,1) NOT NULL,   
  [Title] [nvarchar](255) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,   
  [Publisher] [nvarchar](255) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,   
  [PubDate] [datetime] NOT NULL,   
  [ISBN] [nvarchar](255) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,   
  [Pages] [int] NOT NULL,   
  [Lent] [bit] NULL,   
  [AggregateRootId] [bigint] NOT NULL,   
  [LendTo] [nvarchar](255) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,   
  CONSTRAINT [PK_Books] PRIMARY KEY CLUSTERED    
 (   
  [Id] ASC   
 )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, 
ALLOW_PAGE_LOCKS  = ON)   
 )   
 END   
 GO

由于我们所使用的数据库系统是SQL Server,对于上面的TinyLibraryEventDB而言,它就是直接使用的Apworks应用开发框架中自带的SQL脚本。开发人员可以在Apworks的安装目录中找到该脚本文件。

就关系型数据库而言,我们需要一个额外的步骤去维护对象与数据表的映射关系。Apworks采用的是XML Storage Mapping Schema来维护这样的关系。

1.向TinyLibrary.Services项目添加一个新的XML文件,将其更名为DomainEventStorageMappings.xml,然后输入下面的内容

    <?xml version="1.0" encoding="utf-8"?>    
    <StorageMappingSchema>    
      <DataTypes>    
      <DataType FullName="Apworks.Events.Storage.DomainEventDataObject" MapTo="DomainEvents">   
          <Properties>    
            <Property Name="Id" MapTo="Id" Identity="true" AutoGenerate="true"/>    
          </Properties>    
        </DataType>   
        <DataType FullName="Apworks.Events.Storage.SnapshotDataObject" MapTo="Snapshots">   
         <Properties>   
           <Property Name="Id" MapTo="Id" Identity="true" AutoGenerate="true"/>   
         </Properties>   
       </DataType>   
      </DataTypes>   
   </StorageMappingSchema>

2.向TinyLibrary.Services项目添加一个新的XML文件,将其更名为QueryObjectStorageMappings.xml,然后输入下面的内容

    <?xml version="1.0" encoding="UTF-8"?>
     <StorageMappingSchema>    
	   <DataTypes>    
	     <DataType FullName="TinyLibrary.QueryObjects.ReaderObject" MapTo="Readers">    
	       <Properties>    
	         <Property Name="Id" MapTo="Id" Identity="true" AutoGenerate="true"/>    
	       </Properties>    
	     </DataType>    
	     <DataType FullName="TinyLibrary.QueryObjects.BookObject" MapTo="Books">   
          <Properties>   
            <Property Name="Id" MapTo="Id" Identity="true" AutoGenerate="true"/>   
          </Properties>   
        </DataType>   
        <DataType FullName="TinyLibrary.QueryObjects.RegistrationObject" MapTo="Registrations">   
          <Properties>   
            <Property Name="Id" MapTo="Id" Identity="true" AutoGenerate="true"/>   
            <Property Name="BookAggregateRootId" MapTo="BookId"/>   
            <Property Name="ReaderAggregateRootId" MapTo="ReaderId"/>   
          </Properties>   
        </DataType>   
      </DataTypes>   
    </StorageMappingSchema>

以下是这些XML文件的XSD结构,这个XSD Schema也被包含在Apworks应用开发框架的安装目录中。

    <?xml version="1.0" encoding="UTF-8"?>
     <!-- edited with XMLSpy v2009 (http://www.altova.com) by Administrator (EMBRACE) -->    
	  <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"
 attributeFormDefault="unqualified"> 
     <xs:element name="StorageMappingSchema">    
	   <xs:annotation>    
	    <xs:documentation>Represents the schema for storage mapping.</xs:documentation>    
	   </xs:annotation>    
	   <xs:complexType>    
	    <xs:sequence minOccurs="0">   
        <xs:element ref="DataTypes"/>   
       </xs:sequence>   
      </xs:complexType>   
     </xs:element>   
     <xs:element name="DataTypes">   
      <xs:complexType>   
       <xs:sequence minOccurs="0" maxOccurs="unbounded">   
        <xs:element ref="DataType"/>   
       </xs:sequence>   
      </xs:complexType>   
     </xs:element>   
     <xs:element name="DataType">   
      <xs:complexType>   
       <xs:sequence minOccurs="0">   
        <xs:element ref="Properties"/>   
       </xs:sequence>   
       <xs:attribute name="FullName" type="xs:string" use="required"/>   
       <xs:attribute name="MapTo" type="xs:string" use="required"/>   
      </xs:complexType>   
     </xs:element>   
     <xs:element name="Properties">   
      <xs:complexType>   
       <xs:sequence minOccurs="0" maxOccurs="unbounded">   
        <xs:element ref="Property"/>   
       </xs:sequence>   
      </xs:complexType>   
     </xs:element>   
     <xs:element name="Property">   
      <xs:complexType>   
       <xs:attribute name="Name" type="xs:string" use="required"/>   
       <xs:attribute name="MapTo" type="xs:string" use="required"/>   
       <xs:attribute name="Identity" type="xs:boolean" use="optional"/>   
       <xs:attribute name="AutoGenerate" type="xs:boolean" use="optional"/>   
      </xs:complexType>   
     </xs:element>   
    </xs:schema>

使用Apworks开发基于CQRS架构的应用程序(八):应用程序的配置与编译

Apworks使用配置文件来启动整个系统。在上文中也能够看出,WCF服务在启动时,同时也启动了Apworks系统。所以,本节简要介绍这个WCF服务针对Apworks的相关配置节内容。

双击TinyLibrary.Services项目的web.config文件,根据下面的XML代码编辑该文件。

   <?xml version="1.0"?>   
   <configuration>   
     <configSections>   
       <section name="apworksConfiguration"    
           type="Apworks.Config.ApworksConfigHandler, Apworks"/>   
       <section name="unityConfiguration"    
          type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.
Configuration"/>   
     </configSections>  
    <apworksConfiguration>  
      <application bootStrapper="Apworks.Application.BootStrapper, Apworks"/>  
      <objectContainer provider="Apworks.ObjectContainers.Unity.UnityContainer, Apworks.ObjectContainers.Unity" 
section="unityConfiguration"/>  
      <generators>  
        <generator name="defaultIdentityGenerator" kind="Identity" type="Apworks.Generators.
SequentialIdentityGenerator, Apworks"/>  
        <generator name="defaultSequenceGenerator" kind="Sequence" type="Apworks.Generators.
SequentialIdentityGenerator, Apworks"/>  
      </generators>  
      <handlers>  
        <handler name="TinyLibraryCommandHandlers"  
                 kind="Command"  
                 sourceType="Assembly"  
                 source="TinyLibrary.CommandHandlers"/>  
        <handler name="TinyLibraryEventHandlers"  
                 kind="Event"  
                 sourceType="Assembly"  
                 source="TinyLibrary.EventHandlers"/>  
      </handlers>  
    </apworksConfiguration>  
    <unityConfiguration>  
      <container>  
        <register type="Apworks.Events.Serialization.IDomainEventSerializer, Apworks"  
              mapTo="Apworks.Events.Serialization.DomainEventXmlSerializer, Apworks">  
        </register>  
        <register type="Apworks.Storage.IStorageMappingResolver, Apworks"  
                    mapTo="Apworks.Storage.XmlStorageMappingResolver, Apworks"  
                    name="DomainEventStorageMappingResolver">  
          <constructor>  
            <param name="fileName" value="DomainEventStorageMappings.xml"/>  
          </constructor>  
        </register>  
        <register type="Apworks.Events.Storage.IDomainEventStorage, Apworks"  
                  mapTo="Apworks.Events.Storage.SqlDomainEventStorage, Apworks">  
          <constructor>  
            <param name="connectionString"   
  value="Data Source=.\SQLEXPRESS;Initial Catalog=TinyLibraryEventDB;Integrated Security=True;Pooling=False;
MultipleActiveResultSets=True;"/>  
            <param name="mappingResolver">  
              <dependency type="Apworks.Storage.IStorageMappingResolver, Apworks" name=
"DomainEventStorageMappingResolver"/>  
            </param>  
          </constructor>  
        </register>  
     
        <register type="Apworks.Bus.ICommandBus, Apworks" mapTo="Apworks.Bus.DirectBus.DirectCommandBus, 
Apworks.Bus.DirectBus">  
          <constructor>  
            <param name="dispatcher">  
              <dependency type="Apworks.Bus.IMessageDispatcher, Apworks"/>  
            </param>  
          </constructor>  
          <lifetime type="ContainerControlledLifetimeManager"/>  
        </register>  
        <register type="Apworks.Bus.IEventBus, Apworks" mapTo="Apworks.Bus.DirectBus.DirectEventBus, 
Apworks.Bus.DirectBus">  
          <constructor>  
            <param name="dispatcher">  
              <dependency type="Apworks.Bus.IMessageDispatcher, Apworks"/>  
            </param>  
          </constructor>  
          <lifetime type="ContainerControlledLifetimeManager"/>  
        </register>  
     
        <register type="Apworks.Bus.IMessageDispatcher, Apworks" mapTo="Apworks.Bus.DirectBus.DirectMessageDispatcher, 
Apworks.Bus.DirectBus">  
          <lifetime type="ContainerControlledLifetimeManager"/>  
        </register>  
        <register type="Apworks.Repositories.IDomainRepository, Apworks" mapTo="Apworks.Repositories.DomainRepository,
Apworks">  
          <constructor>  
            <param name="storage">  
              <dependency type="Apworks.Events.Storage.IDomainEventStorage, Apworks"/>  
            </param>  
            <param name="eventBus">  
              <dependency type="Apworks.Bus.IEventBus, Apworks"/>  
            </param>  
          </constructor>  
        </register>  
     
        <register type="Apworks.Storage.IStorageMappingResolver, Apworks"  
                    mapTo="Apworks.Storage.XmlStorageMappingResolver, Apworks"  
                    name="QueryStorageMappingResolver">  
          <constructor>  
            <param name="fileName" value="QueryObjectStorageMappings.xml"/>  
          </constructor>  
        </register>  
        <register type="Apworks.Queries.Storage.IQueryObjectStorage, Apworks" 
                  mapTo="Apworks.Queries.Storage.SqlQueryObjectStorage, Apworks"> 
          <constructor> 
            <param name="connectionString"  
  value="Data Source=.\SQLEXPRESS;Initial Catalog=TinyLibraryQueryDB;Integrated Security=True;Pooling=False;
MultipleActiveResultSets=True;"/> 
            <param name="mappingResolver"> 
              <dependency type="Apworks.Storage.IStorageMappingResolver, Apworks" name="QueryStorageMappingResolver"/> 
            </param> 
          </constructor> 
        </register> 
      </container> 
    </unityConfiguration> 
    <system.web> 
      <compilation debug="true" targetFramework="4.0" /> 
    </system.web> 
    <system.serviceModel> 
      <behaviors> 
        <serviceBehaviors> 
          <behavior> 
            <serviceMetadata httpGetEnabled="true"/> 
            <serviceDebug includeExceptionDetailInFaults="false"/> 
          </behavior> 
        </serviceBehaviors> 
      </behaviors> 
      <serviceHostingEnvironment multipleSiteBindingsEnabled="true" /> 
    </system.serviceModel> 
   <system.webServer> 
      <modules runAllManagedModulesForAllRequests="true"/> 
    </system.webServer> 
     
  </configuration>

以上配置文件主要有两个configSection:apworksConfiguration和unityConfiguration,分别定义了针对Apworks框架和Microsoft Unity的配置内容。以下是Apworks框架配置的部分信息,仅供参考:

  • application:指定应用程序的启动器,它将被Apworks.Application.BootStrapperFactory类的Create方法创建
  • objectContainer:指定用于Apworks框架的IoC/DI容器适配器,目前Apworks只支持基于Microsoft Unity的适配器
  • generators:指定整个系统所使用的标识产生器与序列号产生器。如果未指定针对标识产生器的配置,则系统会默认使用Apworks.Generators.SequentialIdentityGenerator作为标识产生器;如果未指定针对序列号产生器的配置,则系统会默认使用Apworks.Generators.SequentialIdentityGenerator作为序列号产生器
  • handlers:指定定义了命令处理器与事件处理器的类型或程序集。当应用程序启动器启动系统的时候,这些处理器将被注册到系统中

注意上面配置节中的黄色高亮部分,记得在实际使用中,将其修改为实际的数据库链接字符串。

至此,我们已经完成了应用系统部分的开发任务,UI部分就是通过调用所需的WCF服务来实现其数据处理功能,因此本系列文章将不再讨论UI部分的实现方式。有兴趣的读者请直接到TinyLibraryCQRS的网站http://tlibcqrs.codeplex.com阅读完整源代码。下一讲将介绍系统的启动与运行相关内容。

<span class="image-wrap " style="position:relative; display:inline-block; background:url(image.jpg) no-repeat
 center center; width: 150px; height: 150px;">
    <img src="image.jpg" style="opacity: 0;">
</span>

使用Apworks开发基于CQRS架构的应用程序(九):运行应用程序

启动WCF服务

1.在TinyLibraryCQRS解决方案下,找到TinyLibrary.Services项目

2.右键单击CommandService.svc,然后选择View in Browser,这将启动ASP.NET Development Server

3.当ASP.NET Development Server成功启动,并在浏览器中打开了CommandService.svc后,将出现如下界面

使用soapUI测试WCF服务

可以使用soapUI来对WCF服务进行测试。soapUI是一款用于测试Web Service的工具软件。有关soapUI的更多信息,请参考http://www.soapui.org。

1.打开soapUI,然后选择File | New soapUI Project菜单,这将打开New soapUI Project对话框

2.在Initial WSDL/WADL文本框中,输入WCF服务的WSDL地址,然后单击OK按钮

3.成功添加项目后,即可在Navigator面板中看到WCF的Bindings和Service Operations

4.现在我们使用soapUI调用WCF服务来创建一本图书。在Navigator中展开CreateBook节点,双击Request 1,这将在右边的窗口中显示一个请求XML的样本

5.将这个请求XML文本改成如下形式

    <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tem="http://tempuri.org/">   
       <soapenv:Header/>   
       <soapenv:Body>   
          <tem:CreateBook>   
             <!--Optional:-->   
             <tem:title>Essential .NET, Volume 1: The Common Language Runtime</tem:title>   
             <!--Optional:-->   
             <tem:publisher>Addison Wesley</tem:publisher>   
             <!--Optional:-->  
            <tem:pubDate>2002-11-01</tem:pubDate>  
            <!--Optional:-->  
            <tem:isbn>0-201-73411-7</tem:isbn>  
            <!--Optional:-->  
            <tem:pages>432</tem:pages>  
         </tem:CreateBook>  
      </soapenv:Body>  
   </soapenv:Envelope>

6.单击Request 1子窗口上的绿色右箭头按钮以调用WCF服务,当服务成功调用后,将得到如下结果

    <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> 
      <s:Body>   
         <CreateBookResponse xmlns="http://tempuri.org/">   
            <CreateBookResult>161891709900001</CreateBookResult>   
         </CreateBookResponse>   
      </s:Body>   
   </s:Envelope>

上面的数字161891709900001就是Book ID

7.使用上面相同的方法在soapUI中添加QueryService.svc

8.在GetAllBooks操作上创建并提交如下请求

     <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tem="http://tempuri.org/">
        <soapenv:Header/>   
        <soapenv:Body>   
           <tem:GetAllBooks/>   
        </soapenv:Body>   
     </soapenv:Envelope>

当请求被成功处理后,将得到如下结果

    <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
       <s:Body>   
          <GetAllBooksResponse xmlns="http://tempuri.org/">   
             <GetAllBooksResult xmlns:a="http://schemas.datacontract.org/2004/07/TinyLibrary.QueryObjects"    
    xmlns:i="http://www.w3.org/2001/XMLSchema-instance">   
                <a:BookObject>   
                  <a:AggregateRootId>161891709900001</a:AggregateRootId>   
                   <a:ISBN>0-201-73411-7</a:ISBN>   
                   <a:Id>1</a:Id>  
                  <a:LendTo/>  
                  <a:Lent>false</a:Lent>  
                  <a:Pages>432</a:Pages>  
                  <a:PubDate>2002-11-01T00:00:00</a:PubDate>  
                  <a:Publisher>Addison Wesley</a:Publisher>  
                  <a:Title>Essential .NET, Volume 1: The Common Language Runtime</a:Title>  
               </a:BookObject>  
            </GetAllBooksResult>  
         </GetAllBooksResponse>  
      </s:Body>  
   </s:Envelope>

运行应用程序

1.在TinyLibraryCQRS解决方案下,右键单击TinyLibrary.WebApp项目(之前的文章中没有介绍该项目,请读者朋友到http://tlibcqrs.codeplex.com下载完整的解决方案文件),然后选择Set as StartUp Project

2.按下Ctrl+F5直接运行应用程序

3.当应用程序成功执行后,将出现如下界面


 
分享到
 
 


专家视角看IT与架构
软件架构设计
面向服务体系架构和业务组件
人人网移动开发架构
架构腐化之谜
谈平台即服务PaaS


面向应用的架构设计实践
单元测试+重构+设计模式
软件架构师—高级实践
软件架构设计方法、案例与实践
嵌入式软件架构设计—高级实践
SOA体系结构实践


锐安科技 软件架构设计方法
成都 嵌入式软件架构设计
上海汽车 嵌入式软件架构设计
北京 软件架构设计
上海 软件架构设计案例与实践
北京 架构设计方法案例与实践
深圳 架构设计方法案例与实践
嵌入式软件架构设计—高级实践
更多...