Could not load file or assembly "App_Licenses.dll"的问题

今天在AspDotNetStorefront做定制化开发,编译的时候莫名其妙地报告Could not load file or assembly ‘App_Licenses, Version=0.0.0.0, Culture=neutral, (Exception from HRESULT: 0x80070057 (E_INVALIDARG))的错误,检查文件系统,发现文件还在,然后查看文件夹权限,也没有发现任何异样。于是Google。网上很多网友给出解决方案,比如:重启/重装ASP.NET,设置Network Service权限等等,结果无功而返。

后来我发现,不仅仅是这个App_Licenses.dll无法加载,就算我从References中删去这个引用,仍然会提示其它的DLL也无法加载的情况。而且,我重新安装一份新的AspDotNetStorefront应用也出现同样的问题。很明显,不是我解决方案本身的问题。

突然想起之前有过机器异常重启的经历,我估计是在异常重启的过程中造成了文件丢失或者损坏,于是先用iisreset/stop停止IIS服务,然后打开ASP.NET的临时文件夹(在32bit,.NET 2.0中,是%Windows Install Folder%\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files),删除所有的文件和文件夹,再iisreset/start后,编译解决方案,一切正常。

这问题其实不复杂,但真正碰到这样的问题解决起来也挺难的。在此随笔一下,也希望能够帮到遇到同样问题的网友。

转载至:http://www.cnblogs.com/daxnet/archive/2010/03/29/1699622.html

发表在 C# | 留下评论

一种来源于Microsoft Dynamics AX的权限管理设计思想

网上有很多关于权限管理系统的设计方案以及案例,甚至有相关的可以直接使用的权限管理框架。权限管理并非我的研究对象,我也只通皮毛,今天就在专家面前班门弄斧,简单介绍一下微软Dynamics AX系统的权限管理,以及由此派生出来的一种基于.NET的权限设计思想。

或许在看完本文后,你会觉得,我所提出的这个想法很火星,你早就知道这种方案了。不要紧,就当我在此将这些内容整理一下,以备日后参考便是了。

演练:体验Microsoft Dynamics AX的权限管理

  1. 在Dynamics AX中创建一个项目,在此命名为StudentsPrj,这个项目非常简单,就是维护一张数据表,表里包含了学生的ID、姓名以及年龄。如何创建项目就不介绍了,项目结构如下图:
    image 
    注意上图中的SecurityKeys节点,下面有一个StudentsAdmin的Security Key,这就是AX中权限管理的关键所在。选中该节点,按下Alt+Enter,打开属性页面,设置StudentsAdmin的属性:
    image
  2. 在Students数据表上,按Alt+Enter,打开属性页面,将该表的SecurityKey设置为StudentsAdmin:
    image
  3. 同理,在Students Menu Item上,也设置SecurityKey为StudentsAdmin:
    image
  4. 打开Main Menu –> Administration –> Users窗体,往系统用户中添加一个非Admin的域账户,以便我们做测试:
    image
  5. 打开Main Menu –> Adminstration –> Setup –> User groups菜单,新建两个group,一个叫StuGuest,一个叫StuAdmin,分别表示StudentsPrj的Guest组和Admin组,单击Users选项卡,分别将Admin加入StuAdmin组,将saratoga加入StuGuest组:
    image
     
  6. 选中StuAdmin,点击上图右边的Permissions按钮,打开权限设置对话框。在对话框的底部可以看到Students Administration权限选项,选中该权限,并选择Full Control选项:
    image
  7. 同理,在User groups对话框中选择StuGuest组,同样打开上面的权限设置对话框,选择Students administration权限,并设置Access为View:
    image
  8. 退出Dynamics AX,重新以Admin账户登录,进入Students project菜单,在Common Forms下有一个Students的菜单项,单击该菜单项,将打开Students窗体。你可以在这个窗体中执行增、删、改操作:
    image
  9. 退出Dynamics AX,在AX的图标上单击右键,选择“Run as” 选项,以saratoga账户登录AX,你将发现,你仅可浏览Students记录。无法对其进行增、删、改操作:
    image

思考:Microsoft Dynamics AX的权限管理设计

从上面的演练可以看到,Security Key是Dynamics AX的权限管理的核心。Security Key是一种对象,它关联了角色与权限:权限设置在Security Key上,而不是设置在角色上,而Security Key又关联了权限与系统中的对象。因此,它可以很明确地表述“谁在什么上有什么样的权限”。

我觉得我们可以在.NET的权限设计上,利用.NET的Attribute来模拟Security Key的功能。比如,我们可以设计一个MyPermissionAttribute,使其继承于PermissionAttribute,然后在对象上,应用这个MyPermissionAttribute,以表示该对象可以接受MyPermission的设置;之后提供一个界面,设计某些角色在MyPermission上具有什么权限,这样,也就意味着这些角色在MyPermission所修饰的对象上,具有设置的权限。这要比直接在角色上直接设置权限更加灵活。

使用Attribute的另一个好处就是,我们可以利用对象的继承关系来设计权限的继承关系,就好比AX中,Security Key都可以设置Parent Key一样。如果设置Parent Attribute具有Full Control的权限,那么可以认为,其子Attribute也具有Full Control的权限。

 

这些都是个人想法,具体细节还需要在实践中去体会和把握。

转载至:http://www.cnblogs.com/daxnet/archive/2010/07/20/1781201.html

发表在 C# | 留下评论

使用Object Adapter模式维护成员的访问级别

在设计class library或者framework时有可能遇到这样的问题,或许有的朋友已经碰到过这样的问题了。比如,在实现CQRS体系结构模式时,我们通过Versioning和Branching的方式设计Event Sourcing的版本路线(Version Route),至于什么是Versioning和Branching,以及为何需要在Event Sourcing中引入Version Control,本文不做详细讨论。有兴趣的朋友可以去阅读一些有关版本控制的文章。

那么,我们很有可能会在class library中加入下面这个接口:

隐藏行号 复制代码 代码
  1. public interface IVersionControllable
    
  2.  {
    
  3.     long Version { get; }
    
  4.     long Branch { get; }
    
  5. }
    
  6. 
    

由于Version和Branch的数据是由整个框架内部管理的,只允许客户程序读取这两个值,而不允许客户程序随意修改,因此,在接口中,这两个属性被指定为只读。理所当然,实现了该接口的类,必定需要实现其中的这两个属性:

隐藏行号 复制代码 代码
  1. public class SourcedAggregationRoot : IVersionControllable
    
  2. {
    
  3.     private long version;
    
  4.     private long branch;
    
  5. 
    
  6.     #region IVersionControllable Members
    
  7. 
    
  8.     public long Version
    
  9.     {
    
  10.         get { return this.version; }
    
  11.         internal set { this.version = value; }
    
  12.     }
    
  13. 
    
  14.     public long Branch
    
  15.     {
    
  16.         get { return this.branch; }
    
  17.         internal set { this.branch = value; }
    
  18.     }
    
  19. 
    
  20.     #endregion
    
  21. }
    
  22. 
    

在上面SourcedAggregationRoot的实现中,我们为Version和Branch属性加上了internal setter,目的很明显,就是希望能够在class library中修改version和branch的值,而不允许框架以外的客户程序去修改这两个值。目前看似达到了成员访问级别的控制目的,但实际上事情并没结束。

假设我们现在开发的是一套完整的应用程序框架,既然是框架,就需要支持扩展。假设框架中某组件A(也假设A实现接口IA)会使用以上两个属性去更改SourcedAggregationRoot的版本信息,如果组件A与SourcedAggregationRoot被定义在同一个assembly中,那么,A可以直接使用这两个internal setter来修改SourcedAggregationRoot的version和branch。当然,A组件只是我们的框架所提供的一个默认组件,A的功能是可以被扩展甚至重写的。现在,根据客户需求,A组件的功能需要重写(比如原本是采用的SQL Server DAL,现在要用Oracle DAL了),于是我们就会重新新建一个class library,在这个class library中,新建一个类,实现接口IA,然后填写我们自己的逻辑。在完成某项操作时,也去调用SourcedAggregationRoot的Version和Branch属性,试图更改这两个值。于是,问题来了,由于internal访问级别的限制,我们无法修改Version和Branch,连编译都过不去。

你可能会说,这好办,直接将internal关键字去掉不就ok啦。这样做爽是爽了,但也导致客户程序能够非常轻易地修改version和branch的值,而这两个值本应该由框架进行维护的。Coding上有一个原则就是尽量把代码错误控制在编译时。将Version和Branch属性改为公有可写(public setter)的话,很难保证客户程序不会对其进行误操作。总之一句话,现在希望framework中的组件能够修改version和branch,客户程序不允许对其进行修改。貌似现有的C#访问控制修饰符没法达到这个看似变态的要求。

其实解决方案也很简单,就是在SourcedAggregationRoot所在的assembly中,直接加一个object adapter,然后把adapter设置为public的就可以了:

隐藏行号 复制代码 代码
  1. public class SetterAdapter<TSourcedAggregationRoot>
    
  2.     where TSourcedAggregationRoot : SourcedAggregationRoot
    
  3. {
    
  4.     private TSourcedAggregationRoot obj;
    
  5. 
    
  6.     public SetterAdapter(TSourcedAggregationRoot obj)
    
  7.     {
    
  8.         this.obj = obj;
    
  9.     }
    
  10. 
    
  11.     public SetterAdapter<TSourcedAggregationRoot> SetVersion(long version)
    
  12.     {
    
  13.         this.obj.Version = version;
    
  14.         return this;
    
  15.     }
    
  16. 
    
  17.     public SetterAdapter<TSourcedAggregationRoot> SetBranch(long branch)
    
  18.     {
    
  19.         this.obj.Branch = branch;
    
  20.         return this;
    
  21.     }
    
  22. 
    
  23.     public TSourcedAggregationRoot Unwrap
    
  24.     {
    
  25.         get { return this.obj; }
    
  26.     }
    
  27. }
    
  28. 
    

这样,我们就可以在framework的其它class library或assembly中,使用下面的方式改变SourcedAggregationRoot的version和branch的值了:

隐藏行号 复制代码 代码
  1. SourcedAggregationRoot vc = new SourcedAggregationRoot();
    
  2. // Now Version = 0, Branch = 0
    
  3. vc = new SetterAdapter<SourcedAggregationRoot>(vc)
    
  4.     .SetBranch(100)
    
  5.     .SetVersion(12)
    
  6.     .Unwrap;
    
  7. // Now Version = 12, Branch = 100
    
  8. 
    

当然,客户代码同样可以使用SetterAdapter来修改这两个值,但与直接将Version和Branch属性设置为public相比,这种做法要安全的多。我觉得,在做framework设计的时候,每个细节都要仔细斟酌,成员的访问级别设置过高,并不影响整个框架的运行结果,但这样做会打破面向对象的封装特性,把本不应该暴露的信息一览无余地暴露给调用者。在上面的例子中引入SetterAdapter,维护了Version和Branch属性的访问级别。

欢迎大家针对这样的问题发表自己的观点!

【点击此处下载本文的示例代码】

转载至:http://www.cnblogs.com/daxnet/archive/2010/08/10/1796581.html

发表在 C# | 留下评论

使用InternalsVisibleToAttribute控制internal成员的访问

在上一篇文章中,为了解决成员访问级别的问题,我采用了一个object adapter,使得客户程序无法轻易地修改对象的属性。网友评论说,我的这种做法就是绕了一圈,的确,这种方法没办法阻止客户程序刻意地去修改对象的属性,但在一定程度上起到了保护作用:至少降低了误操作的可能性。

今天发现其实可以用System.Runtime.CompilerServices.InternalsVisibleToAttribute这个特性达到同样的效果。以下是操作步骤。

  1. 假设ExposedSetter.Library这个class library中包含了一个具有internal setter属性的SourcedAggregationRoot类。首先,对ExposedSetter.Library作数字签名。在ExposedSetter.Library项目上点右键选择“Properties”。在“Signing”页下选择“Sign the assembly”选项,然后指定一个强名称密钥文件。SourcedAggregationRoot的代码以及ExposedSetter.Library的签名设置如下
  2. 隐藏行号 复制代码 代码
    1. public class SourcedAggregationRoot : IVersionControllable
      
    2. {
      
    3.     private long version;
      
    4.     private long branch;
      
    5. 
      
    6.     #region IVersionControllable Members
      
    7. 
      
    8.     public long Version
      
    9.     {
      
    10.         get { return this.version; }
      
    11.         internal set { this.version = value; }
      
    12.     }
      
    13. 
      
    14.     public long Branch
      
    15.     {
      
    16.         get { return this.branch; }
      
    17.         internal set { this.branch = value; }
      
    18.     }
      
    19. 
      
    20.     #endregion
      
    21. 
      
    22.     public override string ToString()
      
    23.     {
      
    24.         return string.Format("Version = {0}, Branch = {1}",
      
    25.             version, branch);
      
    26.     }
      
    27. }
      
    28. 
      

     image

  3. 新建一个class library,以调用ExposedSetter.Library中SourcedAggregationRoot的internal setter属性。为方便描述,我们将这个class library命名为ExposedSetter.Library2。在这个class library上添加对ExposedSetter.Library的引用,同时随便写一个测试的类,在类里直接调用SourcedAggregationRoot的internal setter属性来设置属性值。
  4. 隐藏行号 复制代码 代码
    1. public class Class1
      
    2. {
      
    3.     public SourcedAggregationRoot Test()
      
    4.     {
      
    5.         SourcedAggregationRoot sar = new SourcedAggregationRoot();
      
    6.         sar.Branch = 125;
      
    7.         return sar;
      
    8.     }
      
    9. }
      
    10. 
      

    同样,为ExposedSetter.Library2做数字签名(这一点很重要!)

  5. 启动Visual Studio的Command Line,使用sn.exe获得ExposedSetter.Library2的公共密钥(public key),注意:是public key,而不是public key token
    image
  6. 打开ExposedSetter.Library的AssemblyInfo.cs文件,向其添加InternalsVisibleToAttribute:
  7. 隐藏行号 复制代码 代码
    1. [assembly: InternalsVisibleTo("ExposedSetter.Library2, PublicKey=0024000004800000940000000602000000240" +
      
    2.     "000525341310004000001000100bbccb249a2e7a1" +
      
    3. "7cbddf86e24532777568cb13c2ea7643b61cf60367068f2b9ca785dca303c49f015823e4eaa17b" +
      
    4. "50ed60ac47563dc8d8771358f10c3dc41f288530cfa350e6a2a24781dedeb8ec4138f93e76c537" +
      
    5. "bce6c5aa7b25858fa90d6ef5c6ea613b1b49e6e287f9ebb7f990cfa0ce17fbfe1c338e95e88c14" +
      
    6. "81f9598f")]
      

    注意,InternalsVisibleToAttribute的参数指定了允许访问其internal成员的assembly名称和公共密钥数据。在此不能设置assembly的版本等信息,否则编译都过不去。

  8. 编译解决方案

这种方式有点像C++的友元。与我上文提到的引入object adapter的解决方案相比,这种做法更加professional一些,毕竟可以通过.NET Framework直接支持,而且也更加安全一些,它能够明确指定哪些assembly可以访问,其余的则不能访问。但这种做法也有弊端:假设我今后又有一个assembly希望使用ExposedSetter.Library中的internal成员,我需要去修改ExposedSetter.Library中的代码,以添加一个InteralsVisibleToAttribute特性。

 

【点击此处下载本文示例代码】

转载至:http://www.cnblogs.com/daxnet/archive/2010/08/12/1797782.html

发表在 C# | 留下评论

“Unrecoverable build error”的解决办法

今天在为我的新作Visual Benchmark做安装程序。为了简单起见,我选择了Visual Studio自带的Setup Project模板。当设置好所有选项以后,在编译安装程序的时候发生了Unrecoverable build error的错误。打开浏览器google,发现微软有篇文章介绍了解决方案。按着这篇文章实验,问题仍然没有解决。最后发现,连ole32.dll也需要regsvr32一下。整个命令行如下所示:

 

regsvr32 “C:\Program Files\Common Files\Microsoft Shared\MSI Tools\mergemod.dll”

regsvr32 “ole32.dll”

 

完成上两步注册后,重新编译安装程序,问题消失。

转载至:http://www.cnblogs.com/daxnet/archive/2010/09/08/1821262.html

发表在 C# | 留下评论

无法添加对mscoree.tlb的COM引用

出于Cross AppDomain Singleton模式的实现需要,我需要向我的Proxy Library中添加对mscoree.tlb的引用。但是当我从c:\windows\Microsoft.NET\Framework\v2.0.50727目录下选中这个文件的时候,Visual Studio提示,无法添加对mscoree.tlb的引用。一番尝试后,终不得正解。

最后,直接在c:\windows\Microsoft.NET\Framework\v2.0.50727的命令行中敲入“regtlibv12 mscoree.tlb”,在完成注册后,成功添加了对mscoree.tlb的引用。

转载至:http://www.cnblogs.com/daxnet/archive/2010/09/09/1822008.html

发表在 C# | 留下评论

领域驱动设计案例:Tiny Library:简介

应广大网友的要求,我最近抽空基于ASP.NET MVC + WCF + Entity Framework做了一个案例,该案例以图书馆图书管理、读者借书、还书为业务背景,以领域驱动设计为思想指导,全程采用Microsoft技术进行实践,希望能够给Microsoft技术的狂热者以及领域驱动设计的学者提供实践参考。

本案例选用的业务逻辑非常简单,所以项目取名上我选用了“Tiny Library”,在后面一章我将详细介绍这个案例的业务逻辑、模型设计与系统架构。

下载案例

本来打算将项目发布到codeplex上,便于大家交流,也便于代码更新与维护,但由于某些原因,我无法在自己的网络环境中连接codeplex的svn/tfs服务,于是,目前只能以压缩包的形式发布案例源代码,希望大家谅解,等以后有机会更新到codeplex上后再通知大家。

【请单击此处下载案例源代码】

 

系统需求

  • Microsoft Visual Studio 2010
  • Microsoft Patterns & Practices 5.0(v5.0.414.0,Runtime v2.0.50727。请自行到Microsoft官方网站下载安装)
  • Microsoft ASP.NET MVC 2
  • Microsoft Entity Framework(注意:是Visual Studio 2010自带的那个版本,而不是最新发布的那个Feature Pack CTP版本)
  • Microsoft SQL Express 2008 SP1
  • Apworks Application Development Framework

请在打开本案例解决方案之前自行安装上述软件和组件!

说明:Apworks Application Development Framework是我自己开发的一套领域驱动(Domain Driven)的应用程序开发框架,里面提供了对Aggregate Root、Repositories、Specifications以及Transaction Context的支持,基本能够满足基于Microsoft.NET技术的中小型领域驱动项目的应用开发。目前这个框架项目正在进一步实现基于CQRS体系结构模式的框架。为了节约时间,本系列文章不会对Apworks Application Development Framework做太多介绍。本框架目前也还是under construction,所以读者朋友也千万不要将其用在自己的系统开发中,以免发生危险!有关Apworks Application Development Framework的源代码以及更多信息,请访问项目站点:http://apworks.codeplex.com。Tiny Library压缩包里包含了一个可被Tiny Library使用的Apworks版本,因此读者朋友无需自己去Apworks站点上下载并编译源代码。当然,如果您希望了解Apworks的实现方式,可以使用上面的站点查看Apworks的源代码。

 

安装部署

  1. 建立数据库
    使用Microsoft Visual Studio 2010提供的Server Explorer功能,在Data Connections上单击鼠标右键,选择Create New SQL Server Database选项,此时出现Create New SQL Server Database对话框,在对话框的Server name中输入(local)\SQLEXPRESS,在New database name中输入TinyLibraryDB,之后单击OK按钮
    image
  2. 创建数据库Schema
    使用Microsoft Visual Studio 2010打开TinyLibrary解决方案,在TinyLibrary.Domain项目节点下找到TinyLibrary.edmx.sql脚本文件,打开此脚本文件,在SQL Editor区域,点击鼠标右键,选择Connection | Connect菜单,此时弹出Connect to Database Engine对话框,Server选择SQLEXPRESS,然后单击OK
    image
    再次在SQL Editor区域点击鼠标右键,选择Execute SQL选项,执行SQL脚本以创建数据库Schema
    image
  3. 建立演示数据(Demo Data)
    以上述同样的方式,打开TinyLibrary.Domain项目下的TinyLibrary.DemoData.sql脚本并执行
  4. 3722端口
    Tiny Library的WCF Service采用3722端口作为其服务的固定端口,因此在使用本案例钱,确保该端口未被其它应用程序占用


运行案例

  1. 在Microsoft Visual Studio 2010的Solution Explorer上,右键单击TinyLibrary Solution然后选择Rebuild Solution以重新编译解决方案
    image
  2. 在TinyLibrary.Services项目下,选中TinyLibraryService.svc,然后单击右键,选择View in Browser,此时会自动打开ASP.NET Development Server,端口占用3722,同时打开WCF Service的页面。此时将WCF Service的页面关闭,仅留下ASP.NET Development Server
    image
  3. 右键单击TinyLibrary.WebApp项目,选择Set as StartUp Project选项,然后在Microsoft Visual Studio中按下Ctrl+F5或者Debug | Start Without Debugging选项以启动应用程序
  4. 应用程序启动后,可以看到主界面如下
    image 

 

登录账号

测试需要,Tiny Library默认提供三个用户账户:daxnet、acqy和james。用户名、密码如下:

  1. 登录名:daxnet;名称:DaxNet;密码:daxnet@live.com
  2. 登录名:acqy;名称:Sunny Chen;密码:acqy@163.com
  3. 登录名:james;名称:james;密码:james@tinylibrary.com

 

额外说明

时间有限,本案例仅仅是一个基于Microsoft.NET技术的领域驱动设计实践案例,因此,如下内容没有包含在本案例中:

  1. 基于AOP和Policy Injection的技术实践。这包括:异常处理、数据验证与系统日志
  2. 基于用户/角色验证的图书维护页面
  3. ASP.NET MVC的高级应用
  4. WCF的异常捕获与显示
  5. 单元测试
  6. 其它的一些技术细节

有兴趣的朋友可以在本案例源代码的基础上进行扩充,以实现一套完整的图书馆管理应用。

转载至:http://www.cnblogs.com/daxnet/archive/2010/10/18/1853944.html

发表在 C# | 留下评论

领域驱动设计案例:Tiny Library:业务逻辑与系统结构

之前我发布了领域驱动设计的一个实践案例:Tiny Library。本章介绍该案例实现的业务逻辑与系统结构设计。

 

业务逻辑

Tiny Library的业务逻辑非常简单,主要就是如下两条:

  1. 任何用户可以添加Library中的图书(简化起见,图书不能修改也不能删除),也可以查看图书的详细信息
  2. 注册用户,也就是读者,可以借书、还书、查看自己借过的图书列表和借书信息

篇幅有限,我就不在此将案例的操作过程一一截图了,读者朋友们可以自己下载源代码,然后在Visual Studio 2010中编译运行。

 

系统结构

由于是领域驱动设计,本案例系统分层与传统分层略有不同。分为四层:展现层、应用服务层、领域层和基础结构层。展现层采用ASP.NET MVC框架实现;应用服务层则是一个WCF Service;领域层采用Entity Framework结合本人自己研发的Apworks Application Development Framework;基础结构层则为整个应用提供了IoC、Caching、Specifications、Repository等的具体实现。整个系统架构基本上可以以下图描述:

image 

需要说明的是,在上图中的Domain Model和EdmRepository之间出现了双向依赖,于是我在边上用黄色注释文字作了注解。其实,Domain Model只依赖于仓储(Repository)的接口,而EdmRepository是基于Entity Framework的一种仓储实现方式,它实现IRepository接口,同时也对Domain Model产生依赖,以获得对聚合根的访问。关键的一步在于,Tiny Library采用依赖注入,将EdmRepository注射到Domain Model中,于是,Domain Model根本不依赖于仓储的具体实现方式,保证了领域模型层面的纯净度。领域驱动设计的社区中有很多朋友都对这个问题产生疑问,Tiny Library就是一个回答该问题的很好的案例。

 

Visual Studio 2010解决方案结构

Tiny Library在Microsoft Visual Studio 2010的解决方案下包含六个项目:TinyLibrary.Design、TinyLibrary.Domain、TinyLibrary.Repositories、TinyLibrary.Services、TinyLibrary.WebApp以及TinyLibrary.WebApp.Tests。如下所示:

image

  1. TinyLibrary.Design:项目的一些设计图稿,其中包括上面的架构图。一般没什么用(注意:第一个版本的Tiny Library解决方案中不包含这个项目,不过没关系,不影响大家的阅读和学习)
  2. TinyLibrary.Domain:领域模型项目,其中包括了Tiny Library的领域模型与业务逻辑,也是本案例的核心所在。为了能够以可视化的方式设计领域模型,也是为了采用Microsoft技术来实现案例,本项目采用了Microsoft Entity Framework作为建模工具
  3. TinyLibrary.Repositories:仓储的具体实现项目,它引用TinyLibrary.Domain项目,同时引用Apworks组件以实现IRepository接口。本项目仅包含了针对Entity Framework的仓储实现,同时也一并实现了Repository Transaction Context对象
  4. TinyLibrary.Services:WCF Application Service项目,它占用3722端口,其目的是与Presentation层交互。交互采用Data Transferring Objects(DTO),实现上是一组位于DataObjects目录下的Data Contracts。从这个项目可以看到,DTO与Entity/AggregateRoot并非是一一对应的,虽说在Tiny Library中,看上去是一个Entity/AggregateRoot对应一个Data Object,但仔细阅读可以发现,这些Data Objects中包含的数据跟与之对应的Entity/AggregateRoot中包含的对象状态是有出入的。这是根据应用程序的需求来决定的
  5. TinyLibrary.WebApp:本案例的主程序:一个ASP.NET Web应用程序,以ASP.NET MVC框架为基础,提供用户界面与交互接口
  6. TinyLibrary.WebApp.Tests:TinyLibrary.WebApp项目的单体测试项目,在本案例中,它是创建ASP.NET MVC项目时自动生成的,我没有去写或者修改任何Test Case,所以,可以直接无视这个项目

 

从下一讲开始,我将详细介绍Tiny Library的实现过程。这个过程不会是一步步的手把手教如何创建项目、如何编码等,而是对领域驱动设计在Microsoft .NET技术下的实践进行介绍。这些内容将包括:领域模型的实现、仓储的设计与实现、WCF Service与DTO、ASP.NET MVC的整合、领域驱动设计案例答疑以及Apworks Application Development Framework简介等。敬请期待!

转载至:http://www.cnblogs.com/daxnet/archive/2010/10/20/1856249.html

发表在 C# | 留下评论

领域驱动设计案例:Tiny Library:领域模型

本讲主要介绍基于Entity Framework的领域驱动设计建模。首先回顾一下Tiny Library的业务逻辑:

  1. 任何用户可以添加Library中的图书(简化起见,图书不能修改也不能删除),也可以查看图书的详细信息
  2. 注册用户,也就是读者,可以借书还书、查看自己借过的图书列表借书信息

请注意上面描述的黑体部分,这些概念出现在Tiny Library的领域知识(Domain Knowledge)中,换言之,是Tiny Library领域的通用语言的组成元素。

 

一、实体与聚合根

首先分析出实体,不难看出,读者图书是实体;由于每个读者都将有自己的借书信息(比如,什么时候借的哪本书,是否已经归还,或者是否已经过期),而与之对应地,每本书也可以有被借历史(比如,这本书是什么时候借给哪个读者),于是,借书信息也是实体。

再来看看聚合。借书信息是与读者和图书关联的,也就是说,没有读者,借书信息没有存在的意义,同样,没有图书,借书信息也同样不存在。每个读者可以没有任何借书信息(或者说借书记录),也可以有多条借书信息;而每本书也同样可以没有任何被借信息(或者说被借记录),也可以有多条被借记录。因此存在两个聚合:读者-借书信息聚合(1..0.*)以及图书-借书信息聚合(1..0.*)。读者和图书分别为聚合根,借书信息为实体。与Tiny Library对应起来,总结如下:

  • 读者:Reader,聚合根
  • 图书:Book,聚合根
  • 借书信息:Registration,实体

根据上述描述,我们可以确定,我们将来需要针对读者(Reader)和图书(Book)实现仓储以及相应的规约。

 

二、基于Entity Framework建立领域模型

目前Entity Framework支持三种建模方式:Model First、Database First以及Code First。Code First是在今年刚发布的Feature Pack中才支持的。为了迎合领域驱动设计思想,我们采用Model First。

根据上面的分析,现建模如下:

image

注意:如何在Visual Studio中使用Entity Framework进行Model First建模不是本文讨论的重点,读者朋友请自己参阅相关文档。

此时,我们需要使用C#部分类的特性,将Reader和Book定义为聚合根,将Registration定义为实体。我开发的一个DDD框架(Apworks)中为聚合根和实体的接口作了定义,现在,只需要引用Apworks的程序集,然后使用部分类的特性,让Reader和Book实现IAggregateRoot接口,让Registration实现IEntity接口即可。从技术上看,这样就将Apworks框架整合到了领域模型中。代码如下:

隐藏行号 复制代码 Reader聚合根
  1. public partial class Reader : IAggregateRoot
    
  2. {
    
  3. }
    
  4. 
    

 

隐藏行号 复制代码 Book聚合根
  1. public partial class Book : IAggregateRoot
    
  2. {
    
  3. }
    
  4. 
    

 

隐藏行号 复制代码 Registration实体
  1. public partial class Registration : IEntity
    
  2. {
    
  3. }
    
  4. 
    

三、添加业务逻辑

根据DDD,实体是能够处理业务逻辑的,应该尽量将业务体现在实体上;如果某些业务牵涉到多个实体,无法将其归结到某个实体的话,就需要引入领域服务(Domain Service)。Tiny Library案例业务简单,目前不会涉及到领域服务,因此,在本案例中,业务逻辑都是在实体上处理的。

以读者(Reader)为例,它有借书和还书的行为,我们将这两种行为实现如下:

隐藏行号 复制代码 Reader中的业务逻辑
  1. public partial class Reader : IAggregateRoot
    
  2. {
    
  3.     public void Borrow(Book book)
    
  4.     {
    
  5.         if (book.Lent)
    
  6.             throw new InvalidOperationException("The book has been lent.");
    
  7.         Registration reg = new Registration();
    
  8.         reg.RegistrationStatus = RegistrationStatus.Normal;
    
  9.         reg.Book = book;
    
  10.         reg.Date = DateTime.Now;
    
  11.         reg.DueDate = reg.Date.AddDays(90);
    
  12.         reg.ReturnDate = DateTime.MaxValue;
    
  13.         book.Registrations.Add(reg);
    
  14.         book.Lent = true;
    
  15.         this.Registrations.Add(reg);
    
  16.     }
    
  17. 
    
  18.     public void Return(Book book)
    
  19.     {
    
  20.         if (!book.Lent)
    
  21.             throw new InvalidOperationException("The book has not been lent.");
    
  22.         var q = from r in this.Registrations
    
  23.                 where r.Book.Id.Equals(book.Id) &&
    
  24.                 r.RegistrationStatus == RegistrationStatus.Normal
    
  25.                 select r;
    
  26.         if (q.Count() > 0)
    
  27.         {
    
  28.             var reg = q.First();
    
  29.             if (reg.Expired)
    
  30.             {
    
  31.                 // TODO: Reader should pay for the expiration.
    
  32.             }
    
  33.             reg.ReturnDate = DateTime.Now;
    
  34.             reg.RegistrationStatus = RegistrationStatus.Returned;
    
  35.             book.Lent = false;
    
  36.         }
    
  37.         else
    
  38.             throw new InvalidOperationException(string.Format("Reader {0} didn't borrow this book.",
    
  39.                 this.Name));
    
  40.     }
    
  41. }
    
  42. 
    

 

业务逻辑的添加仍然是在我们新建的partial class中,这样做的目的就是为了不让Entity Framework的代码自动生成器覆盖我们手动添加的代码。相应地,我们在Book和Registration中实现各自的业务逻辑(具体请参见案例源代码)。从TinyLibrary.Domain这个Project上看,TinyLibrary.edmx定义了基于Entity Framework的领域模型,而其它的几个C#代码文件则使用部分类的特性,分别针对每个实体/聚合根实现了一些业务逻辑。

image

 

下一讲将详细介绍基于TinyLibrary领域模型与Entity Framework的仓储的实现方式。

转载至:http://www.cnblogs.com/daxnet/archive/2010/10/27/1862752.html

发表在 C# | 留下评论

领域驱动设计案例:Tiny Library:仓储

在领域驱动设计的案例中,仓储的设计是很具有争议性的话题,因为仓储这个角色本身就与领域模型和基础结构层对象相关,它需要序列化领域对象(应该说是聚合),然后将其保存到基础结构层的持久化机制。于是,在领域驱动设计的社区中,存在两种观点:

1、领域模型不能访问仓储,理由是:仓储需要跟技术架构层打交道,在领域模型中访问仓储就会破坏领域模型的纯净度。需要使用仓储的,需要在领域模型上加上一层,比如Application层,在该层中获取仓储实例并处理持久化逻辑

2、领域模型可以访问仓储,但仅仅是通过仓储接口和IoC容器访问仓储;仓储的具体实现通过IoC注入到领域模型中

其实,这只不过是个人习惯问题,我认为两种方法都可以接受,具体采用哪种方法,就要看具体项目的需求和实现情况而定。在Tiny Library中,由于业务简单,所以采取的是第一种方式,但上述的理由并不充分,换句话说,出于业务需求,我采用了第一种方式,但并不是因为仓储需要跟技术架构层打交道所以才把对仓储的访问放在Application层中。仓储也是领域模型的一部分,领域模型依赖于仓储的抽象。

在TinyLibrary中,仓储的实现仍然依赖Apworks Application Development Framework,因为Apworks已经为仓储、规约的实现搭好了架子,我们无需重新编写一套仓储的实现代码。

 

一、EdmRepository<TEntity>基类

此类继承于Apworks.Domain.Repositories.Repository<TEntity>类,它的主要任务是定义一个Entity Framework中的Object Context(在本案例中为TinyLibraryContainer),然后通过只写属性的方式,向Repository Transaction Context提供一个设置接口;另外,EdmRepository还实现了几个抽象方法,这样做是为了便于子类的实现(开发人员可以少写点重复代码)。

 

二、ReaderRepository和BookRepository子类

这两个类提供了基于Reader聚合和Book聚合的仓储实现。泛型约束分别绑定到Reader类和Book类(详情请参考源代码)。

 

三、EdmTransactionContext类

此类实现Apworks.Domain.Repositories.IRepositoryTransactionContext接口,目的是基于Entity Framework实现一个事务处理上下文。仓储将由此上下文创建获得。从此类的GetRepository泛型方法可以看到,仓储的实体是通过IoC容器获得的,IoC容器的配置位于TinyLibrary.Services项目的web.config文件中。

 

整个Repository的实现类图如下:

ClassDiagram1

 

四、TinyLibrary.Services项目app.config配置文件

打开TinyLibrary.Services项目的app.config配置文件,我们可以看到,Apworks采用Unity作为IoC容器:

隐藏行号 复制代码 app.config文件 – Apworks配置
  1. <apworksConfiguration>
    
  2.   <objectContainer provider="Apworks.IoC.Unity.UnityContainer, Apworks.IoC.Unity"/>
    
  3.   <modelAssemblies>
    
  4.     <modelAssembly name="TinyLibrary.DomainModel"/>
    
  5.   </modelAssemblies>
    
  6.   <caching>
    
  7.     <cached key="Infrastructure.ObjectContainer" active="true" cacheManager="cacheManager"/>
    
  8.     <cached key="Domain.Repository" active="true" cacheManager="cacheManager"/>
    
  9.   </caching>
    
  10. </apworksConfiguration>
    
  11. 
    

而Unity的配置部分,我们分别将ReaderRepository和BookRepository注册到了IRepository<Reader>和IRepository<Book>接口上:

隐藏行号 复制代码 app.config文件 – Unity配置
  1. <unity>
    
  2.   <containers>
    
  3.     <container>
    
  4.       <types>
    
  5.         <type type="Apworks.Domain.Repositories.IRepositoryTransactionContext, Apworks.Domain"
    
  6.               mapTo="TinyLibrary.Repositories.EdmTransactionContext, TinyLibrary.Repositories">
    
  7.         </type>
    
  8.         
    
  9.         <type type="Apworks.Domain.Repositories.IRepository`1[[TinyLibrary.Domain.Book, TinyLibrary.Domain]], Apworks.Domain"
    
  10.               mapTo="TinyLibrary.Repositories.BookRepository, TinyLibrary.Repositories"/>
    
  11.         
    
  12.         <type type="Apworks.Domain.Repositories.IRepository`1[[TinyLibrary.Domain.Reader, TinyLibrary.Domain]], Apworks.Domain"
    
  13.               mapTo="TinyLibrary.Repositories.ReaderRepository, TinyLibrary.Repositories"/>
    
  14.         
    
  15.         <type type="Apworks.Core.IIdentityGenerator, Apworks.Core"
    
  16.               mapTo="Apworks.Core.GeneralGuidGenerator, Apworks.Core"/>
    
  17.       </types>
    
  18.     </container>
    
  19.   </containers>
    
  20. </unity>
    
  21. 
    

五、关于项目依赖(Project Dependencies)

细心的读者会发现,TinyLibrary.Repositories引用了TinyLibrary.Domain项目,这是为了实现仓储的本职工作:持久化领域模型中的聚合。假设领域模型需要访问仓储,则无法直接引用TinyLibrary.Repositories,只能通过IoC/DI。事实上,TinyLibrary.Services项目根本没有去引用TinyLibrary.Repositories项目,因为TinyLibrary.Services并不需要去依赖仓储的具体实现方式,于是从逻辑上讲,不应该将TinyLibrary.Repositories项目添加到TinyLibrary.Services的引用中去。然而,TinyLibrary.Services的正常运行,是需要TinyLibrary.Repositories的支持的,这个在配置文件中已经体现出来了。为了开发方便,我对这些项目作了如下改动:

  1. 右键单击TinyLibrary.Repositories项目,选择Properties,在Build选项卡下的Output部分,将项目的编译输出指定到TinyLibrary.Services的bin目录下
    image
  2. 右键单击TinyLibrary.Services项目,选择Project Dependencies,在弹出的对话框中,勾选TinyLibrary.Repositories项目
    image

 

早在《EntityFramework之领域驱动设计实践(八)》一文中,我就介绍了仓储的实现方式,在那篇文章的最后给出了一幅图,描述了各个组件之间的关系,这种关系在Tiny Library的项目中也是适用的,在此再转贴一次,以示总结。

转载至:http://www.cnblogs.com/daxnet/archive/2010/10/28/1863843.html

发表在 C# | 留下评论

领域驱动设计案例:Tiny Library:应用服务层

Tiny Library使用应用服务层向用户界面层提供服务,具体实现是采用Microsoft WCF Services。在Tiny Library的解决方案中,是由TinyLibrary.Services项目为整个系统提供这一WCF服务的。按照传统的应用系统分层方法,TinyLibrary.Services项目位于领域模型层之上、用户界面层之下,它是UI与Domain的交互界面。TinyLibrary.Services的实现中,与DDD相关的内容主要是数据传输对象(DTO),至于如何编写与实现WCF服务,那是.NET技术上的问题,本文不会做太多的讨论。

 

数据传输对象(Data Transferring Object,DTO)

在TinyLibrary.Services中,有一种特殊类型的对象,我们称之为数据传输对象。根据Fowler在PoEAA一书中的描述,DTO用于进程间数据交换,由于DTO是一种对象,它能够包含很多数据信息,因此,DTO的采用可以有效减少进程间数据交换的来回次数,从而在一定程度上提高网络传输效率。

由于DTO需要跨越进程边界(在我们的案例中,需要跨越网络边界),因此,DTO是可以被序列化/反序列化的,它不能包含上下文相关的信息(比如,Windows句柄)。正因为如此,DTO中的数据类型都是很简单的,它们可以是原始数据类型(Primitive Data Types)或者是其它的DTO。

有些朋友在阅读Fowler的PoEAA一书时,对于DTO的理解还是有困难,我在此将我的理解写下来,供大家参考

  1. 领域模型对象不负责任何数据传输的功能,这是因为,如果将领域模型对象用于数据传输,则势必需要在另一个层面(或者另一个进程中)产生一个领域模型对象的副本,此时才有可能在服务器与客户机之间产生一种数据契约,而这种做法违背了层内高内聚,层间低耦合的原则
  2. DTO是简单而原始的,并且是运行时上下文无关的。这是为了满足序列化/反序列化的需求。因此,DTO所包含的数据要不就是原始数据类型,要不就是其它的DTO
  3. 客户端需求决定DTO的设计。因此,DTO与领域模型对象并非一一对应的关系,相反,它是为了满足客户端需求而存在的

在Tiny Library案例中,我选用了WCF的Data Contracts作为DTO的实现标准,因为这样做不仅能够基于客户端需求设计合理的DTO结构,而且还可以利用WCF的DataContractSerializer实现DTO的序列化/反序列化。不仅如此,WCF为我们在服务器与客户机通讯上提供了技术支持和有力保障。

打开TinyLibrary.Services项目,我们可以看到一个IDataObject的泛型接口,其定义如下:

隐藏行号 复制代码 IDataObject定义
  1. public interface IDataObject<TEntity> where TEntity : IEntity
    
  2. {
    
  3.     void FromEntity(TEntity entity);
    
  4.     TEntity ToEntity();
    
  5. }
    
  6. 
    

我们可以要求所有的DTO都实现这个接口,以便使其具有将实体转换为DTO,或者将DTO转换为实体的能力。在Tiny Library案例中,我并没有强制要求所有的DTO都实现这个接口(也就是说,Tiny Library本身不规定DTO必须实现这个接口),我引入这个接口的目的,就是想说明,其实我们是可以这样做的:在我们的系统中为DTO设计好一个合理的框架,以便让DTO获得更强大的功能。引入这个接口的另一个目的就是为了编程方便:实现基于某个实体的DTO,只需要使其实现IDataObject接口即可,Visual Studio会自动产生方法桩(Method Stubs),无需手动编写代码。

请注意TinyLibrary.Services.DataObjects.RegistrationData这个类,它与BookData、ReaderData有很大的区别,它并不是Registration实体的映射体现,换句话说,它所包含的所有状态属性,并不是与Registration实体所包含的属性一一对应。例如,RegistrationData这个DTO中包含书名(BookTitle)以及ISBN号(BookISBN)的信息,而这些信息都是来自于Registration的关联实体:Book。RegistrationData的设计完全是为了迎合用户界面,因为在UI上,我们需要针对某个读者列出他/她的借书信息,而RegistrationData包含了这所有需要的信息。

 

应用层职责

在《Entity Framework之领域驱动设计实践》系列文章中,我曾经提到过,根据DDD,应用系统分为四层:展现层、应用层、领域层和基础结构层。在Tiny Library案例中,TinyLibrary.Services充当了应用层的职责,它不负责处理任何业务逻辑,而是从更高的层面,为业务逻辑的正确执行提供适当的运行环境,同时起到任务协调的作用(比如事务处理和基础结构层服务调用)。

隐藏行号 复制代码 WCF Service中“还书”操作的具体实现
  1. public void Return(string readerUserName, Guid bookId)
    
  2. {
    
  3.     try
    
  4.     {
    
  5.         using (IRepositoryTransactionContext ctx = ObjectContainer
    
  6.             .Instance
    
  7.             .GetService<IRepositoryTransactionContext>())
    
  8.         {
    
  9.             IRepository<Book> bookRepository = ctx.GetRepository<Book>();
    
  10.             IRepository<Reader> readerRepository = ctx.GetRepository<Reader>();
    
  11.             Reader reader = readerRepository.Find(Specification<Reader>.Eval(r => r.UserName.Equals(readerUserName)));
    
  12.             Book book = bookRepository.GetByKey(bookId);
    
  13.             reader.Return(book);
    
  14.             ctx.Commit();
    
  15.         }
    
  16.     }
    
  17.     catch
    
  18.     {
    
  19.         throw;
    
  20.     }
    
  21. }
    
  22. 
    

上面的代码展示了“还书”操作的具体实现方式,我们可以看到,位于应用层的WCF Services仅仅是协调仓储操作和事务处理,业务逻辑由reader.Return方法实现:

隐藏行号 复制代码 TinyLibrary.Domain.Reader的Return方法
  1. public void Return(Book book)
    
  2. {
    
  3.     if (!book.Lent)
    
  4.         throw new InvalidOperationException("The book has not been lent.");
    
  5.     var q = from r in this.Registrations
    
  6.             where r.Book.Id.Equals(book.Id) &&
    
  7.             r.RegistrationStatus == RegistrationStatus.Normal
    
  8.             select r;
    
  9.     if (q.Count() > 0)
    
  10.     {
    
  11.         var reg = q.First();
    
  12.         if (reg.Expired)
    
  13.         {
    
  14.             // TODO: Reader should pay for the expiration.
    
  15.         }
    
  16.         reg.ReturnDate = DateTime.Now;
    
  17.         reg.RegistrationStatus = RegistrationStatus.Returned;
    
  18.         book.Lent = false;
    
  19.     }
    
  20.     else
    
  21.         throw new InvalidOperationException(string.Format("Reader {0} didn't borrow this book.",
    
  22.             this.Name));
    
  23. }
    
  24. 
    

 

配置文件

TinyLibrary.Services是整个案例的服务供应者(Service Provider),因此,整个系统服务器端的配置与初始化应该由TinyLibrary.Services启动时负责执行。因此,基于服务器的系统配置应该写在TinyLibrary.Services项目的app.config中。其中包括:Apworks的配置、Unity(或者Castle Windsor)的配置、WCF Services的配置以及Entity Framework所使用的数据库连接字符串的设置。

从实践角度考虑,在基于CQRS体系结构模式的应用系统中,各个组件的初始化和配置逻辑应该位于WCF Services的Global.asax文件中,以便在WCF Service Application启动的时候,所有组件都能成功地初始化。我将在今后的CQRS案例中进一步描述这一点。

 

至此,Tiny Library的服务端部分基本介绍完毕,回顾一下,这些内容包括:领域建模、仓储实现和应用服务层。下一讲将简单介绍一下Tiny Library的Web界面的设计与开发。由于本系列文章的重点不是讨论某个技术的具体实现,因此,在下一讲中不会涉及太多有关ASP.NET MVC的细节内容。

转载至:http://www.cnblogs.com/daxnet/archive/2010/11/02/1866797.html

发表在 C# | 留下评论

领域驱动设计系列文章汇总

今天抽空将我写的与领域驱动设计有关的系列文章汇总于此,便于大家查看。根据撰写的进度,本列表会不定期更新,敬请关注!

Entity Framework之领域驱动设计实践

领域驱动设计案例:Tiny Library

CQRS体系结构模式实践案例:Tiny Library

使用Apworks开发基于CQRS架构的应用程序

Microsoft NLayerApp案例理论与实践

转载至:http://www.cnblogs.com/daxnet/archive/2010/11/02/1867392.html

发表在 C# | 留下评论

领域驱动设计案例:Tiny Library:用户界面

工作繁忙,很久没有更新博客了。下面言归正传,简单介绍一下Tiny Library的用户界面实现。

如前所述,Tiny Library采用ASP.NET MVC框架实现了基于浏览器的用户界面。回顾一下《业务逻辑与系统结构》一文,从层次架构图中我们可以清楚地看到,用户界面层是通过WCF Services与系统交互的。

为了快速简单地实现Tiny Library,我只是简单地使用了ASP.NET MVC,因此没有对其做更深入的挖掘,有关ASP.NET MVC的更多知识,请朋友们自行上网搜索或阅读相关书籍。

这里需要说明的问题有两点:用户认证与授权,以及MVC中的M。

用户认证与授权(Authentication & Authorization)

ASP.NET MVC采用基于Forms的认证机制(Forms Authentication),因此Tiny Library也继承了这种方式。事实上,使用ASP.NET MVC的这种认证机制,会在站点的App_Data目录下产生一个ASPNETDB.MDF的数据库文件,用来保存Authentication相关信息,比如某个用户注册了,就会在这个数据库中产生相关信息。然而,我们的Tiny Library也有一套自己的数据库,该库中需要保存用户的相关信息(比如Reader的名字等),目前我是在AccountController中,Register用户的时候,同时在Tiny Library中同步一条数据,代码如下:

隐藏行号 复制代码 Register User
  1. MembershipCreateStatus createStatus = MembershipService.CreateUser(model.UserName, model.Password, model.Email);
    
  2. 
    
  3. if (createStatus == MembershipCreateStatus.Success)
    
  4. {
    
  5.     TinyLibraryServiceClient svcClient = new TinyLibraryServiceClient();
    
  6.     bool ret = svcClient.AddReader(new ReaderData
    
  7.     {
    
  8.         Id = Guid.NewGuid(),
    
  9.         Name = model.Name,
    
  10.         UserName = model.UserName
    
  11.     });
    
  12.     svcClient.Close();
    
  13.     if (ret)
    
  14.     {
    
  15.         FormsService.SignIn(model.UserName, false /* createPersistentCookie */);
    
  16.         return RedirectToAction("Index", "Home");
    
  17.     }
    
  18.     else
    
  19.     {
    
  20. 
    
  21.     }
    
  22. }
    
  23. else
    
  24. {
    
  25.     ModelState.AddModelError("", AccountValidation.ErrorCodeToString(createStatus));
    
  26. }
    
  27. 
    

 

这样做其实很不保险,比如,如果在Tiny Library数据库中添加用户信息失败,那么虽然用户成功注册(提交ASPNETDB.MDF成功),也无法正常使用系统。当然,Tiny Library只是个演示系统,如果是在实际应用中,这个问题还需要通过其他途径解决。我目前没有想到万全之策,欢迎热心的朋友在本文的留言中给出建议。

 

MVC中的M(Model)

有不少使用ASP.NET MVC的朋友,总会将Domain Model带到用户界面层,然后直接将Domain Model中的实体用作MVC中的M。在Fowler的PoEAA一书的DTO一章已经说过,Domain Model是不能暴露给高层的,因此将Domain Model中的实体用于MVC是不合理的。

其实如果你有了解过CQRS体系结构模式,你就会了解到,MVC中的M,应该是Presentation Model,这个Presentation Model与领域毫无关系,仅仅用于界面数据绑定。在MVC Web Application添加了WCF的Service Reference之后,当你创建View的时候,你就可以很方便地将客户端产生的Data Contract用作Data Class,如下:

image

其实,这里列出来的BookData、ReaderData和RegistrationData,就是Model。

OK,用户界面就写这么些吧,也没有采用什么特别的技术,主要还是使用ASP.NET MVC上的这几个问题。有关ASP.NET MVC的内容,朋友们请自己查阅相关资料吧!

转载至:http://www.cnblogs.com/daxnet/archive/2010/11/18/1880949.html

发表在 C# | 留下评论

CQRS体系结构模式实践案例:Tiny Library:简介

写在前面

有段时间没有更新博客了,一方面因为工作繁忙,另一方面则是我最近一直在坚持设计和完善基于DDD的应用系统开发框架Apworks。读过我《领域驱动设计案例:Tiny Library》这一系列文章的朋友一定听说过Apworks框架,虽然这个框架至今仍未成熟到能够用在真正的系统开发上,但它已经实现了CQRS体系结构模式,已经可以用于软件架构设计的演示上了。从这一讲开始,我将逐步介绍如何采用CQRS架构实现Tiny Library的业务。你可能会听得烦了:又是Tiny Library,能不能换点别的?呵呵,我开始时想做一个简单的论坛出来,不过为了能让读者朋友在经典DDD实践和CQRS实践上做个对比,我决定继续使用Tiny Library的业务。

 

扩展阅读

在阅读本系列文章之前,如果您对领域驱动设计(DDD)、命令与查询职责分离(CQRS)、事件溯源(Event Sourcing)、事件存储(Event Store)、WCF、ASP.NET MVC等概念和技术不了解的话,请自己先对这些内容做个了解。这里我给出一些链接,希望能对您有所帮助。

下载案例

请读者朋友到http://tlibcqrs.codeplex.com站点下载源代码。单击进入站点首页,然后在Source Code选项卡下,选择最新的Change Set,在打开的Change Set页面中,单击Download按钮下载源代码包。

 

系统需求

  • Microsoft .NET Framework 3.5 SP1
  • Microsoft Patterns&Practices Enterprise Library 5.0 (April 2010)
  • Microsoft SQL Express 2008
  • Microsoft Visual Studio 2010 (打开解决方案时需要VS2010)
  • Apworks DDD framework (http://apworks.codeplex.com)。 Apworks的程序集已经被包含在Tiny Library CQRS的源代码包中,您无需单独下载Apworks

注意:目前Apworks框架只能用于演示,仍在开发中,请不要用于实际项目!

 

安装部署

请按下列步骤安装部署Tiny Library CQRS:

  • 打开Microsoft Visual Studio 2010
  • 打开Server Explorer
  • 邮件单击Data Connections, 选择Create New SQL Server Database option
  • Create New SQL Server Database对话框中,在Server name中输入.\SQLEXPRESS,然后在 New database name 中输入 TinyLibraryEventDB
  • 使用上面同样的方法创建另一个数据库,取名为TinyLibraryQueryDB
  • 在Microsoft Visual Studio 2010中,打开 TinyLibraryCQRS 解决方案
  • Additions 目录下,执行 TinyLibraryEventDB.sql 和 TinyLibraryQueryDB.sql 脚本
  • 编译整个解决方案 
  • Solution Explorer上, 邮件单击 TinyLibrary.Services.CommandService.svc 文件, 选择 View in Browser, 这将启动 ASP.NET Development Server,端口号是1421
  • 将TinyLibrary.WebApp项目设置成启动项目,然后按下CTRL+F5以启动应用程序

运行案例应用程序

启动应用程序后,将出现如下界面:

image

请使用默认的账户登录系统:用户名:daxnet,密码:123456

 

从下一讲开始,我将详细介绍Tiny Library CQRS的体系结构和设计思路。敬请期待!

转载至:http://www.cnblogs.com/daxnet/archive/2010/12/14/1905602.html

发表在 C# | 留下评论

CQRS体系结构模式实践案例:Tiny Library:系统架构

写在前面

也许在阅读了上篇文章中我列出的那部分资料后,还是有很多朋友对领域驱动设计不了解。正如上文评论中有网友提到微软西班牙团队也做了一个面向领域的分布式应用系统架构的案例,地址是http://microsoftnlayerapp.codeplex.com/。在这个站点的首页上,又对领域驱动设计做了诠释,我觉得总结的很好,特地将其翻译成中文写在这里,供大家参考:

DDD is much more than this!

We’re talking about complex apps…, all their business rules (Domain logic) are points that, in most of the cases, need to be changed quite a lot during the app’s life. So it is critical to be able to change the app and test it in an easy way and independently from infrastructure areas (technology, data stores, data access technolgies, ORMs, etc.). Because of that, it is important to have the Domain/Business Layer (domain logic, entities, etc.) decoupled from the other layers.
Having said so, in our opinion, the best actual architectural style that fits with those requirements is a Domain Oriented N-Layered Architecture which is part of DDD (Domain Driven Design). And this Sample-app is showing a way to implement a Domain Oriented N-Layered Architecture.
But, and this is extremely important, DDD is on the other hand, much more than just a proposed Architecture and patterns. DDD is a way to build apps, a way for the team, to work in projects. According to DDD, the project’s team should work in a specific way, should have direct communication with the Domain Experts (the customer, in many cases). The team should use an ‘Ubiquitous Language’ which has to be the same language/terms used by the domain experts, etc. But, all those ways to work are not part of this Sample-App, because it is process, kind of ALM, so, if you want to really “Do DDD”, you’ll need to read Eric-Evans’ book or any other DDD book where they talk about the DDD process, about the project and the team, not just the Architecture and patterns. Architecture and patterns are just a small part of DDD, but, in this case those are the points we are showing here (DDD architecture and patterns). But we want to highlight that DDD is much more than Architecture and design patterns.

 

DDD – 不仅仅是这些

我们讨论的是复杂的应用系统……,通过这些应用系统的业务规则(领域逻辑)我们可以了解到,在大多数情况下,在应用系统的整个生命周期里,业务规则是可变的一部分。因此,能够很方便地对应用系统进行变更并测试,并使其独立于基础结构层设施(比如:技术、数据存储、数据访问技术、ORM等等),就显得非常重要。正因为如此,我们很有必要将领域/业务层(领域逻辑、实体等等)从其它分层中解耦出来。

话虽如此,根据我们的建议,能够适应上述需求的架构风格就是面向领域的多层架构,但它仅仅是领域驱动设计(DDD)的一个部分。本案例展示了实现面向领域的多层架构的一种方式。但需要注意的,而且非常重要的是,DDD不仅仅只是架构+模式。DDD是开发应用程序的一种方式,是团队在项目中工作的一种方式。根据DDD,项目团队需要以一种特殊的方式进行合作,应该能够直接与领域专家(通常就是客户)进行沟通。整个团队需要使用“通用语言”这一能够被所有人接受的语言,等等。然而,本案例没有包含这些内容,因为这是一种“过程”,一种ALM。所以,如果你真的希望100%实践DDD,你需要阅读Eric Evans写的《领域驱动设计-软件核心复杂性应对之道》一书,或者其它的一些讨论DDD过程的书籍,这些书籍会对DDD过程、项目和团队做比较详细的介绍,而不仅仅是谈论架构和模式。架构和模式仅仅是DDD中很小的一部分,而我们在这里展示的就恰好是这一部分。总之,我们必须强调,DDD不仅仅是架构+模式。

很多网友对什么是DDD有很大争议,我想,看了上面的文字,问题应该大致清楚了。也在此借用这段文字,说明我研究学习DDD实践的意图。言归正传,本文将对Tiny Library CQRS(简称tlibcqrs,下同)的解决方案结构和系统架构做个简要的介绍。

 

Tiny Library CQRS解决方案结构

image

从Solution Explorer上可以看到,tlibcqrs包含9个项目,依次为:

  • TinyLibrary.CommandHandlers:命令处理器,负责处理来自客户端的命令请求
  • TinyLibrary.Commands:命令集,其中包含了tlibcqrs所用到的命令,客户端请求将转化为命令,并由命令处理器负责处理执行
  • TinyLibrary.Domain:领域模型,业务逻辑与之前系列文章中的Tiny Library一样,话不多说。不过你会发现,tlibcqrs的领域模型与经典Tiny Library的领域模型在实现上会有区别
  • TinyLibrary.EventHandlers:领域事件处理器,负责处理由领域仓储转发的领域事件
  • TinyLibrary.Events:领域事件集,其中包含了tlibcqrs中所有的领域事件。就是这些领域事件造成了领域模型中实体的状态更新
  • TinyLibrary.QuerObjects:查询对象集,负责数据的查询和传递,它是用户界面的数据映射,是一些DTO
  • TinyLibrary.Services:.NET WCF Services,包含两个endpoint:CommandService和QueryService,为用户界面层提供服务
  • TinyLibrary.Tests:单体测试项目
  • TinyLibrary.WebApp:基于ASP.NET MVC实现的用户界面

仍然说明一下,本案例没有加入太多的cross-cutting基础结构层设施,因此,诸如日志、缓存、异常处理都很弱甚至没有,朋友们不要觉得好奇,这只是一个演示,为了节约时间,我忽略了这部分内容。今后我会将其补上。

 

系统架构

tlibcqrs的系统架构就是经典的CQRS架构,之前在我的《EntityFramework之领域驱动设计实践【扩展阅读】:CQRS体系结构模式》一文中,我已经给出了整个系统的结构简图,在此再一次贴出来,以表示tlibcqrs的系统架构。

 

为了让大家看tlibcqrs源代码方便,在此特地以“创建读者”为例,将sequence描述一下。

  • Client通过TinyLibrary.Services.CommandService,向系统发送CreateReader的请求
  • CommandService获得请求,创建RegisterReaderCommand对象,并将其推送到命令总线(Command Bus)
  • 在Command Bus被提交时,它将调用RegisterReaderCommandHandler,以处理RegisterReaderCommand
  • 在RegisterReaderCommandHandler中,创建新的Reader实体,并向其发送ReaderCreatedEvent事件
  • Reader实体捕获到ReaderCreatedEvent后,根据事件中的数据,设置其自身的状态属性,同时将事件记录下来
  • RegisterReaderCommandHandler调用Domain Repository保存新建的Reader实体
  • Domain Repository从Reader实体中读取未提交的事件列表,使用DomainEventStorage将事件保存到外部持久化机制(在tlibcqrs中,是TinyLibraryEventDB这个关系型数据库)
  • 完成事件的保存后,Domain Repository将事件推送到事件总线(Event Bus),然后返回
  • 事件总线接收到事件后,使用消息派发器(Message Dispatcher)将事件发送给相应的事件处理器进行处理
  • ReaderCreatedEventHandler在接到ReaderCreatedEvent事件后,将事件数据保存到查询存储机制中(在tlibcqrs中,是TinyLibraryQueryDB这个关系型数据库),同时返回
  • 下一轮,当用户需要获取Reader信息时,直接通过TinyLibrary.Services.QueryService读取查询存储机制以获得数据

大致可以用下面的UML Sequence Diagram描述这一过程【请单击此处下载XPS文件以查看大图】:

image

 

下一讲,我会介绍一些tlibcqrs中的一些新思想,至于CQRS框架的设计,我会在另外的系列文章中详述。

转载至:http://www.cnblogs.com/daxnet/archive/2010/12/15/1906376.html

发表在 C# | 留下评论

CQRS体系结构模式实践案例:Tiny Library:对象的行为和状态

从结构上看,tlibcqrs项目并不复杂,但对其进行介绍,的确让我感到有点无从着手。还是先从领域模型中的对象的行为和状态谈起吧。

先来谈谈对象状态。据我理解,状态就是一种数据,它用来描述,在某个特定的时间上,这个对象所具有的特质,它将作为对象行为发生的依据和结果。我们平时做设计和编程的时候,尤其是在做数据访问层的时候,特别喜欢一些仅仅包含getter/setter属性的对象,以便调用方能够通过getter获得对象的状态,使用setter设置对象的状态。之前我也说明过,状态并非getter/setter属性,在OOP上,状态表现为“字段”(fields)。现在我们讨论的不是数据访问层的DAO,而是领域模型中的实体。当然,实体也是对象,自然也有状态,不仅仅是状态,实体是参与业务逻辑的重要对象,它还有处理业务逻辑的行为。

现在假设我们有个实体为Customer,它同时也是某个聚合的聚合根,在通常情况下,我们会用下面的形式去定义这个Customer实体(为了简化,省去了对象行为):

image

当然你不会觉得这样设计有什么太大的问题,事实上在我们平时的开发中,也的确是这么做的,而且非CQRS架构的DDD实践也支持这样的实体模型。于是,我们可以使用下面的代码来更新某个Customer的姓名:

   1: [TestMethod]

   2: public void ChangeCustomerNameTest()

   3: {

   4:     Customer customer = new Customer

   5:     {

   6:         Birth = DateTime.Now.AddYears(-20),

   7:         Email = "daxnet@live.com",

   8:         FirstName = "dax",

   9:         LastName = "net",

  10:         Password = "123456",

  11:         Username = "daxnet"

  12:     };

  13:     using (IRepositoryContext ctx = ObjectContainer.Instance.GetService<IRepositoryContext>())

  14:     {

  15:         IRepository<Customer> customerRepository = ctx.GetRepository<Customer>();

  16:         customerRepository.Add(customer);

  17:     }

  18:     ISpecification<Customer> spec = Specification<Customer>.Eval(p => p.Username.Equals("daxnet"));

  19:     using (IRepositoryContext ctx = ObjectContainer.Instance.GetService<IRepositoryContext>())

  20:     {

  21:         IRepository<Customer> customerRepository = ctx.GetRepository<Customer>();

  22:         var customer2 = customerRepository.Get(spec);

  23:         Assert.AreEqual(customer.Username, customer2.Username);

  24:  

  25:         customer2.FirstName = "qingyang";

  26:         customer2.LastName = "chen";

  27:         customerRepository.Update(customer2);

  28:     }

  29:     using (IRepositoryContext ctx = ObjectContainer.Instance.GetService<IRepositoryContext>())

  30:     {

  31:         IRepository<Customer> customerRepository = ctx.GetRepository<Customer>();

  32:         var customer3 = customerRepository.Get(spec);

  33:         Assert.AreEqual("qingyang", customer3.FirstName);

  34:         Assert.AreEqual("chen", customer3.LastName);

  35:     }

  36: }

在上面代码段的25行和26行,我们直接设置了Customer的姓名,然后在27行调用仓储进行实体更新。一切都非常顺利。今后,实体中的属性,就成为了我们实现业务逻辑和实体行为的依据。然而,CQRS体系结构模式的实践就与这种方式大相径庭。CQRS是一种事件驱动的架构(Event Driven Architecture,EDA),它的设计与实践需要遵循事件驱动的基本特征。

 

事件驱动架构下的对象状态

事件驱动架构是一种体系结构模式,它对事件的整个生命周期进行检测、跟踪和管理,并对事件的产生与发展做出反应。“事件”,可以定义为“造成状态变化的信号”。比如:当用户买了一辆轿车,那么,这辆车的状态就从“等待销售”转变为“已出售”。汽车销售系统就会将此作为一种事件将其发布到事件总线,以便应用程序的其它系统或组件能够订阅到这个事件并做进一步的处理(请参见维基百科:http://en.wikipedia.org/wiki/Event_driven_architecture)。

从现在开始,我们需要重新认识对象状态的变化。由于EDA的引入,对象状态只能通过事件的发生而产生变化,外界不能无缘无故地对其进行改变(就像上面的例子一样)。这样做的好处是:我们不仅能够知道对象的当前状态,而且还能知道,到底发生了哪些事情,才使对象变成现在这副“模样”。当某一事件发生时,对象捕获到这一事件并对其进行处理,而在处理的过程中再根据事件的类型和数据来改变自己的状态。由于通常情况下,这样的处理过程是对象的内部行为,因此,我们也就无需将更改状态的接口暴露给外部。在理解了这部分内容后,我们的Customer对象的设计就需要做出修改,变成下面这种形式(为了讨论方便,在此将Customer实体命名为SourcedCustomer,意为支持事件溯源):

   1: public class SourcedCustomer : SourcedAggregateRoot

   2: {

   3:     public virtual string Username { get; private set; }

   4:     public virtual string Password { get; private set; }

   5:     public virtual string FirstName { get; private set; }

   6:     public virtual string LastName { get; private set; }

   7:     public virtual string Email { get; private set; }

   8:     public virtual DateTime Birth { get; private set; }

   9: }

将setter定义为private,以防止外界直接修改对象状态。对于某些外部也不关心的状态,我们甚至连getter都可以省去(也就是不需要再实现为property了),取而代之的是一个private的字段。比如:

   1: public class SourcedCustomer : SourcedAggregateRoot

   2: {

   3:     private DateTime dayOfBirth;

   4:     // ....

   5: }

 

对象的行为导致状态变化

现在回到tlibcqrs项目,让我们看看TinyLibrary.Domain下Book实体的实现方式。它的状态是一系列的public getter和private setter的自动实现的属性。Book实体状态的改变,是通过其行为实现的。当某个行为被外界调用时,行为本身会产生一个事件,而对象本身又去处理这个事件,从而导致状态变化。

   1: public class Book : SourcedAggregateRoot

   2: {

   3:     public string Title { get; private set; }

   4:     public string Publisher { get; private set; }

   5:     public DateTime PubDate { get; private set; }

   6:     public string ISBN { get; private set; }

   7:     public int Pages { get; private set; }

   8:     public bool Lent { get; private set; }

   9:  

  10:     public Book() : base() {  }

  11:     public Book(long id) : base(id) {  }

  12:  

  13:     public static Book Create(string title, string publisher, DateTime pubDate, string isbn, int pages, bool lent)

  14:     {

  15:         Book book = new Book();

  16:         book.RaiseEvent<BookCreatedEvent>(new BookCreatedEvent

  17:         {

  18:             Title = title,

  19:             Publisher = publisher,

  20:             PubDate = pubDate,

  21:             ISBN = isbn,

  22:             Pages = pages,

  23:             Lent = lent

  24:         });

  25:         return book;

  26:     }

  27:  

  28:     public static Book Create(long id, string title, string publisher, DateTime pubDate, string isbn, int pages, bool lent)

  29:     {

  30:         Book book = new Book(id);

  31:         book.RaiseEvent<BookCreatedEvent>(new BookCreatedEvent

  32:         {

  33:             Title = title,

  34:             Publisher = publisher,

  35:             PubDate = pubDate,

  36:             ISBN = isbn,

  37:             Pages = pages,

  38:             Lent = lent

  39:         });

  40:         return book;

  41:     }

  42:  

  43:     public void LendTo(Reader reader)

  44:     {

  45:         this.RaiseEvent<BookLentEvent>(new BookLentEvent { ReaderId = reader.Id, LentDate = DateTime.Now });

  46:     }

  47:  

  48:     public void ReturnBy(Reader reader)

  49:     {

  50:         this.RaiseEvent<BookGetReturnedEvent>(new BookGetReturnedEvent { ReaderId = reader.Id, ReturnedDate = DateTime.Now });

  51:     }

  52:  

  53:     #region Domain Event Handlers

  54:     [Handles(typeof(BookCreatedEvent))]

  55:     private void OnBookCreated(BookCreatedEvent evnt)

  56:     {

  57:         this.Title = evnt.Title;

  58:         this.Publisher = evnt.Publisher;

  59:         this.PubDate = evnt.PubDate;

  60:         this.ISBN = evnt.ISBN;

  61:         this.Pages = evnt.Pages;

  62:         this.Lent = evnt.Lent;

  63:     }

  64:     [Handles(typeof(BookLentEvent))]

  65:     private void OnBookLent(BookLentEvent evnt)

  66:     {

  67:         this.Lent = true;

  68:     }

  69:     [Handles(typeof(BookGetReturnedEvent))]

  70:     private void OnBookReturnedBack(BookGetReturnedEvent evnt)

  71:     {

  72:         this.Lent = false;

  73:     }

  74:     #endregion

  75: }

上面的代码就是TinyLibrary.Domain.Book实体,我们可以看到,在Book被创建的时候,会产生BookCreatedEvent事件,当Book被借出时,会产生BookLentEvent事件,当Book被归还时,又会产生BookGetReturnedEvent事件。而这些事件会被OnBookCreated、OnBookLent和OnBookReturnedBack私有方法捕获,从而引起对象状态的变化。

不仅如此,SourcedAggregateRoot基类会将这些事件记录下来。领域仓储(Domain Repository)在保存聚合的时候,就只需要保存这一系列事件就可以了。这也是为什么在tlibcqrs项目中有一个TinyLibraryEventDB的数据库,而这个数据库却只有一张DomainEvents数据表的原因。由于实体本身能够通过捕获并处理事件来恢复状态,因此,通过事件回放即可重塑实体。当然,在领域仓储保存聚合的同时,这些事件也会被推送到事件总线,以便系统的其它部分能够对这些事件进行处理。

有关领域仓储(Domain Repository)和事件存储(Event Store)相关的内容和疑问,将在下一讲中进行讲解,敬请期待!

转载至:http://www.cnblogs.com/daxnet/archive/2010/12/22/1913745.html

发表在 C# | 留下评论

发布Apworks应用开发框架(Alpha版本)

Tiny Library CQRS的介绍文章有好些日子没有更新了,因为最近一直在忙着发布Apworks应用开发框架。原本打算在2011年1月1日发布,以迎接新年的到来,后来确定了还是在2010年12月31日发布,就算是给过去的一年做个留念。哈哈。

一直关注我的博客的园友都知道,之前我的一些领域驱动设计的案例,都是以Apworks为基础的。由于时间关系,Apworks一直没有一个固定的版本,所以在那些案例中,我都是将Apworks的程序集加入了案例的发布包里。现在,终于能够为Apworks整出一个“可用”的版本,并将其发布到了codeplex上,地址是:http://apworks.codeplex.com。版本:Alpha(v1.0.4016.23016)。有关Apworks的所有文档,我都上传到了http://apworks.org/documents.aspx。不过目前只有一篇文章,就是:Walkthrough – Building Applications with Apworks,它也同时被包含在了Apworks Alpha版本的安装包里。

 

先决条件

如果你希望使用Apworks进行开发,请确保你的机器满足如下先决条件:

  • .NET Framework 3.5 SP1
  • Visual Studio 2010
  • Microsoft Patterns & Practices Enterprise Library 5.0 (April 2010)
  • Microsoft SQL Server (Express) 2005或更高版本

下载Apworks Alpha安装包

请点击下面的链接下载安装包,在下载开始之前,你需要同意LGPL 2.1许可证协议,否则下载不会继续。

单击此处下载Apworks Alpha 1.0.4016.23016

 

关于Alpha版本

在Release Notes里面,我列出了Alpha版本功能的缺失点,虽然Apworks没有这些功能点照样能够成为一个独立而且完整的组件,但它目前仍然不适于用在实际的项目开发中,尤其是那些非常关键的应用系统中。在将来的Beta版中,我会把功能缺失点加上,使其能够真正支撑一个应用系统的设计与开发。现在,我再把Alpha版本的功能缺失点介绍一下。

  1. Alpha版本仅支持Direct Message Bus,这种消息总线使用共享内存存储消息,因此,应用程序不得不将Command端和Query端设计在同一台物理服务器上(因为Command端需要向Query端的事件总线发布事件通知,而Direct Message Bus却不支持远程的内存操作)
  2. 事件存储与快照:Alpha版本仅支持基于SQL Server关系型数据库的事件存储与快照机制。当然,开发人员可以自己定制。快照策略也非常简单:每当事件存储去保存第1000个事件的时候,就会针对该聚合做一次快照。这个逻辑是被定义在SqlDomainEventStorage类的CanCreateOrUpdateSnapshot方法里的,而这个方法本来是定义在IDomainEventStorage接口里的。如果你希望使用Alpha版本的Apworks,并且需要定制快照策略,那么你可以继承SqlDomainEventStorage类,并重写CanCreateOrUpdateSnapshot方法。对于其他类型数据库或存储机制的支持,将在今后的版本中发布
  3. 查询数据库:目前也仅支持SQL Server
  4. 对象IoC容器:目前仅提供对Microsoft Unity的接口支持,下个版本中会支持Castle Windsor(不过这个优先级不是很高)
  5. PropertyBag仅仅支持非常简单的条件组合:aaa=xxx AND bbb=yyy。不支持OR,也不支持各种其它的不等运算符。打算在下个版本中用Expression替换掉

关于安装包

在制作安装包以前,一直在想,该用什么工具来制作安装包。InstallShield虽然强大,但是太繁琐而且不是免费的;InnoSetup也不贴心,与.NET整合不够紧密;于是选择了WiX。选择WiX的原因是:它是.NET下原生的安装制作工具,能够直接集成到Visual Studio里,而且小巧简单(注意:不是简洁!),此外,我们项目组也正好是用的WiX,正好拿来练手。当然WiX也有它的缺点:繁琐(需要手动维护XML文件,而且定制用户界面也不方便),编译速度也不是很快。不过它的小巧确实让人感到清爽。

image

 

关于Tiny Library CQRS

如今,开源的CQRS案例项目:Tiny Library CQRS(http://tlibcqrs.codeplex.com)已经采用了Alpha版本的Apworks了,在CommandHandler、EventHandler等方面作了些实现上的修正。在下载并编译Tiny Library CQRS之前,请先下载并安装Apworks Alpha。如果你的Apworks不是装在C:\Program Files目录下,那么你需要在Tiny Library CQRS的项目中重新添加对Apworks的引用。

 

No Framework!

框架是需要去遵循的,这样才能利用框架带来的便捷来解决实际问题。然而,软件并不是想像的那么简单。业务千差万别,而特定业务下的需求又是千变万化,这又怎么可以用一个框架去限定应用系统的开发呢?确实是如此。虽然Apworks是一个框架,它能够实现基于CQRS体系结构的应用程序,但是并非大部分的(甚至所有的)应用系统开发都需要去使用Apworks框架,都需要去往CQRS体系结构模式上套。经典的应用系统架构模式在目前仍然很流行,仍然有很多成功的案例采用的是经典的系统架构模式。这种架构模式大致可以用下面的视图描述(此图摘自Greg Young的CQRS Documents一文):

image

这样的架构首先是简单:它比较通用,近80%的项目都可以用这种结构进行设计,而且对于开发人员来说,非常容易理解,因此门槛也比较低;正由于它简单通用,于是,就会有一系列的工具来支持基于这种结构的应用程序的开发(比如ORM),大大提高了生产率(摘自Greg Young的CQRS Documents一文)。所以,我还是那句话:从需求出发,来决定你的应用系统架构风格,进而做技术选型。上周跟朋友聚会,跟他们聊起了CQRS模式,他们都倍感惊讶,惊讶关系型数据库居然会沦落到“可有可无”的地步。有一位朋友问我:如果我发现我的系统不适合选用CQRS的风格,该怎么办呢?我说:那就不要CQRS了,因为它不适合你!所以,我想No Framework的更深层的含义应该就在于此:不要因为Framework而影响你对系统需求和架构风格作出的判断,不要因为Framework而去生搬硬套。

转载至:http://www.cnblogs.com/daxnet/archive/2010/12/31/1922809.html

发表在 C# | 留下评论

使用LINQ Expression构建Query Object

这个问题来源于Apworks应用开发框架的设计。由于命令与查询职责的分离,使得基于CQRS体系结构风格的应用系统的外部存储系统的结构变得简单起来:在“命令”部分,简单地说,只需要Event Store和Snapshot Store来保存Domain Model;而“查询”部分,则又是基于事件派送与侦听的系统集成。之前我也提到过,“查询”部分由于不牵涉到Domain Model,于是,它的设计应该随意性很大:不需要受到Domain Model的牵制,例如,我们可以根据UI所需的数据源结构进行“查询”库的设计。Greg Young在他的“CQRS Documents”一文中也提到了这样一些相关话题:就“查询”部分而言,虽然也存在“阻抗失衡”效应,但是事件模型与关系模型之间的这种效应,要远远小于对象模型与关系模型之间的“阻抗失衡”效应。这是因为,事件模型本身没有结构,它仅仅表述“该对关系模型做哪些操作”这样的概念。在设计上,Greg Young建议,采用一种非正规的方式设计“查询”数据库,以便尽量减少读取数据时所需的JOIN操作,比如可以选用基于第一范式(1NF)的关系模型。这是一种反范式模型,虽然Greg Young并没有建议根据UI所需的数据源结构进行设计,但思想是相同的:基于事件而不拘泥于对象模型本身。由此引申出来的另一个好处就是外部存储系统架构的随意性:你可以选用任何存储技术和存储媒介,这又给基于系统架构的性能优化提供了便利。因为并非所有的存储架构都支持“表”、“字段”、“存储过程”、“JOIN”这些概念。

根据上面的简单分析,我们得到一个结论:通常情况下,或许基于CQRS体系结构风格的应用系统更多的是采用的“平整”的外部存储结构,简而言之,就是一个数据访问对象(DAO)对应一张数据表。这也是我所设计的Apworks应用开发框架中默认支持的一种存储结构。有读过Apworks源代码的朋友会发现,在Apworks.Events.Storage命名空间下,有两个定制的DAO:DomainEventDataObject,用于表述领域事件的数据结构,以及SnapshotDataObject,用于表述快照的数据结构,与之相对应的就是数据库中的两张表:DomainEvents和Snapshots。虽然结构变得这么简单,但是映射关系总还是需要维护的:最简单的就是需要在对象类型名称与数据表名之间,以及对象属性与数据表字段之间建立起映射关系。在Apworks中,这种映射关系是由Apworks.Storage.IStorageMappingResolver接口完成的。有关这个接口的内容不是本文讨论的重点,暂且不深入分析了。

至此,也许你不会接受我上面的讨论,认为“基于UI设计数据库结构”或者“采用1NF、反范式设计数据库结构”是无法接受的,那么,接下来的讨论可能对你来说意义也不大了。因为下面的问题是以上面的描述为基础的:一个数据访问对象对应一张数据表。不过即使你不认同我的观点,我也建议你继续看完本文。

 

Query Object模式

虽然只是简单的映射,但毕竟不能忽略这样的映射关系。Apworks作为一个应用开发框架,需要提供方便的整合接口,以便今后能够根据不同的客户需求进行扩展。例如在存储部分,数据的增删改查(CRUD)是基于数据访问对象(DAO)的,这样做的一个好处是能够对外部存储系统进行抽象,使得访问存储系统的部分能够无需关系存储系统的细节问题。客户有可能选择SQL Server、Oracle、MySQL等关系型数据库作为存储系统,也可以选择其它的非关系型数据库作为存储系统,因此,我们的设计不能仅仅局限于关系型数据库,我们需要同时考虑其它形式的数据存储产品以便将来能够方便地集成新的存储方案。假设我们要设计一个针对DomainEventDataObject的“查询”功能,我们需要考虑的问题可能会有(但不一定仅限于):

  • 需要查询对象的哪些属性(或者说与DomainEventDataObject相对应的数据表的哪些字段)
  • 需要根据什么样的条件进行查询
  • 查询是否需要排序
  • 是否只查结果集中的任意一条记录,还是要返回所有的记录

在Apworks框架的Alpha版本中,查询的方法定义在Apworks.Storage.IStorage接口中。比如,根据给定的查询条件和排序方式,对指定DAO进行查询的方法定义如下:

   1: /// <summary>

   2: /// Gets a list of ordered objects from storage by given selection criteria and order.

   3: /// </summary>

   4: /// <typeparam name="T">The type of the object to get.</typeparam>

   5: /// <param name="criteria">The <c>PropertyBag</c> instance which contains the criteria.</param>

   6: /// <param name="orders">The <c>PropertyBag</c> instance which contains the ordering fields.</param>

   7: /// <param name="sortOrder">The sort order.</param>

   8: /// <returns>A list of ordered objects.</returns>

   9: IEnumerable<T> Select<T>(PropertyBag criteria, PropertyBag orders, SortOrder sortOrder)

  10:     where T : class, new();

这个方法是个泛型方法,泛型类型就是DAO的类型。它接受三个参数:前两个是用于指定查询条件和排序字段的PropertyBag,最后一个是指定排序方式的SortOrder。之所以采用PropertyBag,而不是接受SQL字符串,原因不仅是因为框架本身需要在今后能够方便地支持非关系型数据库,而且更重要的是,虽然SQL已经成为一种业界标准,但实际上不同的关系型数据库产品对SQL的支持和实现方式也有所不同,有些关系型数据库产品或许只支持SQL的一些子集,如果单纯地把SQL字符串作为Select方法的参数,明显是不合理的。事实上,Apworks.Storage.IStorage实现了Query Object模式[MF, PoEAA],Martin Fowler在他的PoEAA(《企业应用架构模式》)中有以下几点可以供读者参考:

  • “编程语言是可以支持SQL语句的,但大多数开发人员对此不太熟悉。而且,你需要了解数据库设计方案以便构造出查询。可以通过创建特殊的、隐藏了SQL内部参数化方法的查询器方法避免这一点。但这样难以构造更多的特别查询。而且,如果数据库设计方案改变,就会需要复制到SQL语句中”
  • “查询对象的一个共同特征是,它能够利用内存对象的语言而不是用数据库方案来描述查询。这意味着我可以使用对象和域名,而不是表和列名。如果对象和数据库具有相同的结构,这一点就不重要,如果两者有差异,查询对象就很有用。为了实现这样的视角变化,查询对象需要知道数据库结构怎样映射到对象结构,这一功能实际上要用到元数据映射”【daxnet注:上面提到过,在Apworks框架中,这个元数据映射的实现,就是IStorageMappingResolver
  • “为了描述任意的查询,你需要一个灵活的查询对象。然而,应用程序经常能用远少于SQL全部功能的操作来完成这一任务,在此情况下,你的查询对象就会比较简单。它不能代表任何东西,但它可以满足特定的需要。此外,当需要更多功能而进行功能扩充时,通常不会比从零开始创建一个全能的查询对象更麻烦。因此,应该为当前需求创建一个功能最小化的查询对象,并随着需求的增加改进这个查询对象”

以上三点让我很有感触,特别是第三点。目前基于PropertyBag的设计,只能够支持以AND连接的查询条件,比如,类似“WHERE a=va AND b=vb AND c=vc…”这样的查询,虽然在Apworks Alpha版本中,这样的查询已经够用了,但它不具备扩展性,基于关系型数据库的存储设计Apworks.Storage.RdbmsStorage已经将这种逻辑写死了,倘若我们需要一个复杂的查询,这种方式不仅没法胜任,而且没法扩展。PropertyBag应该要退休了。

在下一个版本的Apworks中,我使用.NET中的LINQ Expression代替了PropertyBag,并引入了一个WhereClauseBuilder的对象,专门根据LINQ Expression,针对关系型数据库产生WHERE子句。使用LINQ Expression的好处有:

  • LINQ Expression是.NET下的一种查询标准,多数存储系统产品能够提供针对LINQ Expression的查询解决方案,即使不提供,也可以自己定制Provider,虽然麻烦一点,但总归是可以实现的
  • LINQ Expression能够完美地“利用内存对象的语言而不是用数据库方案”来描述查询,语言集成的特性,为开发人员带来了更多的便捷
  • Apworks中,Specification是基于LINQ Expression的,于是,Apworks.Storage.IStorage就能够实现基于Specification的查询,实现接口统一

于是技术问题来了:如何将LINQ Expression转换成WHERE子句,以便Apworks.Storage.IStorage的类(Query Objects)能够使用这个WHERE子句构造出SQL语句,进而通过ADO.NET直接访问数据库?Apworks选用的是Expression Visitor的方案:使用Expression Visitor遍历表达式树(Expression Tree)然后产生WHERE子句。在讨论Expression Visitor之前,让我们回顾一下对象结构以及Visitor模式。

 

Visitor模式

网上面有关Visitor模式的文章太多了,还有相当一部分讨论的比较深入透彻,我也就不多说了。总之,Visitor模式在处理较复杂的对象结构时会显得十分自然:它能够遍历结构中的每一个对象,然后针对不同的对象类型作不同的处理。这就看上去像是为这些对象扩展了一些方法一样。之前,我有用过Visitor模式来验证程序配置节点的合理性,当节点的类型增加后,只需要扩展Visitor即可实现新的验证逻辑,非常方便。模式归模式,不同的应用场景,实现方式还是有所不同的。经典的Visitor例子,通常都是利用了函数的重载(多态性),并结合了Composite模式来说明问题,但实际上Visitor并非一定需要使用函数重载,也不是仅能用在Composite上。Expression Visitor的实现方式,就与这经典的Visitor案例有所不同。

 

Expression Visitor

在System.Linq.Expressions命名空间下,有一个ExpressionVisitor的抽象类,我们只需要继承这个抽象类,并重写其中的某些Visit方法,即可实现WHERE子句的生成。在这里我不打算继续去细究ExpressionVisitor是如何遍历表达式树的,我还是描述一下实现WHERE子句生成的几个细节问题。

  1. 支持哪些运算?
    LINQ Expression的类型有85种,但并不是SQL中会支持到所有的这85种类型。目前Apworks打算支持常用的条件运算,比如:大于、大于等于、小于、小于等于、不等于、等于这几种,打算支持常用的逻辑运算:AND、OR、NOT
  2. 支持哪些方法(函数)?
    目前Apworks支持的方法仅有三种:object.Equals、string.StartsWith和string.EndsWith。object.Equals将被翻译成“object = value”,string.StartsWith和string.EndsWith将被翻译成“LIKE”子句
  3. 支持内联函数和变量?
    目前仅支持变量,不支持内联函数。
    比如:可以用下面的方式来指定Expression:
       1: int a = GetAge();

       2: Expression<Func<Employee, bool>> expr = p => p.Age.Equals(a);

    而不能使用下面的方式来指定Expression:

       1: Expression<Func<Employee, bool>> expr = p => p.Age.Equals(GetAge());

  4. 支持扩展?
    当然,只需要继承已有的ExpressionVisitor类,并重写其中某些方法即可

在当前的Apworks版本中,Apworks.Storage.Builders命名空间下定义了针对关系型数据库的IWhereClauseBuilder接口,以及一个抽象实现:Apworks.Storage.Builders.WhereClauseBuilder类,它不仅实现了IWhereClauseBuilder接口,同时继承于System.Linq.Expressions.ExpressionVisitor抽象类,因此,WHERE子句生成的主体逻辑都在这个类中。SqlWhereClauseBuilder类继承WhereClauseBuilder类,以便实现特定于SQL Server语法的WHERE子句生成器。

由于Apworks.Storage.Builders.WhereClauseBuilder类的源代码比较长,我就不贴在这里了,读者朋友请【点击此处】查看该类的全部源代码。

 

与规约(Specification)整合

《EntityFramework之领域驱动设计实践(十):规约模式》一文中,我提出了基于.NET的规约模式的实现方式,为了迎合.NET对LINQ Expression的支持,规约模式的实现也采用了LINQ Expression,而原来的IsSatisfiedfiedBy方法则改为直接使用LINQ Expression来获得结果:

   1: public interface ISpecification<T>

   2: {

   3:     bool IsSatisfiedBy(T obj);

   4:     Expression<Func<T, bool>> Expression { get; }

   5: }

   6:  

   7: public abstract class Specification<T> : ISpecification<T>

   8: {

   9:  

  10:     #region ISpecification Members

  11:     

  12:     public virtual bool IsSatisfiedBy(T obj)

  13:     {

  14:         return this.Expression.Compile()(obj);

  15:     }

  16:     

  17:     public abstract Expression<Func<T, bool>> Expression { get; }

  18:     

  19:     #endregion

  20: }

回过头来考察Select方法,原本第一个参数是用Expression<Func<T, bool>>类型代替PropertyBag的,现在则可以直接使用ISpecification接口了,于是,我们的Query Object可以使用规约模式来支持数据查询了。

 

执行过程与客户端调用示例

基于上面的讨论,Select方法的定义,已经从使用PropertyBag作为查询条件,转变为使用ISpecification接口。注意:orders参数仍然使用PropertyBag,因为目前不打算支持基于表达式的排序条件:

   1: IEnumerable<T> Select<T>(ISpecification<T> specification, PropertyBag orders, SortOrder sortOrder)

   2:     where T : class, new();

在Apworks.Storage.RdbmsStorage中,使用WhereClauseBuilder.BuildWhereClause方法,根据LINQ Expression生成WHERE子句,进而产生SQL语句并使用ADO.NET访问关系型数据库:

   1: public IEnumerable<T> Select<T>(ISpecification<T> specification, PropertyBag orders, Storage.SortOrder sortOrder)

   2:             where T : class, new()

   3: {

   4:     try

   5:     {

   6:         Expression<Func<T, bool>> expression = null;

   7:         WhereClauseBuildResult whereBuildResult = null;

   8:         string sql = string.Format("SELECT {0} FROM {1}",

   9:             GetFieldNameList<T>(), GetTableName<T>());

  10:         if (specification != null)

  11:         {

  12:             expression = specification.GetExpression();

  13:             whereBuildResult = GetWhereClauseBuilder<T>().BuildWhereClause(expression);

  14:             sql += " WHERE " + whereBuildResult.WhereClause;

  15:         }

  16:         if (orders != null && sortOrder != Storage.SortOrder.Unspecified)

  17:         {

  18:             sql += " ORDER BY " + GetOrderByFieldList<T>(orders);

  19:             switch (sortOrder)

  20:             {

  21:                 case Storage.SortOrder.Ascending:

  22:                     sql += " ASC";

  23:                     break;

  24:                 case Storage.SortOrder.Descending:

  25:                     sql += " DESC";

  26:                     break;

  27:                 default: break;

  28:             }

  29:         }

  30:         using (DbCommand command = CreateCommand(sql))

  31:         {

  32:             if (command.Connection == null)

  33:                 command.Connection = Connection;

  34:             if (Transaction != null)

  35:                 command.Transaction = Transaction;

  36:             if (specification != null)

  37:             {

  38:                 command.Parameters.Clear();

  39:                 var parameters = GetSelectCriteriaDbParameterList<T>(whereBuildResult.ParameterValues);

  40:                 foreach (var parameter in parameters)

  41:                 {

  42:                     command.Parameters.Add(parameter);

  43:                 }

  44:             }

  45:             DbDataReader reader = command.ExecuteReader();

  46:             List<T> ret = new List<T>();

  47:             while (reader.Read())

  48:             {

  49:                 ret.Add(CreateFromReader<T>(reader));

  50:             }

  51:             reader.Close(); // Very important: reader MUST be closed !!!

  52:             return ret;

  53:         }

  54:     }

  55:     catch (ExpressionParseException)

  56:     {

  57:         throw;

  58:     }

  59:     catch (InfrastructureException)

  60:     {

  61:         throw;

  62:     }

  63:     catch (Exception ex)

  64:     {

  65:         throw ExceptionManager.HandleExceptionAndRethrow<StorageException>(ex,

  66:             Resources.EX_SELECT_FROM_STORAGE_FAIL,

  67:             typeof(T).AssemblyQualifiedName,

  68:             specification != null ? specification.ToString() : "NULL",

  69:             orders != null ? orders.ToString() : "NULL",

  70:             sortOrder);

  71:     }

  72: }

  73:  

 

下面这个方法将根据Aggregate Root的类型与ID,返回与之相关的所有Domain Events:

   1: public virtual IEnumerable<IDomainEvent> LoadEvents(Type aggregateRootType, long id)

   2: {

   3:     try

   4:     {

   5:  

   6:         PropertyBag sort = new PropertyBag();

   7:         sort.AddSort<long>("Version");

   8:         var aggregateRootTypeName = aggregateRootType.AssemblyQualifiedName;

   9:         ISpecification<DomainEventDataObject> specification = Specification<DomainEventDataObject>

  10:             .Eval(p => p.AggregateRootId == id && p.AggregateRootType == aggregateRootTypeName);

  11:  

  12:         return Select<DomainEventDataObject>(specification, sort, Apworks.Storage.SortOrder.Ascending)

  13:             .Select(p => p.ToEntity());

  14:     }

  15:     catch { throw; }

  16: }

转载至:http://www.cnblogs.com/daxnet/archive/2011/01/19/1939027.html

发表在 C# | 留下评论

在.NET Workflow 3.5中使用多线程提高工作流性能

最近在工作上碰到一个性能问题,由于项目是基于SOA的架构,使得整个系统完全依赖于各种各样的Service,其中用于处理业务逻辑的Business Services全部都用.NET Workflow 3.5实现(历史原因,项目还没升级到Workflow 4)。在众多的Business Service中,其中有一个的主要功能是,通过调用不同的Data Service来获取数据,然后根据业务逻辑来组织这些数据并返回给它的调用者。该Business Service的工作流(Workflow)主要包含三个活动组件(Activity),大致可以用下图表示:

image

需要说明一下,在实际项目中,这个Workflow本身不仅仅只是简单地包含上面三个Activity,通过性能测试的数据分析,瓶颈就在这三个Activity上,而每个Activity的执行时间又主要消耗在反复调用Data Service上。在此,为了简化问题的描述,我把其它不相干的Activity剔除了,于是就得到了上图的结构。

图中的三个Activity都会分别去调用不同的Data Service来获得数据,尤其在getNotesActivity中,Data Service会被循环调用,这使得系统性能大打折扣。原本有一个解决方案可以在一定程度上提高getNotesActivity的效率,就是修改被调用的Data Service,使得它能够一次性接收多个request的数据,处理完之后再将所有的结果一次性返回,这样就避免了Data Service的循环调用,有效地减少了数据在网络上的来回次数。但是,这种解决方案需要更改Data Service的Request Schema,这个改动是很大的,因为可能有很多其它的Business Service都在调用这个Data Service,牵涉的范围太广了。

根据实际项目,稍加分析不难发现,这个Workflow中的Activity有如下几个特点:

  • 三个Activity的输入属性参数都来自于Workflow(即通过与Workflow中定义的DependencyProperty进行绑定而获得数据),并不存在下游的Activity的输入参数需要依赖上游Activity的输出参数的情况
  • 每个Activity的输出属性参数都只关注某一种类型的数据,在Workflow Runtime执行完某个Activity后,也会通过DependencyProperty将处理结果传递给Workflow,而这些输出属性参数之间也并没有任何关联
  • 三个Activity所调用的Data Service也比较独立,基本上可以说是在做三个完全不同的工作
  • 时间主要消耗在Data Service的调用上,不存在由于复杂的运算逻辑导致CPU利用率近似100%的情况,也不存在由于物理内存用完而需要频繁读写虚拟内存的情形

上述的几个特点中,第四点为我们引入多线程或并行任务处理提供了主要依据。这里需要额外岔开一下。有很多软件人员认为,多线程一定能够提高系统性能,因为事务可以分派到多个线程中进行并行处理。我觉得,应该这样去看待这个问题:首先,根据Martin Fowler的《企业应用架构模式》(也就是著名的PoEAA)一书中有关性能的讨论认为,有很多术语可以描述性能,比如:响应时间、响应性、等待时间、吞吐率、负载、负载敏感度等。假设完成某个任务需要的时间很长,比如需要5秒,那么其响应时间就是5秒,而如果让用户等待这5秒过去后,再将系统的控制权交给用户,就会让用户明显感觉很不顺,于是响应性就很差;但如果能将这个任务交给另一个执行体去处理,而程序本身直接将系统控制权交给用户,等那个执行体完成任务处理后,再将结果提供给用户,那么,同样处理这个任务需要5秒钟,这种方式的响应性就明显要好于前者,这也是我们以前做Windows Forms开发的时候,要把耗时的处理交给另一个线程处理,以不至于因为主线程的阻塞而导致界面冻结的尴尬局面。因此,多线程的引入,可以提高系统的响应性。

其次,多线程是否能够提高系统的响应时间?这也未必,在单核处理器上,多线程是采用时间片轮循的方式实现的,也就是说,相同时间点上,只有一个线程在执行,只不过是时间片足够小,轮循频率足够高,才让我们感觉线程是并行执行的,在这样一种体系结构下,完成任务的处理还是需要那么长时间,甚至时间片的切换倒还会带来额外的开销。在多核系统中,或许真的可以提高响应时间,不过我目前没有实际的测试数据用来比较,因此在这个问题上,我还没有足够的发言权。

而对于目前项目的情况,Data Service是分布在网络上不同位置的资源,如果能让这些Data Service同时处理数据请求,再让Business Service去组织分别来自这些Data Service的处理结果,那么整个Business Service的响应时间是可以明显提高的,响应时间提高了,响应性也同样提高了。假设第一个Activity耗时t1,第二个Activity耗时t2,第三个Activity耗时t3,那么,如果按上图中的顺序方式执行,Business Service的响应时间就是t1+t2+t3。但如果让这些Activity并行处理(也就相当于并行调用Data Service使其同时处理数据请求),那么Business Service的响应时间应该就是max(t1, t2, t3)。

于是,我打算将上述的Workflow修改一下,采用多线程的方式来分别运行每个Activity,最后再将结果汇总。我修改后的Workflow如下所示:

image

在此需要对ParallelActivity说明一下。.NET Workflow 3.5的ParallelActivity并没有做到所谓的“并行执行”,因为Workflow Runtime是在单独的线程上执行Workflow Instance的,因此,要让多个Activity真正并行执行是做不到的。ParallelActivity的真正用意在于协调每个分支中的SequenceActivity(注意:ParallelActivity的每个分支只能接收一个SequenceActivity),使得其中的每个Activity都有一次执行的机会。某个分支中的一个活动执行过后,就会轮到下一个分支。当这个分支执行了一个活动后,执行又会转移到再下一个分支,以此类推。当所有分支都有了执行机会之后,又会从第一个(最左侧)分支开始这一过程,继续执行第一个分支的下一个活动(如果存在的话)。因此,在我们的这个例子中,完全可以不用ParallelActivity,而仍然选择原来的结构即可。之前我并没有完全清楚地了解ParallelActivity,开始一直以为ParallelActivity的意思是,让Workflow Runtime同时安排(Schedule)每个分支的执行,以便当每个分支都以异步方式运行时,所有的分支可以实现并行处理。不过也不要紧,在这里使用ParallelActivity,虽然没有有效地利用它的特性,但与原来的Workflow相比,从可读性上讲,这种结构更容易让人觉得这是一种并行的运行方式。

另一个变化是,原本每个操作都是写在一个自定义的Activity中的,通过重写Activity的Execute方法来做业务处理,而现在则是用CodeActivity来代替原来的Activity,这样做的好处是,可以将业务处理的代码放在同一个Context中,这也为线程同步提供了便利,降低了使用线程的复杂度。

以下是改进后的Workflow的代码,供参考。

隐藏行号 复制代码 改进后的Workflow
  1. using System;
    
  2. using System.Collections.Generic;
    
  3. using System.Threading;
    
  4. using System.Workflow.Activities;
    
  5. 
    
  6. namespace WorkflowConsoleApplication3
    
  7. {
    
  8.     public sealed partial class Workflow1 : SequentialWorkflowActivity
    
  9.     {
    
  10.         List<Thread> threads = new List<Thread>();
    
  11. 
    
  12.         public Workflow1()
    
  13.         {
    
  14.             InitializeComponent();
    
  15.         }
    
  16. 
    
  17.         private void getAdditionalInfoActivity_Execute(object sender, EventArgs e)
    
  18.         {
    
  19.             var t1 = new Thread(() =>
    
  20.                 {
    
  21.                     // Call Data Service 1 to implement business logic...
    
  22.                 });
    
  23.             threads.Add(t1);
    
  24.             t1.Start();
    
  25.         }
    
  26. 
    
  27.         private void getNotesActivity_Execute(object sender, EventArgs e)
    
  28.         {
    
  29.             var t2 = new Thread(() =>
    
  30.                 {
    
  31.                     // Call Data Service 2 in a loop to implement business
    
  32.                     // logic...
    
  33.                 });
    
  34.             threads.Add(t2);
    
  35.             t2.Start();
    
  36.         }
    
  37. 
    
  38.         private void getSpecialPointsActivity_Execute(object sender, EventArgs e)
    
  39.         {
    
  40.             var t3 = new Thread(() =>
    
  41.                 {
    
  42.                     // Call Data Service 3 to implement business logic...
    
  43.                 });
    
  44.             threads.Add(t3);
    
  45.             t3.Start();
    
  46.         }
    
  47. 
    
  48.         private void syncCodeActivity_Execute(object sender, EventArgs e)
    
  49.         {
    
  50.             // Wait for all threads to terminate...
    
  51.             threads.ForEach(p => p.Join());
    
  52. 
    
  53.             // TODO: Process with results and exceptions
    
  54.         }
    
  55.     }
    
  56. 
    
  57. }
    
  58. 
    
  59. 
    

从上面的代码中可以看到,每个CodeActivity在执行的时候都会启动一个线程,这个线程会调用相应的Data Service来实现其业务逻辑,线程创建以后,会被保存在一个线程列表里,用来在syncCodeActivity中进行线程同步。syncCodeActivity则通过线程的Join方法来等待所有线程全部完成各自的工作,最后对运行结果和异常进行处理。

此处线程的运用需要遵循.NET线程使用的最佳实践,应该尽量避免线程的阻塞,在访问临界资源的时候应作加锁处理以防止状态异常。由于在这个例子中,每个线程又会牵涉到其它Service的调用,因此在线程中捕获的异常,我建议还是先将其记录下来,然后“温和”地直接使用return语句终止线程执行,而不是随意抛出异常而使得线程进入一个不确定的状态。当然,读者朋友如果在多线程环境中有处理异常的经验,也恳请在本文留言指导。

对Workflow进行调整之后,重新编译、部署并运行这个Business Service,然后用已经写好的Client程序进行测试,我们得到了如下的结果(几个明显的噪音数据已经被划掉,没有包含在统计中)。从这个报表可以看到,针对我们的这个案例,在Workflow中引入多线程的确可以明显地提高系统性能。

image

转载至:http://www.cnblogs.com/daxnet/archive/2011/02/21/1959423.html

发表在 C# | 留下评论

Microsoft NLayerApp案例理论与实践 – 项目简介与环境搭建

项目简介

Microsoft – Spain团队有一个很不错的“面向领域多层分布式项目”案例:Microsoft – Domain Oriented N-Layered .NET 4.0 App Sample(在本系列文章中,我使用NLayerApp作为该项目的名称进行介绍),在codeplex上的地址是:http://microsoftnlayerapp.codeplex.com/。它是学习领域驱动设计(DDD)的一个非常不错的案例项目。该项目采用的是经典的DDD架构,而不是CQRS架构,但我觉得整个案例做的非常不错,基本上包含了基于DDD的架构实践的各个方面。因此,应不少社区朋友的要求,我打算花一部分精力来写一个介绍该项目理论与实践的系列文章。这部分系列文章将分为两个部分:

  1. 原理部分:这部分介绍Microsoft NLayerApp的一些理论依据,包括架构设计原则、分层架构、DDD、Distributed DDD、面向对象分析与设计等。事实上,microsoftnlayerapp.codeplex.com站点上已经有一些文档对这部分内容作了介绍,因此,原理部分的内容我将基本上是对这些英文文档进行翻译整理,然后再添加一些自己的注释,这样做的好处是,能够就整个企业级项目的开发与设计为读者提供一套相对系统全面的学习材料。NLayerApp的官方站点本身也在做西班牙语到英语的翻译工作,所以这部分英文文档也并不全面,我会在新英文版文档发布后,在此相应地添加所缺失的部分
  2. 实践部分:这部分将对整个NLayerApp Solution的结构、各个逻辑层、各种用到的技术进行剖析和介绍。与原理部分不同,此部分内容更关注技术的具体实现细节,而不是去讨论什么是面向对象,什么是分层架构等基础性问题

注意:Microsoft – Spain团队一直以“Domain Oriented”一词来形容这个项目,而不是用“Domain Driven Design”,原因是,Domain Driven Design包含的内容,不仅仅是某一种架构技术,它还包含软件项目的开发方式、开发团队的协作管理、用于领域专家和软件人员之间的“通用语言”的创建等内容。然而,在整个NLayerApp项目中,并没有用到DDD的所有这些内容,项目的范围仅限于逻辑/技术层面的架构设计。

NLayerApp项目环境搭建

在开始这个系列文章之前,先让我们把NLayerApp的项目环境搭建好。在搭建环境之前,请检查你的电脑是否满足下面的先决条件:

请按下面的步骤安装和配置NLayerApp:

  1. 完成上述开发包的安装和配置(最后两项可以不安装,本系列文章没有用Windows Server AppFabric和Azure的功能)
  2. 下载NLayerApp v1.0的压缩包,地址是:http://microsoftnlayerapp.codeplex.com/releases/view/56660,选择V1.0 – N-Layer DDD Sample App NET4.0,本系列文章将使用这个版本进行介绍
    image
  3. 解压缩下载完的zip包,包含三个文件夹:CORE、CORE-APPFABRIC和CORE-AZURE。本系列文章没使用AppFabric和Azure,所以,直接进入CORE目录
  4. 暂时直接无视Tests,所以,双击打开NLayerAppWithoutTesting.sln解决方案
  5. 这个解决方案没有将Infrastructure.Data.MainModule.Mock项目添加进来,这会导致Infrastructure.CrossCutting.IoC项目无法编译通过。在Visual Studio中,将解决方案展开到1.5.1 Data节点,在该节点上右键单击,选择Add | Existing Project,然后在CORE的Infrastructure.Data.MainModule.Mock目录下选择Infrastructure.Data.MainModule.Mock.csproj项目文件,并单击Open按钮
  6. Server Explorer中,右键单击Data Connections节点,选择Create New SQL Server Database选项
    image
  7. 在打开的Create New SQL Server Database对话框中,填入你的Server地址,然后输入数据库名称,再单击OK按钮。本案例使用SQL Express(with Windows Authentication),使用默认的数据库名称NLayerApp
    image

    你完全可以选择自己定义的SQL Server和数据库名称,如果你是使用自己定义的SQL Server和数据库的话,请同时修改2 – Database节点下NLayerAppDatabase项目的属性:右键单击NLayerAppDatabase项目,选择Properties,在Property页的Deploy选项卡中修改相关参数:
    image

  8. 右键单击NLayerAppDatabase项目,然后单击Deploy,这将创建数据库Schema
  9. 编译整个解决方案
  10. 1.2 – Distributed Services节点下,找到DistributedServices.Deployment项目,右键单击项目下的MainModule.svc文件,选择View in Browser,以启动WCF服务
    image
  11. 启动用户界面。NLayerApp v1.0提供以下几种用户界面:基于RIA的Silverlight 4.0 Client,基于Web的ASP.NET MVC Client,基于Windows的WPF Client

    基于RIA的Silverlight 4.0 Client
    右键单击Silverlight.Client.Web项目下的Silverlight.Client.Web.html文件,选择View in Browser,则启动基于RIA的Silverlight 4.0 Client
    image

    基于Web的ASP.NET MVC Client
    MVC.Client项目设置为启动项目并直接运行,可以启动基于Web的ASP.NET MVC Client
    image

    可能是我浏览器版本低的缘故,得到的ASP.NET MVC页面布局有点乱

    基于Windows的WPF Client
    WPF.Client项目设置为启动项目并直接运行,可以启动基于Windows的WPF Client
    image

本文介绍了NLayerApp项目的基本情况和环境搭建。从下一讲开始,我们将进入架构设计的理论学习部分,包括:分层架构、SOLID与设计原则、面向领域驱动设计(DDD)的架构趋势以及分布式DDD(Distributed DDD,DDDD)。这部分内容将主要来自于NLayerApp官网(microsoftnlayerapp.codeplex.com)提供的英文文档,daxnet将在此做翻译、整理与注解。

转载至:http://www.cnblogs.com/daxnet/archive/2011/03/01/1967896.html

发表在 C# | 留下评论