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# | 留下评论

Microsoft NLayerApp案例理论与实践 – 多层架构与应用系统设计原则

在对NLayerApp实际项目进行讨论之前,让我们首先学习一下(或者应该说重温一下)分层/多层架构与应用系统设计原则。很多朋友会认为这些都是老掉牙的内容,只要是软件从业人员,都会对这些内容非常熟悉。然而,果真如此吗?我在这里整理这部分内容,一方面是为介绍NLayerApp打下基础,而另一方面,则是希望借此机会将这些理论性的东西做个归纳,也希望读者朋友能够认真阅读,毕竟温故知新嘛。

需要说明的是,从本章节开始,大多数理论性的东西都源自Microsoft Spain团队针对NLayerApp所编写的《Architecture Guide Book》,事实上这本Guideline的英文版至今也还没有完成,我会从中抽出部分章节做些翻译和归纳,有兴趣的朋友请直接上microsoftnalyerapp.codeplex.com站点上下载英文版阅读。

Layers与Tiers

对Layers与Tiers这两个单词进行区分是非常重要的。从中文翻译看,两者都是“层”的意思,因此我们往往会将这两个概念弄混。Layer一词更多的是表示对系统组件或功能的逻辑区分,它并没有包含将组件分布到不同的区域、不同的服务器上的意思。而Tier则是表示系统组件和功能在服务器、网络环境以及远程位置的物理部署。尽管这两个概念同时使用者非常相近的一些术语,比如“展示”、“服务”、“业务”和“数据”等,但我们必须了解它们之间的差别。下面这幅图表明了多层(N-Layer)逻辑架构与三层(3-Tier)物理结构之间的差异:

image

需要注意的是,对于具有一定复杂度的应用程序而言,采用多层(N-Layer)逻辑架构的实现方式是非常必要的,这会降低系统的复杂度,并在设计、开发、测试、部署及维护等各个环节为应用系统带来高可用性、高延展性等正面效应。然而,并非所有的应用程序必须以三层(3-Tier)/多层(N-Tier)物理结构进行部署,我们可以将多个逻辑层部署在同一台机器上,也可以根据需求,将这些逻辑层部署在网络中的不同机器上。

逻辑分层(Layer)的设计

在讨论DDD的分层之前,先让我们看看传统的分层方式。就像上文所述,我们应该根据项目的实际需求,将组件/功能模块合理地划分到“逻辑层”中。同一层中的组件,应该是高内聚的,并具有相同的抽象层次。层与层之间应该低耦合。对于以分层设计的应用程序而言,最关键的问题就是如何处理层与层之间的依赖关系。考察传统的多层架构应用,处于某层的组件,只能对同层或下层的其它组件进行访问,这样做可以有效地降低层与层之间的依赖关系。通常会有两种分层设计:严格分层与灵活分层

  1. “严格分层”迫使组件只能访问同层的其它组件,或者只能访问直接下层的其它组件,于是,第N层的组件只能访问第N或N-1层的组件,而第N-1层的组件只能访问第N-1或N-2层的组件,以此类推
  2. “灵活分层”允许组件访问同层的其它组件,以及所有下层的其它组件,于是,第N层的组件可以访问第N或N-1、N-2…层的组件

使用“灵活分层”的架构可以提高系统性能,因为这样的结构无需引入过多的请求/反馈的传递操作,因为一个层可以直接访问位于其下的任何层;而“严格分层”却降低了层与层之间的耦合性,对低层的修改不会对整个系统造成广泛的影响。根据Eric Evans在其《领域驱动设计-软件核心复杂性应对之道》一书中的描述,DDD的分层选用的是“灵活分层”模式。

让我们再把讨论的粒度细化,来看看层中的组件与组件之间的关系。事实上,在很多复杂的应用中,位于同一层的组件与组件虽然具有相同的抽象层次,它们也不一定是高内聚的。因此,我们可以引入“模块(Module)”的概念,将同一层中高内聚的组件放在同一个模块中,于是,每个层又会由一个或多个高内聚的子系统(模块)所组成,如下UML组件图所示:

image

使用分层架构,有如下几点好处:

  • 提高系统的可测试性
  • 对解决方案的维护和管理变得更加简单。层内高内聚、层间低耦合的结构,使得系统实现与分层组织方式变得非常灵活方便
  • 其它外部应用程序能够非常方便地使用不同的层所提供的特定功能
  • 当系统以层的方式进行组织时,分布式开发也变得非常简单易行
  • 在某些情况下,分层系统的物理部署方式能够给系统带来延展性,当然,应该有效地评估具体的实践方式,因为这种做法有可能损伤系统性能

应用系统基本设计原则 – SOLID

应用系统的设计应该遵循一些基本的设计原则,这能帮助你有效地创建一个低成本、高可用、高可扩展的应用程序。在这里,我们引入一个SOLID设计原则,SOLID由如下几点构成:

  • Single Responsibility Principle(单一职责原则)
  • Open Close Principle(开-闭原则)
  • Liskov Substitution Principle(里氏替换原则)
  • Interface Segregation Principle(接口分离原则)
  • Dependency Inversion Principle(依赖反转原则)

下面简要介绍一下这几个“原则”。

  • 单一职责原则:每个类应该只有一个独一无二的职责,或者说每个类只能有一个主要功能,由此派生出一个结论:每个类应该尽可能少地依赖于其它类
  • 开-闭原则:每个类,应该对“扩展”进行开放,而对“修改”进行封闭,也就是支持扩展,而不是支持修改:类中的方法可以通过继承关系进行扩展,而不会改变类本身的代码
  • 里氏替换原则:子类可以被基类型(基类或者接口)替换。应用程序依赖抽象运行,其行为不会因为具体实现的改变而更改,应用程序应该依赖于抽象(基类或者接口),而不是具体实现。接下来将要讨论到的“依赖注入(Dependency Injection)”就与这条原则有关
  • 接口分离原则:接口的职责也应该是单一的,接口中应该包含哪些方法,需要进行严格的评估,如果其中某些方法的职责与接口的本身定义不相符合,则应该将其分离到其它接口中。类需要根据其调用者所需要的不同接口类型,来暴露不同的接口
  • 依赖反转原则:抽象不能依赖于具体,而具体则应该依赖于抽象。类之间的直接依赖应该用抽象来取代,这样做的一个优点是,我们可以实现自上而下的设计方式:在下层的具体实现还没有确定的情况下,只要能够在抽象层面将接口确定,就能够完成上层的设计与开发,这同样给可测试性带来便捷

除了以上所述的SOLID原则之外,还有以下几个关键的设计原则可供参考:

  • 组件设计应该是高内聚的:相信大家都很熟悉这点了,就不多说了。例如:不要将数据访问逻辑写进领域模型的业务逻辑中,这与上述“单一职责原则”是密切相关的
  • 将Cross-Cutting的代码从特定于应用程序的逻辑中分离开来:Cross-Cutting的代码是一些面向横面的代码,比如安全、操作管理、日志以及测量/计量系统等。将这些代码与应用系统业务逻辑混在一起会增加系统的复杂性,给将来的扩展和维护造成很大的麻烦。这与“面向方面编程(Aspect-Oriented Programming,AOP)”有关
  • 关注点分离(Separation of Concerns,SoC):将应用系统分成多个子部分(子系统),各个部分之间的功能尽量不要重复,其目的就是为了减少交互点,以实现高内聚和低耦合
  • Don’t Repeat Yourself(DRY):一个特定的功能只能在某个特定的组件中实现一次,同样的功能不要在多个组件中重复多次
  • 避免YAGNI(You Ain’t Gonna Need It)效应:只考虑和设计必须的功能,避免过度设计

好了,本讲就介绍到这里,估计对大多数接触过架构的软件朋友来说,本讲的部分内容都是废话。下一讲开始,我会花部分笔墨在DDD/DDDD的分层介绍上,虽然有可能还是废话,但这对我们理解NLayerApp的解决方案组织结构会有相当的帮助。

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

发表在 C# | 留下评论

.NET分布式事务处理总结【上】 – 实现分布式事务处理

在继续实现Apworks框架的过程中,发现一个必须解决的问题,就是分布式事务处理。它要求两个原本相对独立的工作能够在同一个事务上下文中完成处理。如果处理成功,则两者同时提交,否则,两者同时回滚。Apworks框架需要依赖分布式事务来解决二次提交(2PC)的问题,这个在我之前的博客文章中也提到过,简单地说,就是领域事件的存储和发布必须是一个原子操作。在此,我打算使用2-3篇文章的篇幅对.NET下分布式事务处理的实现做个简单的总结,其中并不会涉及到有关分布式事务的原理/理论方面的内容,仅仅是对其实现方式做个记录。

首先需要了解到,虽然.NET分布式事务在一定程度上能够解决Apworks框架中2PC的问题,但它不一定是最好的选择。原因很简单:Apworks允许开发人员选用各种不同类型的数据库作为数据存储机制,也允许选用各种不同的消息派发产品作为事件发布机制,因此,并不是所有的这些技术选型都支持基于MS-DTC的分布式事务处理。但就目前的项目情况而言,SQL Server、Oracle以及MSMQ等,都是支持MS-DTC的。至于其他的技术选型如何去解决2PC的问题,将是今后需要讨论的话题,需要根据具体实践情况具体分析了。不过Greg Young提出过一个解决办法,就是通过引入序列标识符,使Event Store同时作为Event Bus,减少提交的次数,从而避免2PC的问题。

言归正传,现在让我们看看在.NET Framework 1.1和.NET Framework 2.0+版本的.NET Framework中,分布式事务处理是如何实现的。

.NET Framework 1.1下的实现方式

在.NET Framework 1.1下,我们需要使用System.EnterpriseServices.ServicedComponent类来实现分布式事务处理,大致步骤如下:

  1. 向提供分布式事务处理函数的程序集添加对System.EnterpriseServices的引用
  2. 使用System.EnterpriseServices.ApplicationNameAttribute来标注该程序集,以指定应用名称
  3. 指定程序集的COM可见性(ComVisibleAttribute)为True
  4. 创建一个公共的、包含默认构造函数(没有任何参数的构造函数)的类,并使其继承于System.EnterpriseServices.ServicedComponent类,同时,使用System.EnterpriseServices.TransactionAttribute来标注该类,使其支持分布式事务处理
  5. 在这个新创建的类中,添加支持分布式事务处理的方法,并将涉及分布式事务处理的代码置入try块中,并在代码结束处使用ContextUtil.SetComplete调用以在COM+上下文中将consistent bit设置为true,通知可以成功提交;在catch块中,使用ContextUtil.SetAbort调用以在COM+上下文中将consistent bit设置为false,表示无法成功提交
  6. 同样,也可以直接使用System.EnterpriseServices.AutoCompleteAttribute来标注这个支持分布式事务处理的方法,那么,就无需在代码中显式地调用ContextUtil.SetComplete和ContextUtil.SetAbort方法
  7. 使用sn.exe工具,对该程序集进行数字签名(根据网上收集的资料,在完成数字签名后,该程序集还需要被发布到GAC中,但根据实验结果,即使不发布到GAC,分布式事务处理同样可以正确进行)

下面的代码完整地展示了这种分布式事务处理的实现方式。注意:这段代码使用了Apworks框架,本文最后将给出完整的Visual Studio解决方案以供读者下载参考。

隐藏行号 复制代码 .NET Framework 1.1下的实现方式
  1. using System;
    
  2. using System.EnterpriseServices;
    
  3. using System.Runtime.InteropServices;
    
  4. using Apworks;
    
  5. using Apworks.Bus;
    
  6. using Apworks.Events;
    
  7. using Apworks.Events.Storage;
    
  8. using Apworks.Storage;
    
  9. 
    
  10. [assembly: System.EnterpriseServices.ApplicationName("PublishEventService")]
    
  11. [assembly: ComVisible(true)]
    
  12. 
    
  13. namespace TPCDemo.ServicedComponents
    
  14. {
    
  15.     [System.EnterpriseServices.Transaction(System.EnterpriseServices.TransactionOption.Required)]
    
  16.     public class PublishEventService : System.EnterpriseServices.ServicedComponent
    
  17.     {
    
  18.         public PublishEventService() { }
    
  19. 
    
  20.         public bool Publish(IDomainEvent evt, bool thrw)
    
  21.         {
    
  22.             IStorage storage = ObjectContainer.Instance.GetService<IStorage>();
    
  23.             IEventBus eventBus = ObjectContainer.Instance.GetService<IEventBus>();
    
  24. 
    
  25.             DomainEventDataObject data = new DomainEventDataObject().FromEntity(evt);
    
  26. 
    
  27.             try
    
  28.             {
    
  29.                 storage.BeginTransaction();
    
  30.                 eventBus.BeginTransaction();
    
  31. 
    
  32.                 storage.Insert<DomainEventDataObject>(new PropertyBag(data));
    
  33.                 eventBus.Publish(evt);
    
  34. 
    
  35.                 storage.Commit();
    
  36.                 eventBus.Commit();
    
  37. 
    
  38.                 if (thrw)
    
  39.                     Throw();
    
  40. 
    
  41.                 ContextUtil.SetComplete();
    
  42.                 return true;
    
  43.             }
    
  44.             catch
    
  45.             {
    
  46.                 ContextUtil.SetAbort();
    
  47.                 return false;
    
  48.             }
    
  49.         }
    
  50. 
    
  51.         private void Throw()
    
  52.         {
    
  53.             throw new Exception();
    
  54.         }
    
  55.     }
    
  56. }
    
  57. 
    

.NET Framework 2.0+下的实现方式

从.NET Framework 2.0开始,在System.Transactions程序集中提供了TransactionScope类,使得程序员能够在不了解甚至不接触COM+的前提下使用分布式事务处理,大大降低了开发难度。开发人员只需要将涉及分布式事务处理的代码包括在TransactionScope中即可。大致步骤如下:

  1. 向提供分布式事务处理函数的程序集添加对System.Transactions的引用
  2. 创建一个新的TransactionScope对象,并使用using关键字限定该对象的作用范围
  3. 将涉及分布式事务处理的代码置于TransactionScope的作用范围之内
  4. 在事务完成处添加TransactionScope.Complete调用

下面的代码完整地展示了这种分布式事务处理的实现方式。注意:这段代码使用了Apworks框架,本文最后将给出完整的Visual Studio解决方案以供读者下载参考。

隐藏行号 复制代码 .NET Framework 2.0+下的实现方式
  1. MyEvent myEvent = MyEvent.CreateForTest();
    
  2. using (TransactionScope ts = new TransactionScope())
    
  3. {
    
  4.     // Note that these two lines below MUST be declared within
    
  5.     // the transaction scope to obtain the DTC context.
    
  6.     IStorage storage = ObjectContainer.Instance.GetService<IStorage>();
    
  7.     IEventBus eventBus = ObjectContainer.Instance.GetService<IEventBus>();
    
  8. 
    
  9.     DomainEventDataObject data = new DomainEventDataObject().FromEntity(myEvent);
    
  10. 
    
  11.     storage.BeginTransaction();
    
  12.     eventBus.BeginTransaction();
    
  13. 
    
  14.     storage.Insert<DomainEventDataObject>(new PropertyBag(data));
    
  15.     eventBus.Publish(myEvent);
    
  16. 
    
  17.     storage.Commit();
    
  18.     eventBus.Commit();
    
  19. 
    
  20.     ts.Complete(); // complete the DTC
    
  21.  }
    
  22. 
    

为了同时验证这两种实现方式的可行性,我创建了一个测试项目,对每种方法的成功及失败的提交进行测试。结果如下:

image

示例源代码下载

【请单击这里下载本文示例工程文件和源代码】

源代码使用指南

  1. 确保计算机已经成功安装Microsoft SQL Server 2005/2008 Express Edition,并成功安装了MSMQ
  2. 在Visual Studio 2010中打开TPCDemo.sln解决方案并编译
  3. 右键单击TPCDemo.Database项目,并选择“部署(Deploy)”,这将在SQL Server Express中创建TPCDemoDB数据库,并向其创建DomainEvents数据表
  4. 单击“开始”菜单,右键单击“计算机”并选择“管理”,这将打开计算机管理控制台,在“计算机管理(本地)”节点下,找到“服务和应用程序”,然后展开“消息队列”节点
    clip_image002
  5. 右键单击“专用队列”节点,选择“新建”、“专用队列”菜单,这将打开“新建专用队列”对话框。在对话框中输入TPCDemoQueue作为队列名称,并确保勾选了“事务性”选项(一旦队列被创建,该选项将无法修改),然后单击“确定”按钮
    clip_image004
  6. 在Visual Studio 2010的测试视图中运行所有测试用例,以得到测试结果

下文将介绍MSMQ的内部事务处理以及具有MSMQ参与的分布式事务处理实现方式。

转载至:http://www.cnblogs.com/daxnet/archive/2011/03/15/1984397.html

发表在 C# | 留下评论

.NET分布式事务处理总结【下】 – 包含MSMQ的分布式事务处理

.NET直接提供对MSMQ的访问支持,只需要添加System.Messaging程序集引用即可方便地操作MSMQ。MSMQ支持两种事务处理模式:内部事务处理以及基于MS-DTC的分布式事务处理。

MSMQ的内部事务处理

MSMQ的内部事务处理是指,仅采用MSMQ本身提供的事务处理机制完成事务处理。比如,假设有一系列的消息需要发布到MSMQ,那么,就可以启动一个内部事务,确保这些消息的发布过程是一个原子操作。要使用MSMQ的内部事务处理机制,在创建消息队列的时候,就需要勾选“事务性”选项,如下图所示:

image

首先,需要创建一个MessageQueueTransaction的对象,并使用Begin调用以启动MSMQ的内部事务处理。然后,在MessageQueue的Send方法中,使用Send(object, MessageQueueTransaction)的重载函数发送消息,将创建好的MessageQueueTransaction对象作为第二个参数传递给Send方法;在完成所有消息的发送之后,使用MessageQueueTransaction对象的Commit方法提交事务。如果在发送消息的过程中遇到问题,则使用MessageQueueTransaction对象的Abort调用回滚事务。请参见下面的示例代码:

隐藏行号 复制代码 MSMQ的内部事务处理
  1. using (MessageQueue messageQueue = new MessageQueue(@".\private$\TPCDemoQueue", 
    
  2.     false, false, QueueAccessMode.SendAndReceive))
    
  3. {
    
  4.     MessageQueueTransaction trans = new MessageQueueTransaction();
    
  5.     try
    
  6.     {
    
  7.         trans.Begin();
    
  8.         for (int i = 0; i < 5; i++)
    
  9.         {
    
  10.             messageQueue.Send(new Message(i), trans);
    
  11.         }
    
  12.         trans.Commit();
    
  13.     }
    
  14.     catch
    
  15.     {
    
  16.         trans.Abort();
    
  17.     }
    
  18.     messageQueue.Close();
    
  19. }
    
  20. 
    

注意:如果你的消息队列在创建的时候没有设置“事务性”选项,那么,在完成消息队列的创建以后,你将无法修改该选项。更糟糕的是,在非事务性队列上执行上面的代码,则无法将消息发布到消息队列上,框架本身也不会提示任何错误信息,指示消息并未发布成功。

在分布式事务处理中使用MSMQ

在分布式事务处理的上下文中(比如,.NET 2.0+的TransactionScope中),上面所提到的MessageQueueTransaction将毫无用处,也就是说,MessageQueueTransaction与分布式事务处理毫无关系。你所要做的是,用正常的方式初始化一个MessageQueue的实例,然后,调用Send方法发布消息,在发布消息的时候,通过设置MessageQueueTransactionType的值来告诉MessageQueue,目前正处于一个分布式事务的上下文中。于是,你需要使用Send/Receive的重载方法:Send(object, MessageQueueTransactionType)以及Receive(MessageQueueTransactionType)。如下:

隐藏行号 复制代码 分布式事务中的MSMQ调用
  1. using (TransactionScope transaction = new TransactionScope())
    
  2. {
    
  3.     Message inputMsg = inputQueue.Receive(MessageQueueTransactionType.Automatic);
    
  4.     // do some work
    
  5.     transaction.Complete();
    
  6. }
    
  7. 
    

注意:对于一些生命周期相对较长的事务处理,比如,假设你的用例是这样的:你首先需要从一个消息队列中获得消息,然后更新你的数据库记录,那么你的代码可能会是这样的:

隐藏行号 复制代码 分布式事务中的MSMQ调用
  1. using (TransactionScope transaction = new TransactionScope())
    
  2. {
    
  3.     using (MessageQueue someQueue = new MessageQueue("<queue connection>"))
    
  4.     {
    
  5.         Message msg = someQueue.Receive();
    
  6.         // do something else
    
  7.     }
    
  8.     transaction.Complete();
    
  9. }
    
  10. 
    

这样做其实是不对的!因为Receive方法是一种同步调用,如果消息队列中根本没有任何内容,那么Receive调用就会被阻塞,直到消息队列中出现新的消息。这就意味着你的分布式事务一直都是处于开启的状态,而且可能由于等待时间过长而导致超时,最终导致一个MessageQueueException。

正确的做法是,在MessageQueue上使用BeginPeek调用(注意:不是BeginReceive方法,因为BeginReceive方法并不是用来处理事务性队列的),然后订阅PeekComplete事件,在事件处理过程中,再使用TransactionScope以及Receive等方法实现消息的获取。例如:

隐藏行号 复制代码 分布式事务中的MSMQ调用
  1. MessageQueue inputQueue = new MessageQueue("<queue connection>");
    
  2. inputQueue.PeekCompleted += (s, e) =>
    
  3.     {
    
  4.         using (TransactionScope transaction = new TransactionScope())
    
  5.         {
    
  6.             Message inputMsg = inputQueue.Receive(MessageQueueTransactionType.Automatic);
    
  7.             // do some work
    
  8.             transaction.Complete();
    
  9.         }
    
  10.         inputQueue.BeginPeek();
    
  11.     };
    
  12. inputQueue.BeginPeek();
    
  13. 
    

最后再提醒一下,就是如果你所要做的事情仅限于与MSMQ打交道,那么只要使用MSMQ的内部事务处理机制就可以了,毕竟使用分布式事务处理会涉及到MS-DTC,从而造成过大的系统开销,影响性能。

转载至:http://www.cnblogs.com/daxnet/archive/2011/03/15/1984995.html

发表在 C# | 留下评论

在Visual Studio 2010中使用Modeling Project定制DSL以及自动化代码生成

从Visual Studio 2010开始,有一个Modeling Project的项目模板,允许应用程序设计人员通过该项目完成统一的UML模型设计。与Visual Studio 2008 DSLTools相比,通过Modeling Project创建出来的UML模型对象,能够被使用到各个不同的UML视图中,这一功能是由UML Model Explorer维护的。我们可以看到,相同的UML模型对象,可以同时在Class Diagram以及Sequence Diagram中引用。从Visual Studio 2010开始,基于Visual Studio 2010 SDK的Visual Studio Visualization & Modeling SDK允许开发人员通过Domain Specific Language(DSL) Designer来设计开发自己的DSL,不过本文不会从Visualization & Modeling SDK着手来创建一个新的DSL,而是通过已有的Modeling Project来实现DSL的定制。

谈起DSL,或许有些朋友还不太了解。其实DSL就是一种应用于特定场景的语言,比如Entity Framework,它有一个Designer Surface,也就是我们平时所说的设计器,我们可以在设计器上设计各种类型以及类之间的关系,而整个设计器是面向Entity Framework这个特定应用场景的,例如我们可以在类的图形上设置这个类需要映射到数据库的哪张表,还可以在类的某个属性上标注它可以被映射到表里的哪个字段等等;相比之下,用于设计企业组织结构的设计器,就不会出现数据库、数据表、字段等这些概念,因为这些内容与企业组织结构扯不上什么关系。在Apworks框架开发的过程中,我意识到一个问题,就是能否通过使用Microsoft Visual Studio的功能,来图形化地设计我们的Domain Model,然后根据Domain Model来产生所需的C#/VB.NET代码。这样一来基于Apworks框架开发的Domain Model就可以根据图形化设计来自动生成代码。

通过本文的学习,你将了解到,如何使用Visual Studio 2010的Modeling Project来为自己的开发框架定制图形化的设计器并自动产生代码。在本文中,我将以Apworks为例,向大家介绍DSL的定制与代码自动化生成的过程。在开始学习以前,请确保你的电脑安装了Visual Studio 2010、Visual Studio 2010 SDK以及Visual Studio 2010 Visualization & Modeling SDK。

自定义UML Model的Profile以及stereotype

在图形元素上应用stereotype,就使得该元素能够表述一种特定的语义。例如,在一个图形元素上应用<<table>>这个stereotype,那么就可以认为该元素是一张数据表。在Visual Studio 2010 Modeling Project中,stereotype是在UML Model的Profile中定义的,而Profile最终会被应用在UML Model上。一个UML Model允许应用多个Profile。Visual Studio 2010 Modeling Project默认自带三种Profile:C# Profile、Standard Profile L2以及Standard Profile L3,如下图所示:

image

如果你需要使用VS2010 Visualization & Modeling Feature Pack提供的Generate Code命令来产生C#代码,那么你就需要在UML Model Explorer上对选中的Model采用C# Profile,并在表示“类”的图形上应用C# class这个stereotype。

现在,让我们考虑一下,基于Apworks框架的Domain Model,会有哪些种类的元素出现,比如,一个类可能会是一个聚合根,或者会是一个实体,或者会是一个领域事件;而类中的方法,可以是领域事件的处理过程。为了让Modeling Project能够支持针对Apworks框架的建模功能,我们需要新建一个Profile,并在Profile里定义一些必须的stereotype:Aggregate Root、Entity、Domain Event以及Domain Event Handler。Profile的新建非常简单,它本身就是一个扩展名为.Profile的XML文件,在此,我们可以为Apworks创建如下的Profile文件:

<?xml version="1.0" encoding="utf-8"?>
<profile xmlns="http://schemas.microsoft.com/UML2.1.2/ProfileDefinition" 
         dslVersion="1.0.0.0"
         name="ApworksDomainModelProfile"
         displayName="Apworks Domain Model Profile">
  <stereotypes>
    <stereotype name="entity" displayName="Entity">
      <metaclasses>
        <metaclassMoniker name="/ApworksDomainModelProfile/Microsoft.VisualStudio.Uml.Classes.IClass"/>
      </metaclasses>
    </stereotype>
    <stereotype name="sourcedAggregateRoot" displayName="Sourced Aggregate Root">
      <metaclasses>
        <metaclassMoniker name="/ApworksDomainModelProfile/Microsoft.VisualStudio.Uml.Classes.IClass"/>
      </metaclasses>
    </stereotype>
    <stereotype name="domainEvent" displayName="Domain Event">
      <metaclasses>
        <metaclassMoniker name="/ApworksDomainModelProfile/Microsoft.VisualStudio.Uml.Classes.IClass"/>
      </metaclasses>
    </stereotype>
    <stereotype name="aggregateRoot" displayName="Aggregate Root">
      <metaclasses>
        <metaclassMoniker name="/ApworksDomainModelProfile/Microsoft.VisualStudio.Uml.Classes.IClass"/>
      </metaclasses>
    </stereotype>
    <stereotype name="domainEventHandler" displayName="Domain Event Handler">
      <metaclasses>
        <metaclassMoniker name="/ApworksDomainModelProfile/Microsoft.VisualStudio.Uml.Classes.IOperation"/>
      </metaclasses>
      <properties>
        <property name="eventType" displayName="Event Type">
          <propertyType>
            <externalTypeMoniker name="/ApworksDomainModelProfile/System.String" />
          </propertyType>
        </property>
      </properties>
    </stereotype>
  </stereotypes>
  
  <metaclasses>
    <metaclass name="Microsoft.VisualStudio.Uml.Classes.IClass"/>
    <metaclass name="Microsoft.VisualStudio.Uml.Classes.IOperation" />
  </metaclasses>
  
  <propertyTypes>
    <externalType name="System.String" />
  </propertyTypes>
  
</profile>

Profile文件主要包含stereotypes、metaclasses以及propertyTypes三个子节点。stereotypes下定义了整个Profile包含的所有stereotype,每个stereotype通过metaclassMoniker指定了它能够被应用到哪些类型的设计图元素上,同时,还可以在stereotype中设置一些属性,比如上面的Domain Event Handler stereotype就有一个Event Type的属性,用来表示当前的Domain Event Handler所处理的事件类型。metacalsses节点包含了当前Profile中被用到的所有metaclass,而propertyTypes节点下则包含了当前Profile中所使用的所有属性类型。

Profile文件的部署与应用

在创建好Profile之后,我们需要将其部署到Visual Studio中,以便能够在UML Model上使用。这个过程我们可以通过创建Visual Studio Extension来完成。在Visual Studio 2010上选择File | New | Project菜单,在弹出的New Project对话框中选择Visual C# | Extensibility | VSIX Project模板,在Name文本框中输入项目名称然后单击OK按钮完成项目的创建。

image

在创建好的项目上单击右键,选择Add | Existing Item菜单,然后选中刚才我们新建的Profile文件,并将其Copy to Output Directory属性设置为Copy Always。在已打开的VSIX编辑器里输入必要的信息,如下:

image

在Content部分,单击Add Content按钮,在打开的Add Content对话框中:Select a content type选Custom Extension Type,Type文本框中输入Microsoft.VisualStudio.UmlProfile,Select a source中选File in project,然后选择刚才的Profile文件,并单击OK按钮将其添加进来。

编译VSIX项目,在编译输出目录下,找到.vsix文件并双击,此时会将我们创建的Visual Studio Extension安装到系统中:

image

重新启动Visual Studio 2010,新建一个Model Project,在UML Model的Profiles属性上单击下拉箭头,你将看到我们刚刚新建的Apworks Domain Model Profile:

image

现在,让我们选中Apworks Domain Model Profile,然后开始创建我们的Domain Model。为了节省篇幅,我简单地描述一下这个过程。比如需要创建一个名为Book的Aggregate Root,那么我们就在UML Model Explorer的UML Model上单击右键,选择 Add | Class 菜单,将新建的Class命名为Book,在其Stereotypes属性中同时选中C# class以及Aggregate Root:

image

之后,我们用相同的方法在UML Model中创建其它的元素,并将这些元素拖放到Class Diagram中,同时设置好各元素之间的关系(Visual Studio Modeling Project支持如下几种关系:关联、聚合、组合、依赖、继承以及包导入)。在完成了这些工作以后,我们得到了如下这样一个简单的Domain Model。从图中我们可以看到,Book和Category都被冠以Aggregate Root的stereotype。

image

基于T4的自动化代码生成

现在我们有了Domain Model,接下来就是自动化代码生成。我们可以使用T4来实现这一功能。在项目中单击右键,选择Add | New Item菜单,在打开的Add New Item对话框中,选择Visual C# Items | Text Template项目,输入名称后,单击Add按钮将其添加到项目中,同时将如下Assembly添加到项目引用中:Apworks.dll、Microsoft.VisualStudio.ArchitectureTools.Extensibility.dll以及Microsoft.VisualStudio.Uml.Interfaces.dll。

在T4文件中输入如下代码:

<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="System.Core" #>
<#@ Assembly Name="Microsoft.VisualStudio.ArchitectureTools.Extensibility.dll" #>
<#@ Assembly Name="Microsoft.VisualStudio.Uml.Interfaces.dll" #>
<#@ Import Namespace="Microsoft.VisualStudio.ArchitectureTools.Extensibility" #>
<#@ Import Namespace="Microsoft.VisualStudio.Uml.Classes" #>
<#@ Import Namespace="Microsoft.VisualStudio.ArchitectureTools.Extensibility.Uml" #>
<#@ Import Namespace="System.Linq" #>
<#@ Import Namespace="System.Text" #>

<#
	string modelPath = this.Host.ResolvePath(@"..\TinyLibraryCQRS\DomainModel.modelproj");
    using (IModelingProjectReader reader = ModelingProject.LoadReadOnly(modelPath))
    {
	    IModelStore store = reader.Store;
        foreach (IElement element in store.Root.OwnedElements)
        {
	        if (element is IClass)
            {
	            IClass classElement = element as IClass;
#>
<#= GetClassModifier(classElement) #>
{
	// TODO: Dump the attributes and operations...
}

<# } } } #>

<#+
private string GetClassModifier(IClass clz)
{
	System.Text.StringBuilder sb = new System.Text.StringBuilder();
    sb.Append("public ");
    if (Convert.ToBoolean(clz.AppliedStereotypes
        .Where(p => p.Name == "class")
        .First()
        .PropertyInstances
        .Where(p => p.Name == "IsPartial").First().Value) == true)
    {
	    sb.Append("partial ");
    }
    sb.Append("class ");
    sb.Append(clz.Name);
    if (clz.AppliedStereotypes.Any(p => p.Name == "aggregateRoot"))
    {
	    sb.Append(" : Apworks.AggregateRoot");
    }
    return sb.ToString();                                                                                                                    
}
#>

将代码保存后,就会相应地生成类似如下的C#代码:

public partial class Book : Apworks.AggregateRoot
{
	// TODO: Dump the attributes and operations...
}

public class Category : Apworks.AggregateRoot
{
	// TODO: Dump the attributes and operations...
}

有兴趣的读者可以继续完善上面的T4文本,进而根据Modeling Project生成类的字段、属性以及类与类之间的关联属性等。

基于CodeDom的自动化代码生成

T4的代码生成比较便捷,无需编写繁琐的CodeDom代码,但T4也有其限制,比如无法动态设置modelproj的位置,同一时间只能面向一种.NET语言,而且产品工具化也比较困难。为此,Apworks采用了基于CodeDom的源代码生成工具gcgc.exe(一个专为Apworks框架设计的代码产生工具,General Source Code Generator Collection),它能够根据XML生成配置代码,根据XSD生成类(类似xsd.exe工具的作用),也可以根据Modeling Project生成Domain Model的源代码。使用CodeDom的一个好处是,它使得开发工具能够同时支持多种.NET语言,而且部署起来会变得非常简单。gcgc.exe核心部分采用了我之前开发的Adaptive Console Framework,使得该应用程序能够非常方便地支持各种不同的源代码自动化生成引擎。以下是使用gcgc.exe根据Modeling Project来产生Domain Model源代码的使用情况:

image

产生的test.cs代码如下:

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.225
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------



/// <remarks />
public partial class Book : Apworks.AggregateRoot {
    
    private long idField;
    
    private string titleField;
    
    private string iSBNField;
    
    private int pagesField;
    
    /// <remarks />
    public virtual long Id {
        get {
            return this.idField;
        }
        set {
            this.idField = value;
        }
    }
    
    /// <remarks />
    public virtual string Title {
        get {
            return this.titleField;
        }
        set {
            this.titleField = value;
        }
    }
    
    /// <remarks />
    public virtual string ISBN {
        get {
            return this.iSBNField;
        }
        set {
            this.iSBNField = value;
        }
    }
    
    /// <remarks />
    public virtual int Pages {
        get {
            return this.pagesField;
        }
        set {
            this.pagesField = value;
        }
    }
}

/// <remarks />
public class Category : Apworks.AggregateRoot {
    
    private long idField;
    
    private string nameField;
    
    private System.Collections.Generic.List<Category> categoriesField;
    
    private System.Collections.Generic.List<Book> booksField;
    
    /// <remarks />
    public virtual long Id {
        get {
            return this.idField;
        }
        set {
            this.idField = value;
        }
    }
    
    /// <remarks />
    public virtual string Name {
        get {
            return this.nameField;
        }
        set {
            this.nameField = value;
        }
    }
    
    /// <remarks />
    public virtual System.Collections.Generic.List<Category> Categories {
        get {
            return this.categoriesField;
        }
    }
    
    /// <remarks />
    public virtual System.Collections.Generic.List<Book> Books {
        get {
            return this.booksField;
        }
    }
}

总结

本文简要地介绍了通过使用Visual Studio 2010 Modeling Project来定制DSL的过程,并对Modeling Project的自动化代码生成作了简要介绍。在引入这种技术后,开发人员可以很方便地使用Modeling Project开发出基于Apworks框架的领域模型,源代码可由T4或者Apworks工具gcgc.exe代为产生,从而减少了繁杂的代码录入工作,提高了软件项目的生产率。

转载至:http://www.cnblogs.com/daxnet/archive/2011/05/26/2059090.html

发表在 C# | 留下评论

Microsoft NLayerApp案例理论与实践 – 基础结构层(Cross-Cutting部分)

从这篇文章开始,我将逐步介绍NLayerApp的基础结构层、领域层、应用层以及分布式服务层。本文着重介绍基础结构层,根据上文对NLayerApp的架构分析,它将包含两大部分的内容:处理数据访问的基础结构层组件和Cross-Cutting的基础结构层组件。处理数据访问的基础结构层组件主要包含了仓储的具体实现、Unit Of Work(PoEAA,Martin Fowler)的实现、NLayerApp的实体模型定义,以及为单体测试做准备的Service Stubs(PoEAA,Martin Fowler);Cross-Cutting的基础结构层组件则主要包含了IoC(Inversion of Control)容器以及跟踪应用程序执行过程的Trace工具。虽然这些都是基础结构层的组件,但也包含了很多技术细节甚至是设计要点,就让我们一起对这些内容做一个详细的解读。

NLayerApp中IoC容器的实现

在应用程序设计的过程中,我们会基于这样一个设计准则,就是类型之间的关联应该依赖于接口或者抽象,而非具体的实现。这样就使得我们能够在保证整个程序结构不变的情况下,很方便地替换组件的具体实现方式,这不仅使得Service Stub模式的应用成为可能,从而提高了系统的可测试性,而且解耦了组件之间的依赖关系,降低了应用程序的维护成本。IoC容器是这样一种对象,它在应用程序的执行环境中维护着接口与其实现之间的映射关系,以及各个实现对象之间的依赖关系,以便当客户程序向IoC容器提出请求时,能够返回与所请求的接口或抽象类型所对应的具体实现,客户程序不需要去关心返回的具体实现究竟是什么,以及如何去初始化这个具体实现。本文不会对IoC作过多的介绍,有兴趣的朋友可以阅读《Inversion of Control Containers and the Dependency Injection pattern》这篇文章。

NLayerApp中IoC容器的实现依赖于Microsoft Patterns&Practices Unity,其实大多数应用程序甚至是开发框架都会依赖于第三方的类库来实现IoC容器,因为IoC本身涉及的内容就比较多,很好地解决类型之间复杂的依赖关系也不是一件很容易的事情。Unity并非IoC的唯一选择,除了Unity之外,Spring.NET、Castle Windsor、Ninject、StructureMap等都可以成为IoC容器不错的选择。NLayerApp中与IoC容器实现有关的类及其之间的关系如下图所示:

image

在上图中,IContainer接口定义了IoC容器相关的方法,它是与具体的实现技术无关的接口(接口的层次结构树、其中定义的方法的参数以及返回值等都不会依赖于任何第三方的组件),因此,理论上我们可以通过继承IContainer接口然后用我们自己的技术方式来实现IoC容器。NLayerApp是使用Unity作为IoC容器的,因此,上图的IoCUnityContainer类实现了IContainer接口,然后在IoCFactory的单件实例中通过new关键字创建了IoCUnityContainer的实例:

#region Constructor
/// <summary>
/// Only for singleton pattern, remove before field init IL anotation
/// </summary>
static IoCFactory() { }
IoCFactory()
{
    _CurrentContainer = new IoCUnityContainer();
}
#endregion

当然,对于NLayerApp这一特定的应用程序案例而言,这样做是没什么问题的,但如果我们目前设计的是一个开发框架的话,直接使用new关键字来创建IoCUnityContainer的实例,就会使得IoCFactory强行依赖于IoCUnityContainer类型,于是也就违背了“关联应该依赖于接口或者抽象,而非具体实现”的设计准则。在最新版的Apworks框架的代码中,开发人员可以通过应用程序的配置信息来选择合适的IoC容器,比如你可以在应用程序启动的时候就决定是使用Unity还是Castle Windsor,这就使得框架本身具有更好的扩展性。刚才我们也讨论过,如果要使NLayerApp能够使用我们自定义的IoC容器,就要继承IContainer接口,那么现在我们还需要修改IoCFactory的私有构造函数,以使用我们自己的IoC容器来初始化_CurrentContainer私有成员。

Unity容器有一个非常实用的特点,就是“根容器”与“子容器”的概念,在“根容器”上通过调用CreateChildContainer方法即可创建与之关联的子容器。根容器和子容器都可以接受抽象类型的注册。每当客户程序向子容器请求类型(Resolve Type)时,Unity首先检查子容器中是否有所请求的类型,如果有,则直接返回该类型的具体实现,如果没有,则会将该请求转发给其父容器。利用Unity的这种特性,我们可以将针对不同部署环境的IoC容器进行统一管理,比如将各种部署环境中相同的类型映射注册在根容器中,然后为每个部署环境创建一个子容器,将与部署环境相关的特定类型映射注册在各自的子容器中。下图展示了NLayerApp中Unity IoC容器的基本情况:

image

通过阅读IoCUnityContainer的源代码我们可以了解到,在IoCUnityContainer的构造函数中,创建了rootContainer,并在rootContainer上使用CreateChildContainer创建了用于真实运行环境的realAppContainer以及用于单体测试的fakeAppContainer,之后就是使用下面的私有方法逐个初始化这些容器:

/// <summary>
/// Configure root container.Register types and life time managers for unity builder process
/// </summary>
/// <param name="container">Container to configure</param>
void ConfigureRootContainer(IUnityContainer container)
{
    // Omitted... Please refer to the source code for details.
}

/// <summary>
/// Configure real container. Register types and life time managers for unity builder process
/// </summary>
/// <param name="container">Container to configure</param>
void ConfigureRealContainer(IUnityContainer container)
{
    container.RegisterType<IMainModuleUnitOfWork, MainModuleUnitOfWork>(new PerExecutionContextLifetimeManager(),
        new InjectionConstructor());
}

/// <summary>
/// Configure fake container.Register types and life time managers for unity builder process
/// </summary>
/// <param name="container">Container to configure</param>
void ConfigureFakeContainer(IUnityContainer container)
{
    //Note: Generic register type method cannot be used here, 
    //MainModuleFakeContext cannot have implicit conversion to IMainModuleContext

    container.RegisterType(typeof(IMainModuleUnitOfWork), 
        typeof(FakeMainModuleUnitOfWork), new PerExecutionContextLifetimeManager());
}

在ConfigureRootContainer方法中,对所有环境(真实运行环境以及单体测试环境)需要用到的类型进行了注册,然后,就IMainModuleUnitOfWork而言,由于真实运行环境和单体测试环境所使用的Unit Of Work具体实现不同:真实运行环境使用的是MainModuleUnitOfWork实现,而测试环境则是使用的FakeMainModuleUnitOfWork,于是,也就在ConfigureRealContainer和ConfigureFakeContainer方法中分别作了注册。

最后,每当IContainer.Resolve方法被调用时,系统会通过读取配置文件来决定目前应该使用哪个容器来解析类型,因此,我们只需要在配置文件中正确设置容器的名称,即可在NLayerApp中使用指定的Unity IoC容器。下面这段配置信息来自于DistributedServices.Deployment项目,从中我们可以看到,NLayerApp的Distributed Services使用的是realAppContainer:

<appSettings>
  <!--RealAppContext - Real Container-->
  <!--FakeAppContext - Fake Container-->
  <!--<add key="defaultIoCContainer" value="FakeAppContext" />-->
  <add key="defaultIoCContainer" value="RealAppContext" />
</appSettings>

NLayerApp中用于跟踪程序执行过程的Trace工具

NLayerApp中Trace工具的实现非常简单,在Infrastructure.CrossCutting项目中定义了ITraceManager,然后在Infrastructure.CrossCutting.NetFramework项目中定义了ITraceManager的具体实现。TraceManager使用了System.Diagnostics命名空间下与Trace相关的类型实现其功能,应用程序则通过IoCFactory来获得ITraceManager的具体实现。

在上面讨论的ConfigureRootContainer方法中,NLayerApp对ITraceManager类型进行了注册:

//Register crosscuting mappings
container.RegisterType<ITraceManager, TraceManager>(new TransientLifetimeManager());

因此,在整个应用程序中,就可以使用下面的方式来获取ITraceManager的具体实现,以便完成Trace功能:

ITraceManager traceManager = IoCFactory.Instance.CurrentContainer.Resolve<ITraceManager>();
traceManager.TraceError(/* error message*/);

总结

本文对NLayerApp的基础结构层(Cross-Cutting部分)进行了研究与探讨,与这部分相关的项目有:Infrastructure.CrossCutting、Infrastructure.CrossCutting.IoC以及Infrastructure.CrossCutting.NetFramework。下一讲我们将继续研究NLayerApp的基础结构层(数据访问部分)。

转载至:http://www.cnblogs.com/daxnet/archive/2011/06/01/2067134.html

发表在 C# | 留下评论

Microsoft NLayerApp案例理论与实践 – 基础结构层(数据访问部分)

上篇文章讲解了NLayerApp案例的基础结构层(Cross-Cutting部分),现在,让我们继续解读NLayerApp的基础结构层(数据访问部分)。NLayerApp的基础结构层(数据访问部分)包含如下内容:Unit Of Work(PoEAA)、仓储的具体实现、NLayerApp的数据模型以及与测试相关的类。下面,我们将对前三个部分进行讨论,与测试相关的内容,我打算最后单独一章进行介绍。

Unit Of Work(PoEAA)

Unit Of Work(UoW)模式在企业应用架构中被广泛使用,它能够将Domain Model中对象状态的变化收集起来,并在适当的时候在同一数据库连接和事务处理上下文中一次性将对象的变更提交到数据中。在没有引入UoW之前,你可以在每次增加、删除对象或者更改对象状态后,直接调用数据库以保存对象的变化,但这样做会导致应用程序对数据库这一外部技术架构的频繁访问,严重影响了系统性能。这就好像我们打开Notepad进行文字编辑一样,我们完全可以每输入一个字符,就按下Ctrl+S保存一次,但这样做非常耗时(也没必要),我们通常的做法可能是,每完成一个段落的编辑(输入字符、删除字符或者更改字符等)再保存一次,那么Notepad就会在我们编辑段落的时候跟踪段落及其中字符的变化情况,最后一次性将这些变更写到硬盘上。从UoW的模式描述上看,它有点像数据库事务(Transaction),因为它们都具有“提交”和“回滚”的操作。但从语义上讲,它并不能等同于数据库事务。我觉得应该这样理解:我们可以将UoW看成是一个事务对象,但它不是数据库事务,它的事务性体现在能够在一个原子操作中将对象一次性提交给持久化机制,或者如果在提交过程中出现问题,它还能将对象返回到提交前的状态。不仅如此,UoW还具有跟踪领域对象变化的功能,它能够跟踪某一个业务步骤范围内领域对象的变化情况,正如上面的例子中,每个段落的编辑就可以看成是一个业务步骤,那么在这个业务步骤中(编辑段落的过程中),UoW会对领域对象进行跟踪,而在业务步骤完成之时(完成段落编辑之时),UoW就会对跟踪到的变更做一次性提交。

从上面的分析让我们大致了解到,UoW与仓储一样,本身应该是属于Domain Model的,它的设计应该是技术无关的(也就是常说的POCO或者IPOCO),因为它跟踪的是Domain Model中领域对象的变化情况;当然,一个更好的设计应该是使用Separated Interface(PoEAA)模式,将UoW接口与仓储的接口一起设计在Domain Model中。从UoW的实现上来看,NLayerApp采用了Entity Framework的一些特性,并基于Entity Framework的模型,利用T4自动化产生代码。目前我们不要去关心在NLayerApp中是如何使用T4产生这些代码的,我们需要关心的是为什么需要产生这些代码。有关Visual Studio中的模型项目、Domain Specific Language(DSL)以及T4代码自动化生成,我们在此将不作讨论。有兴趣的朋友可以参考我前面的文章《在Visual Studio 2010中使用Modeling Project定制DSL以及自动化代码生成》。以下是NLayerApp中与UoW相关的类关系图:

image

在了解NLayerApp的UoW执行机制之前,首先让我们了解一下NLayerApp中与UoW相关的三个接口。

  • IObjectWithChangeTracker接口
    该接口下只定义了一个ObjectChangeTracker的属性,在NLayerApp中,所有的实体都要实现IObjectWithChangeTracker接口,以向外界(主要是UoW和仓储)提供ObjectChangeTracker实例。ObjectChangeTracker的主要功能就是记录当前实体中的状态变化。比如,实体的当前状态、变更前所有属性的原始数据、向集合属性添加的所有对象、从集合属性中删除的所有对象等等。当仓储通过Unit Of Work来注册已变更的实体时,Unit Of Work会使用ObjectChangeTracker所提供的信息来向Entity Framework进行变更注册。
  • INotifyPropertyChanged接口
    NLayerApp的实体不仅实现了IObjectWithChangeTracker接口,同时还实现了INotifyPropertyChanged接口。实现这个接口的主要目的就是为了在实体的某个属性发生变化时,能及时地将这种变化记录在ObjectChangeTracker中。因此,只要客户程序通过实体的属性来改变实体的状态时,实体本身就会将状态变化记录到ObjectChangeTracker中。
  • IRepository接口
    IRepository接口是定义在Domain Model层的接口,之所以在此提及,是因为对象的持久化过程是通过仓储完成的,而持久化又离不开UoW。在NLayerApp中,IRepository接口有一个IUnitOfWork的属性,因此所有的仓储都必须实现这个属性,以便Repository能够在UoW中记录对象的变更信息。从NLayerApp的源代码可以看到,其实仓储本身并不负责将实体保存到数据库的这一具体任务,它只是通过IObjectWithChangeTracker接口,将需要保存的对象设置为相应的状态,并向UoW注册对象变更;剩下的与数据库打交道的任务,则是由UoW完成的

通过这些信息我们可以了解到,NLayerApp中的实体都是各自管理自己的变更记录,称之为“自跟踪实体”(Self-Tracking Entities,STE)。其实从DDD的角度来看,STE并不是一个很好的设计,因为它给Domain Model带来了太多技术关注点。例如在实现STE的时候,当你向Customer添加一个Order时,你需要首先判断Customer的ObjectChangeTracker中是否已经将该Order标记为“删除”状态了,如果是这样的话,那么你需要将这个Order从ObjectChangeTracker的“删除”列表中移去。类似这样的业务逻辑本不应该放在Domain Model中。此外,NLayerApp为了迎合Entity Framework的需求,所实现的STE也并非纯粹的与技术无关的。UoW的实现也是如此,比如在上面的类图中,我们可以很明显地看到,MainModuleUnitOfWork是ObjectContext的子类。

现在我们将思路串联起来,以修改Customer为例,从整个架构服务端的最上层(Distributed Service层)开始,看看Unit Of Work与仓储是如何协作的。

1、DistributedServices.MainModule项目:MainModuleService类通过使用位于应用层的CustomerManagementService实现Customer信息的变更:

public void ChangeCustomer(Customer customer)
{
    try
    {
        //Resolve root dependency and perform operation
        ICustomerManagementService customerService = IoCFactory
            .Instance
            .CurrentContainer.Resolve<ICustomerManagementService>();
        customerService.ChangeCustomer(customer);
    }
    catch (ArgumentNullException ex)
    {
        // ......
    }

}

上述代码通过IoCFactory从IoC容器中获得ICustomerManagementService的具体实现,有关NLayerApp中IoC容器的实现,请参考前一篇文章。

2、Application.MainModule项目:CustomerManagementService类实现了ICustomerManagementService接口,同时实现了ChangeCustomer方法。在该方法中,首先通过CustomerRepository的UnitOfWork属性获得UoW,然后调用仓储的Modify方法以将要更改的Customer实体注册到UoW中,同时改变了Customer实体的状态。最后,使用UoW的CommitAndRefreshChanges方法将变更的实体对象提交到数据库:

public void ChangeCustomer(Customer customer)
{
    if (customer == (Customer)null)
        throw new ArgumentNullException("customer");

    IUnitOfWork unitOfWork = _customerRepository.UnitOfWork as IUnitOfWork;

    _customerRepository.Modify(customer);

    unitOfWork.CommitAndRefreshChanges();

}

值得一提的是,在CustomerManagementService中,CustomerRepository以构造器注入的方式获得实例化的:

/// <summary>
/// Create new instance 
/// </summary>
/// <param name="customerRepository">Customer repository dependency, 
/// intented to be resolved with dependency injection</param>
/// <param name="countryRepository">Country repository dependency, 
/// intended to be resolved with dependency injection</param>
public CustomerManagementService(ICustomerRepository customerRepository, 
    ICountryRepository countryRepository)
{
    if (customerRepository == (ICustomerRepository)null)
        throw new ArgumentNullException("customerRepository");

    if (countryRepository == (ICountryRepository)null)
        throw new ArgumentNullException("countryRepository");

    _customerRepository = customerRepository;
    _countryRepository = countryRepository;
}

3、Infrastructure.Data.Core项目:Repository类的Modify方法首先将当前状态不是Deleted的实体设置为“Modified”,同时在UoW中,通过RegisterChanges调用以向UoW注册该实体:

public virtual void Modify(TEntity item)
{
    //check arguments
    if (item == (TEntity)null)
        throw new ArgumentNullException("item", Resources.Messages.exception_ItemArgumentIsNull);

    //Set modifed state if change tracker is enabled and state is not deleted
    if (item.ChangeTracker != null
        &&
        ((item.ChangeTracker.State & ObjectState.Deleted) != ObjectState.Deleted)
       )
    {
        item.MarkAsModified();
    }
    //apply changes for item object
    _CurrentUoW.RegisterChanges(item);

    _TraceManager.TraceInfo(
                   string.Format(CultureInfo.InvariantCulture,
                                Resources.Messages.trace_AppliedChangedItemRepository,
                                typeof(TEntity).Name));
}

4、Infrastructure.Data.MainModule项目:MainModuleUnitOfWork类的RegisterChanges方法简单地利用Entity Framework所提供的机制,向Entity Framework注册对象状态变更。这是Entity Framework技术实现的细节内容,我们在此也不去深入分析其中的实现方式了:

public void RegisterChanges<TEntity>(TEntity item)
	where TEntity : class, IObjectWithChangeTracker
{
	this.CreateObjectSet<TEntity>().ApplyChanges(item);
}

5、Infrastructure.Data.MainModule项目:MainModuleUnitOfWork类的CommitAndRefreshChanges方法通过Entity Framework将变更提交到数据库,同时将实体对象的状态设置为“未更改”:

public void CommitAndRefreshChanges()
{
    try
    {
        //Default option is DetectChangesBeforeSave
        base.SaveChanges();

        //accept all changes in STE entities attached in context
        IEnumerable<IObjectWithChangeTracker> steEntities = (from entry in
            this.ObjectStateManager
            .GetObjectStateEntries(~EntityState.Detached) where
                entry.Entity != null &&
                (entry.Entity as IObjectWithChangeTracker != null)
        select
            entry.Entity as IObjectWithChangeTracker);

        steEntities.ToList().ForEach(ste => ste.MarkAsUnchanged());
    }
    catch (OptimisticConcurrencyException ex)
    {
        //......
    }
}

整个执行过程我们可以使用下面的序列图来表示:

image

NLayerApp中的Unit Of Work我们先介绍到这里,有疑问的朋友可以以评论的方式交流。

 

仓储的具体实现

NLayerApp中的仓储实现也是基础结构层(数据访问部分)的一个重要组件,这一点与DDD的经典架构风格是相符的。因为从理论上讲,仓储的具体实现需要依赖于外部系统,而这部分内容是不能暴露给Domain Model层的,也就是我们平时所说的,需要做到Persistence Ignorance。NLayerApp首先为所有实体(确切地说应该是聚合根)设计了一个通用的泛型仓储,你可以在Infrastructure.Data.Core项目中找到这个泛型仓储的源代码,它实现了一个仓储应具有的所有基本功能,比如添加、删除、修改实体对象以及基于规约的一些查询操作等;然后,针对某些聚合根,NLayerApp会根据项目的实际需求,在仓储中实现一些特定的操作。比如:CustomerRepository继承于Repository这个通用仓储,同时实现了ICustomerRepository接口,以向外界提供通过规约(Specification)来查找Customer信息的功能。这样的设计在一定程度上做到了关注点分离,比如当我们对实体进行通用的仓储操作时,我们只需要获得IRepository接口的具体实现即可,而无需使用ICustomerRepository来获得与Customer有关的仓储实现。有关ICustomerRepository与关注点分离的相关内容,我将在下一讲(领域模型层)进行讲解。

以下是NLayerApp中仓储的类关系图,在此贴出以供读者参考。

image

NLayerApp的仓储实现也使用了不少与Entity Framework相关的技术细节,比如ObjectSet等,这些都是具体技术实现上的内容,在此就不多作介绍了。有兴趣的读者请参考与Entity Framework技术相关的资料文档。

 

NLayerApp的数据模型

NLayerApp使用Entity Framework的ADO.NET Entity Data Model设计器来设计数据模型,这使得我们能够对整个Domain Model的对象结构有一个很直观的认识。该数据模型位于Infrastructure.Data.MainModule项目下,直接双击MainModuleDataModel.edmx就可以在设计器中打开,对象结构及其之间的关系就能很清楚地展现在你面前。你会发现,其实在这个数据模型的后台代码文件中,除了一些注释以外,并没有任何实质性内容,这是因为NLayerApp仅仅是利用这个设计器来设计数据模型,而真正的Domain Model的代码则会在Domain Model层中,根据该数据模型,利用T4进行自动化生成,详情请见Domain.MainModule.Entities项目。这也使得我们会去思考这样一个纠结的问题:Entity Framework为我们提供的,到底是一个面向数据库设计的数据模型,还是面向领域驱动的领域模型?或许在实际应用中,我们更多地是将其放在ORM的位置上,于是Entity Data Model就变成了位于Domain Model实体对象与数据库之间的行数据入口(Row Data Gateway,PoEAA)。之前我对于基于Entity Framework的领域驱动设计实践也写过一些文章,读者朋友可以参考《领域驱动设计系列文章汇总》。

总结

本文对NLayerApp的基础结构层(数据访问部分),尤其是Unit Of Work的实现进行了分析与介绍;下一讲开始,我们将一起学习NLayerApp的Domain Model部分。

参考阅读

转载至:http://www.cnblogs.com/daxnet/archive/2011/06/03/2071931.html

发表在 C# | 留下评论

Microsoft NLayerApp案例理论与实践 – 领域模型层

本文将重点介绍Microsoft NLayerApp的领域模型层,这涉及到Domain.CoreDomain.Core.EntitiesDomain.MainModule以及Domain.MainModule.Entities四个项目。Domain.Core项目包含了基本接口的定义以及规约模式(Specification Pattern)的实现;Domain.Core.Entities则包含了支持Entity Framework的STE(Self-Tracking Entity)的实现代码,在上文《Microsoft NLayerApp案例理论与实践 – 基础结构层(数据访问部分)》我对STE做了一些介绍,但它的实现与Entity Framework(EF)结合的比较紧密,EF超出了本系列文章的讨论范围,因此,我们也不会针对STE的具体实现方式做太多讨论;Domain.MainModule根据项目需求,针对不同的实体定义了仓储接口,同时实现了项目所需的规约类型。领域服务也是该项目的重要部分;Domain.MainModule.Entities项目中包含了NLayerApp领域模型的核心代码。本文将从仓储接口、规约、领域服务、领域模型这四个方面对NLayerApp的Domain Model层做一个简单的介绍。

仓储接口

根据我们在《Microsoft NLayerApp案例理论与实践–DDD、分布式DDD及其分层》一文中的讨论,仓储的具体实现是放在基础结构层的,而仓储的接口则是放在领域模型层的。Domain.Core项目的IRepository接口就是仓储接口,所有的仓储类都需要实现该接口中定义的属性与方法。在Domain.Core项目下还有一个继承IRepository接口的IExtendedRepository接口,它包含了一些额外的方法来扩展IRepository的功能。事实上在整个NLayerApp中并没有真正用到IExtendedRepository接口,因此我们也不在此做过多讨论。下图是NLayerApp中与仓储的接口和实现相关的类关系图,为了方便浏览和描述,该图中仅包含了Customer仓储的定义与实现部分:

image

首先,ICustomerRepository接口继承于IRepository接口,以扩展IRepository来定义特定于Customer实体的仓储。因此,所有实现ICustomerRepository接口的类,不仅具备仓储的基本功能,而且还具有特定于Customer实体的仓储操作。其次,Repository类实现了IRepository接口,并作为所有仓储实现的基类,实现了IRepository接口中定义的方法,它在仓储部分的角色就是一个层超类型(Layer Supertype)。最后,CustomerRepository类继承于Repository类,同时实现了ICustomerRepository接口,由于Repository类中已经实现了IRepository中定义的所有方法,因此CustomerRepository类就无需去实现这些方法,只需要把关注点放在ICustomerRepository的实现上即可。以下是位于基础结构层的CustomerRepository代码,供读者朋友参考:

public class CustomerRepository
    :Repository<Customer>,ICustomerRepository
{
    #region Constructor
    /// <summary>
    /// Default constructor
    /// </summary>
    /// <param name="traceManager">Trace manager dependency</param>
    /// <param name="unitOfWork">Specific unitOfWork for this repository</param>
    public CustomerRepository(IMainModuleUnitOfWork unitOfWork, ITraceManager traceManager) 
        : base(unitOfWork, traceManager) { }
    #endregion

    #region ICustomerRepository implementation
    /// <summary>
    /// <see cref="Microsoft.Samples.NLayerApp.Domain.MainModule.Customers.ICustomerRepository"/>
    /// </summary>
    /// <param name="specification">
    /// <see cref="Microsoft.Samples.NLayerApp.Domain.MainModule.Customers.ICustomerRepository"/>
    /// </param>
    /// <returns>Customer that match <paramref name="specification"/></returns>
    public Customer FindCustomer(ISpecification<Customer> specification)
    {
        //validate specification
        if (specification == (ISpecification<Customer>)null)
            throw new ArgumentNullException("specification");

        IMainModuleUnitOfWork activeContext = this.UnitOfWork as IMainModuleUnitOfWork;
        if (activeContext != null)
        {
            //perform operation in this repository
            return activeContext.Customers
                                .Include(c => c.CustomerPicture)
                                .Where(specification.SatisfiedBy())
                                .SingleOrDefault();
        }
        else
            throw new InvalidOperationException(string.Format(
                CultureInfo.InvariantCulture,
                Messages.exception_InvalidStoreContext,
                this.GetType().Name));
    }
    #endregion
}

正如上图所述,ICustomerRepository接口扩展了IRepository接口以提供与Customer有关的仓储操作。对于应用程序开发框架来说,这样的设计有助于提高系统的扩展性。比如之前有网友针对Apworks框架提问,觉得Apworks的仓储接口只提供了一些很基本的操作,但他希望能够在仓储上增加一些诸如分页查询对象的操作,之前他的设计是,另外定义一个接口(IFooRepository),其中添加一些分页查询操作,然后让仓储实例同时实现IRepository和IFooRepository。如下:

image

这样做看上去FooRepository是一个完整的仓储实现,但IFooRepository与IRepository之间没有任何联系,IFooRepository本身并没有体现“仓储”的语义,但它原本就是一种仓储。从实践上看,我们需要在IoC容器中分别为IRepository和IFooRepository注册相同的类型:FooRepository,以便在程序中能够正确地解析IRepository和IFooRepository的具体实现,从而通过IRepository或者IFooRepository分别获得不同的仓储操作。当然,对于我们目前的情形,FooRepository同时实现IRepository和IFooRepository接口,那么C#是可以通过as关键字将该实例在IRepository和IFooRepository的实例间进行转换的,比如:

IContainer container = IoCFactory.Instance.CurrentContainer;
using (IRepositoryContext ctx = container.Resolve<IRepositoryContext>())
{
    IRepository<Foo> repository = ctx.GetRepository<Foo>();
    // do sth. with repository ...
    IFooRepository<Foo> fooRepository = repository as IFooRepository<Foo>();
    if (fooRepository != null) // this is required...
    {
        // do sth. with fooRepository
    }
}

但是在应用程序开发的过程中,我们无法去约束开发人员一定要让FooRepository去实现IFooRepository接口,这就造成了上面的类型转换不成功,因此,判断fooRepository实例是否为空就显得非常重要。

这样的设计还有另外一个缺陷,就是由于IFooRepository没有体现“仓储”的语义,这就导致它无法应用到基于仓储的类型约束上。例如,假设根据需求我们需要用到一个接口IMyInterface,它的定义如下:

interface IMyInterface<T, S>
    where T : IRepository<S>
    where S : class
{    }

那么很明显我们就无法去定义一个类,在这个类中通过泛型参数T来使用IFooRepository接口:

// error:
class MyClass : IMyInterface<IFooRepository<MyEntity>, MyEntity>
{    }

相比之下,NLayerApp用了一个从语义上来讲更为合理的设计(如下图),它充分体现了“IFooRepository是一种仓储”的概念,总之,两种不同的设计的主要区别在各自所表达的面向对象语义上。

image

规约(Specification)

Domain.Core项目下,NLayerApp定义了应用程序领域模型层所需要用到的规约框架,主要是通过LINQ Expression来实现的。在ISpecification接口中定义了SatisfiedBy方法,该方法返回一个LINQ Expression,用来执行判断领域对象是否能够满足当前规约条件的逻辑。NLayerApp的规约结构如下图所示:

Specification

有关规约模式,请参见:《Specifications》、《Specification Pattern》;有关规约模式、应用场景以及支持LINQ Expression的.NET规约实现,请参见:《EntityFramework之领域驱动设计实践(十):规约(Specification)模式》。本文就不再重复这些内容了。

值得一提的是,NLayerApp的规约实现,在Specification抽象类中重载了一些逻辑运算符,这使得在实际应用中使用规约变得非常方便。

领域服务(Domain Services)

在DDD中,“服务”的概念得到了扩展,它表示在任何层中,包含了这样一种操作的类型,这种操作从逻辑上无法归结到任何对象上。因此“服务”并不仅仅是应用层或者基础结构层的专利,领域模型中也存在服务。在我的《EntityFramework之领域驱动设计实践【扩展阅读】:服务(Services)》一文中,对领域服务做了简单的介绍,供读者朋友参考。就NLayerApp而言,它实现了一个Bank Transfer的服务,首先定义了IBankTransferDomainService的接口,然后由BankTransferDomainService实现该接口。服务执行的参与者就是两个BankAccount实体,参数就是需要转账的金额。在Application层,BankingManagementService的PerformTransfer方法就使用了该服务来实现银行账户转账。

领域模型(Domain Model)

之前我也提到过,NLayerApp的领域模型是根据Entity Framework的Data Model,通过T4自动生成的,代码中除了包含了Data Model本身所定义的对象属性及对象间的关系外,还包含了基于Entity Framework实现STE的代码。从严格上讲,这并不是一个纯净的领域模型,其中STE的实现牵涉到了很多技术(而非领域)实现细节;此外,所有的领域对象都被DataContract修饰,也就意味着它们将同时以DTO的身份穿梭在网络中。NLayerApp的官方资料中对这种实现有过说明,解释过这种做法并不是很好的DDD实践,但它能够适用于NLayerApp。另外,NLayerApp采用C#的partial关键字向领域对象中添加了业务方法,Domain.MainModule.Entities项目下Partial子目录中包含了这些代码,比如在Order实体上实现了GetNumberOfItems操作。这一点与我以前在《EntityFramework之领域驱动设计实践 (一):从DataTable到EntityObject》一文中讨论的思路是相同的。在此,我们也不对NLayerApp的领域逻辑实现过程做太多介绍,有问题的朋友可以通过留言进行讨论。

总结

本文对NLayerApp的领域模型层做了简单的介绍,尤其对仓储接口的设计做了详细讨论。下篇文章我将介绍NLayerApp的应用层。

 

转载至:http://www.cnblogs.com/daxnet/archive/2011/06/07/2074451.html

发表在 C# | 留下评论

[设计模式]在CodeDom代码生成中使用Decorator模式实现类型创建

我估计从博客园建站开始,就不断地有文章对设计模式进行讨论了。设计模式被认为是软件工程的基础,是面向对象分析与设计的指南。博客园中不乏大量的优秀文章,针对设计模式中创建型模式、结构型模式以及行为型模式共计23种模式进行讨论,有的文章也自成体系,以不同的角度来分析各种模式的应用场景和动态特性。今天,我也在所有设计模式专家面前班门弄斧一次,冒着被喷的危险,谈谈Decorator模式的具体应用。与大多数其它介绍设计模式的文章相比,本文介绍的案例真正来自实践。另外,对于设计模式,我不会像之前《Entity Framework之领域驱动设计实践》、《Microsoft NLayerApp案例理论与实践》那样特地开个专题去逐个介绍,我会根据实际项目中的使用情况来做针对性分析。比如单件模式,应用范围很广,而且结构简单,大家都知道应该在什么场合应用单件模式,我也就不会再费时间去做重复介绍了。

问题提出

在CodeDom代码生成中使用Decorator模式实现类型创建,这个话题来自我之前的一篇文章(《在Visual Studio 2010中使用Modeling Project定制DSL以及自动化代码生成》)所讨论的内容,文章最后介绍了使用CodeDom来根据Modeling Project自动化产生代码的方式,于是,本文所讨论的问题就来自于这个CodeDom实现代码生成的过程。首先还是回顾一下之前那篇文章中,我们所设计好的一个非常简单的模型类图:

这个模型类图很简单,就包含两个聚合根(Aggregate Root):图书(Book)以及分类(Category)。Category与图书之间是聚合的1:N的关系,而Category本身跟自己也是聚合的1:N的关系,这表明Category本身还可以包含多个Category。现在,我们就需要创建一个工具,它能够根据这个模型类图,通过使用CodeDom技术来自动化产生类的代码。要实现这个工具,就会牵涉到Modeling Project的读入、Modeling Project的Profile、Stereotypes以及CodeDom的具体技术细节,本文不对这些内容做太多介绍,朋友们还是到我之前写的这篇文章去了解这部分内容。

在使用CodeDom生成代码的时候,首先是创建CodeCompileUnit实例,然后创建CodeNamespace实例,并在CodeNamespace中添加CodeTypeDeclaration实例以向CodeNamespace创建类型定义,最后,VBCodeProvider、CSharpCodeProvider或者JScriptCodeProvider就可以根据CodeCompileUnit实例来实现代码生成。假设我们现在需要生成Modeling Project中的某个类,我们可能会使用下面的方式:

private CodeTypeDeclaration GenerateRegularClass(IClass clazz)
{
    CodeTypeDeclaration codeTypeDeclaration = new CodeTypeDeclaration();
    // 1. 创建类的声明部分
    // 2. 处理属性的生成部分
    // 3. 处理关联的生成部分
    // 4. 处理接口实现部分
    // 5. 处理类继承部分
    return codeTypeDeclaration;
}

 

而对于Book这样的聚合根,它又有自己的特性,比如它需要继承于AggregateRoot类,并在其中实现相应的方法,于是,我们就:

private CodeTypeDeclaration GenerateAggregateRootClass(IClass clazz)
{
    CodeTypeDeclaration codeTypeDeclaration = this.GenerateRegularClass(clazz);
    // 在此处理聚合根的特性
    return codeTypeDeclaration;
}

 

事实上Modeling Project不仅仅包含了聚合根这种stereotype,还包含了诸如实体(Entity)、领域事件(DomainEvent)等stereotypes,要针对应用了这些stereotype的类创建类型定义,我们就要写一些类似GenerateEntityClass、GenerateDomainEventClass这样的方法。更进一步,如果某个类型需要在聚合根的类的基础上,再增加一些特殊的代码成分,那么在生成代码的时候,就需要首先调用GenerateAggregateRootClass方法获得聚合根的类型定义,再针对这些特殊的成分做进一步处理。如此一层套一层,不仅使得代码逻辑变得非常复杂,而且在对某种stereotype做进一步扩展或自定义的时候,会很不灵活。

分析问题

从上面的问题我们可以看到,在针对某个类创建类型定义的时候,首先通过new关键字创建了CodeTypeDeclaration的实例,然后就一步步地向CodeTypeDeclaration添加所需的代码成分,比如类的声明、属性、继承等等,进而对于应用了stereotype的类,再添加特定的代码生成成分。于是从CodeTypeDeclaration的角度看,它经历了类似下面的过程:

image

由此可见,CodeTypeDeclaration实例中的内容,是逐步“润色”上去的,我们可以套用Decorator模式来完成CodeTypeDeclaration的创建。

模式应用

以下是实现Modeling Project中类型代码自动生成相关的类图,为了节省篇幅,使读者看得更清晰,图中省略了几个Decorator的具体实现,但不影响对整个结构的理解。

t

相关代码如下:

interface IClassTypeGenerator
{
    void Generate(IClass clazz, CodeTypeDeclaration codeTypeDeclaration);
}

abstract class ClassTypeGenerator : IClassTypeGenerator
{
    public abstract void Generate(IClass clazz, CodeTypeDeclaration codeTypeDeclaration);
}

class ClassTypeDeclarationGenerator : IClassTypeGenerator
{
    public ClassTypeDeclarationGenerator() {  }
    public void Generate(IClass clazz, CodeTypeDeclaration codeTypeDeclaration)
    {
        // generate class declaration
    }
}

class ClassTypePropertyGenerator : ClassTypeGenerator
{
    private readonly IClassTypeGenerator generator;

    internal ClassTypePropertyGenerator(IClassTypeGenerator generator)
    {
        this.generator = generator;
    }

    public override void Generate(IClass clazz, CodeTypeDeclaration codeTypeDeclaration)
    {
        this.generator.Generate(clazz, codeTypeDeclaration);
       // generate properties
    }
}

class ClassTypeApworksAggregateRootGenerator : ClassTypeGenerator
{
    private readonly IClassTypeGenerator generator;

    internal ClassTypeApworksAggregateRootGenerator(IClassTypeGenerator generator)
    {
        this.generator = generator;
    }

    public override void Generate(IClass clazz, CodeTypeDeclaration codeTypeDeclaration)
    {
        this.generator.Generate(clazz, codeTypeDeclaration);
        // generate AggregateRoot facilities
    }
}

 

那么在创建AggregateRoot的类型定义时,就可以:

CodeTypeDeclaration codeTypeDeclaration = new CodeTypeDeclaration();
new ClassTypeApworksAggregateRootGenerator(
    new ClassTypePropertyGenerator(
        new ClassTypeDeclarationGenerator())).Generate(clazz, codeTypeDeclaration);

 

与Builder模式的比较

设计模式将Builder模式归类到创建型模式,而将Decorator模式归类到结构型模式。而在我们的例子中,我们是使用了Decorator模式来创建(严格地说应该是逐步填充)了CodeTypeDeclaration对象。Builder模式用来创建具有特定结构的对象,而对象结构的组成部分可以有不同的实现方式;而Decorator模式则更擅长于向已创建的对象上填充内容。

转载至:http://www.cnblogs.com/daxnet/archive/2011/06/09/2076240.html

发表在 C# | 留下评论

Microsoft NLayerApp案例理论与实践 – 应用层

NLayerApp中,在领域模型层之上是应用层与分布式服务(Distributed Services)部分。应用层主要负责接收来自客户端的请求数据,然后协调领域模型层与基础结构层组件完成语义上相对独立的任务;而分布式服务部分则为应用层与客户端之间提供通讯的接口和技术架构,严格地说它已经不具备任何任务处理的责任了,在整个应用程序中是一个可有可无的角色:对于ASP.NET Web应用程序而言,它只需要引用应用层组件的接口,然后通过IoC获得应用层组件实体即可,无需分布式服务的支持。当然,如果还需要考虑与其它系统的集成的话,那么实现一个分布式服务还是很有必要的。今天我们先讨论NLayerApp中的应用层。NLayerApp在应用层中将服务(Application Service)分为三种:Banking Management、Customers Management以及Sales Management。这可以从Application.MainModule.csproj项目中看出。在每种应用服务中,首先为该种服务定义了接口,比如IBankingManagementService等,然后使用相应的类实现了这些接口。从结构上看,还是比较简单的,本文也不再对其中每个应用层服务的具体实现作过多介绍,但有几个方面我还是打算再进一步讨论一下。

构造器注入(Constructor Injection)

应用服务的实现,使用了构造器注入以获得所需对象的实例。例如CustomerManagementService类的构造函数接收两个参数:ICustomerRepository的实例,以及ICountryRepository的实例。当分布式服务组件使用IoCFactory.Instance.CurrentContainer.Resolve方法来获得ICustomerManagementService的具体实现时,IoC容器会根据配置信息来自动解析ICustomerRepository和ICountryRepository的依赖,从而在创建ICustomerManagementService对象的时候,将解析出来的repository实体传给CustomerManagementService的构造函数。我们可以从Infrastructure.CrossCutting.IoC.csproj项目的IoCUnityContainer类的ConfigureRootContainer中找到这种依赖关系的设置代码。有关NLayerApp中IoC容器的实现请参考《Microsoft NLayerApp案例理论与实践 – 基础结构层(Cross-Cutting部分)》。

//Register Repositories mappings
// ...
container.RegisterType<ICustomerRepository, CustomerRepository>(new TransientLifetimeManager());
container.RegisterType<ICountryRepository, CountryRepository>(new TransientLifetimeManager());

//Register application services mappings
// ...
container.RegisterType<ICustomerManagementService, CustomerManagementService>(new TransientLifetimeManager());

回过来再看CustomerManagementService类,它的构造函数需要ICustomerRepository和ICountryRepository两个参数,这是因为CustomerManagementService类本身在实现上需要用到这些仓储对象。事实上,ICustomerManagementService接口的实现并不规定实现类必须接收这两个参数。例如,假设我们因为测试的需要,设计了一个MockCustomerManagementService,它也实现了ICustomerManagementService接口,但由于是做测试,我们在这个Mock类中使用Dictionary、List等数据结构来模拟repository的功能,于是在MockCustomerManagementService中,也就无需ICustomerRepository和ICountryRepository的实例了。比如我们的MockCustomerManagementService可以实现如下:

public class MockCustomerManagementService 
    : ICustomerManagementService
{
    private readonly List<Customer> customerRepository =
        new List<Customer> customerRepository;
    
    public MockCustomerManagementService() { }
    
    public void AddCustomer(Customer customer)
    {
        if (!customerRepository.Contains(customer))
            customerRepository.Add(customer);
    }
    // other method implementations...
}

然后,在IoCUnityContainer中,将注册ICustomerManagementService的代码改为如下即可:

container.RegisterType<ICustomerManagementService, MockCustomerManagementService>(new TransientLifetimeManager());

数据传输对象(DTO)

在NLayerApp中,使用领域实体(Domain Entities)作为数据传输对象(DTO),同时也实现了一些用于特定用途的DTO,比如DistributedServices.MainModule.csproj项目里的PagedCriteria。在应用服务上将领域实体作为数据传输对象来处理,也就决定了在其更高层:分布式服务中,也必须使用领域实体作为DTO。原因很简单:分布式服务并没有将DTO转换为领域实体的职责,这是应用层的任务。另一方面,原本WCF会在客户端产生Contracts的代理类型的时候,会屏蔽掉领域实体作为DTO所带来的弊端,但貌似NLayerApp的客户端程序是直接引用的领域实体来进行数据交换的,从DDD的角度讲,这种设计是有问题的。当然也应该具体情况具体分析。NLayerApp中,大多数View Model都能够与领域实体的结构相对应,并且直接将领域实体用作DTO在一定程度上降低了开发复杂度,提高了生产率。NLayerApp在其官方的资料中也提到过这个问题:

The latter case is when we use DTOs (Data Transfer Objects) for remote communications between Tiers, where the domain model’s internal entities would not flow to the presentation layer or any other point beyond the internal layers of the Service. DTO objects would be those provided to the presentation layer in a remote location.

If the implementation of the entities is strongly linked to a specific technology, it is contrary to the DDD Architecture recommendations because we are contaminating the entire architecture with a specific technology. However, we have the option of sending domain entities that are POCO (Plain Old CLR Objects), that is, serialized classes that are 100% custom code and do not depend on any data access technology. In this case, the approach can be good and very productive, because we could have tools that generate code for these entity classes for is.

Thus, this approach (Serialization of Domain entities themselves) has the disadvantage of leaving the service consumer directly linked to the domain entities, which could have a different life cycle than the presentation layer data model and even different changing rates. Therefore, this approach is suitable only when we maintain direct control over the whole application (including the client that consumes the web-service), like a typical N-Tier application. On the other hand, when implementing SOA services for unknown consumers it is usually a better option to use DTOs, as explained below.

NLayerApp使用的是“序列化的领域实体”(Serialized Domain Entities)这种方式。现在我们来了解一下几个有关DTO的设计要点。

  1. DTO的设计需要面向客户端(包括客户端应用程序、与外部系统集成的Web Services等),客户端的View Model需要什么样的数据,就设计什么样的DTO。应用层负责收发DTO数据,并根据DTO数据访问领域模型中的实体,根据实体组装DTO。ORM解决的是Domain Model与关系型数据库之间的阻抗失衡,而DTO解决的是View Model与Domain Model之间的阻抗失衡
  2. DTO应该是POCO,它不能依赖于任何技术框架
  3. 对于中小型系统,可以考虑使用类似NLayerApp的Serialized Domain Entities方式,这可以提高开发效率;但如果是大型系统,还是建议使用DTO,有朋友会觉得每次根据View Model去设计DTO很耗时,但我觉得如果应用程序规模较大的时候,还是做足功夫比较好,磨刀不误砍柴工,这样在今后做系统集成的时候也会方便一些。可以考虑使用DSL与自动化代码生成技术来解决DTO的设计问题
  4. WCF产生的代理类Data Contracts就是一种DTO,如果专用微软的技术,那么也就与上述第二点不矛盾,Serialized Domain Entities可以以Data Contracts的形式出现在客户端程序中,一定程度上屏蔽了直接将Serialized Domain Entities用作DTO的负面影响

应用层服务对任务的协调职能

很多朋友无法理解应用层存在的意义,总觉得按照传统的三层架构就是数据访问层(DAL)、业务逻辑层(BLL)和表现层(Presentation)。NLayerApp的系统架构为我们展现了应用层的任务协调职能及其存在的必要性。例如BankingManagementService的PerformTransfer方法中,包含了位于基础结构层的分布式事务处理和位于Domain Model层的repository与UoW的操作。而整个PerformTransfer方法则将这些操作整合起来,以完成一个特定的应用任务:完成转账的功能。通常情况下,应用层的代码中会包含对其下各层组件的访问,因此,DDD的分层并不是严格型的(上层仅能依赖于其直接下层)。当然,如果你的应用程序并不存在需要多层协调才能完成特定任务的情况的话,应用层也可以省略。

OK,今天就先讨论到这里,下一讲我将简要介绍一下NLayerApp中的分布式服务(Distributed Services)部分。

转载至:http://www.cnblogs.com/daxnet/archive/2011/06/24/2088815.html

发表在 C# | 留下评论

Microsoft NLayerApp案例理论与实践 – 分布式服务

Microsoft NLayerApp采用基于WCF的分布式服务组件为外界(各种类型的GUI)提供了访问接口,客户端程序只需要添加Service引用即可使用NLayerApp应用程序所提供的功能。在NLayerApp中,分布式服务部分的设计与结构还是比较简单的,主要包括DistributedServices.CoreDistributedServices.MainModule以及DistributedServices.Deployment三个项目。

DistributedServices.Core

该项目为所有位于分布式服务层的组件提供公共的类型定义与功能实现,比如在这个项目中定义了与异常处理相关的Fault Contract类型与特性(Attribute)定义。

DistributedServices.MainModule

该项目根据NLayerApp应用程序本身的需求,设计了所需的DTO、服务契约(Service Contract)、操作契约(Operation Contract),并根据模块划分,用C#的partial class特性分别实现了银行管理、客户管理和销售管理三个部分的操作。IMainModuleService接口中定义了NLayerApp的分布式服务所能提供的所有操作接口,而MainModuleService部分类则实现了该接口。根据模块划分的不同,MainModuleService类的实现部分被分配到三个不同的文件中:MainModuleService.BankingManagement.cs、MainModuleService.CustomersManagement.cs和MainModuleService.SalesManagement.cs。

打开IMainModuleService.cs文件,我们可以看到,对于所有的方法,无论是方法的参数还是返回值,都是以原始数据类型(Primitive Data Types)或者DTO的形式实现数据传输的。NLayerApp将Domain Entities同时作为DTO来处理,有关DTO以及Domain Entities as DTOs的详细内容,请参考上篇《Microsoft NLayerApp案例理论与实践 – 应用层》一文,这里就不再多说了。

在IMainModuleService接口的实现类MainModuleService类中,各方法都通过IoC容器(前面也详细讲过IoC容器,NLayerApp事实上使用的是Microsoft Unity)获得应用层组件的实例,从而执行相应的操作。通过上文我们可以得知,NLayerApp在应用层中也是使用IoC容器来获得仓储、领域服务(Domain Service)的具体实现的,由此可见,NLayerApp在层与层之间就是使用的IoC容器实现分层解耦。以下是MainModuleService类中GetBankAccounts方法的实现代码,从中我们可以了解到分布式服务中IoC容器的使用方式。

public List<BankAccount> GetBankAccounts(BankAccountInformation bankAccountInformation)
{

    //Resolve root dependency and perform operation
    IBankingManagementService bankingManagement = IoCFactory
        .Instance
        .CurrentContainer
        .Resolve<IBankingManagementService>();

    List<BankAccount> bankAccounts = null;

    //perform work!
    bankAccounts = bankingManagement.FindBankAccounts (
        bankAccountInformation.BankAccountNumber, 
        bankAccountInformation.CustomerName);

    return bankAccounts;

}

DistributedServices.Deployment

该项目其实就是一个WCF Web Application,它是分布式服务的宿主项目,它可以以Web应用程序的方式部署到ASP.NET Web Server(比如IIS)上。该项目下的MainModule.svc文件定义了所使用的WCF Service(也就是DistributedServices.MainModule项目中的MainModuleService类);而web.config文件则对如下信息进行了配置:

  • 用于Entity Framework的连接字符串
  • 所使用的IoC容器的名称
  • 用于诊断和跟踪程序的配置信息
  • Web Application的配置信息
  • WCF Service的配置信息

在部署NLayerApp的时候,需要将DistributedService.Deployment项目部署到ASP.NET Web Server(比如IIS)上,并启动Web服务器,之后,客户端程序即可通过WCF的客户端配置以及代理类来访问NLayerApp的应用程序了。

分布式服务程序的调试

我们可以用soapUI工具来进行分布式服务的调试。soapUI是一款先进的开源的针对Web Service的调试与测试工具,你可以点击此处查看该工具的官网首页,并从中获得下载链接。现在,让我们开始使用soapUI来进行分布式服务程序的调试(由于本人的系统是英文版,为了避免翻译的不准确性以致误导读者,因此请读者朋友们自行参照自己的中文版系统进行演练)。

  • 成功编译NLayerApp
  • 在DistributedServices.Deployment项目下,找到MainModule.svc文件,右键单击并选择View in Browser,这将启动ASP.NET Development Server,并在IE浏览器中展示如下页面:
    image

  • 启动soapUI,在Navigator Panel中,右键单击Projects节点,选择New soapUI Project,此时弹出New soapUI Project对话框,在Initial WSDL/WADL文本框中输入http://localhost:88/MainModule.svc?wsdl,此时Project Name文本框会自动以“MainModule”填充,暂时别管其它的选项,直接单击OK按钮
    image

  • 在Navigator Panel中展开MainModule节点,我们可以看到,它包含两个Endpoint:WS2007ForIntranetClients和BasicBindingForSliverlightClients,这与DistributedServices.Deployment项目的web.config中的配置是相符的
    image

  • 展开BasicBindingForSliverlightClients节点,我们可以看到由IMainModuleService接口所发布的所有方法,展开GetCustomerByCode操作,并双击Request 1,在打开的Request 1对话框中,左边部分列出了调用该操作的SOAP Envelope
    image

  • 在<mic:customerCode>节点上输入A0001,我们获得如下Request XML:
    <soapenv:Envelope xmlns:soapenv=http://schemas.xmlsoap.org/soap/envelope/ 
    xmlns:mic="Microsoft.Samples.NLayerApp.DistributedServices.MainModuleService"> <soapenv:Header/> <soapenv:Body> <mic:GetCustomerByCode> <!--Optional:--> <mic:customerCode>A0001</mic:customerCode> </mic:GetCustomerByCode> </soapenv:Body> </soapenv:Envelope>

  • 单击Request 1对话框左上角的绿色箭头,将直接调用GetCustomerByCode方法,并获得返回结果
    image

  • 要调试分布式服务,首先设置好断点,然后在Visual Studio中选择Debug –> Attach to Process菜单,在弹出的Attach to Process对话框中,选择ASP.NET Development Server – Port 88,然后单击Attach按钮,这将使Visual Studio进入调试模式
    image

  • 再次单击Request 1对话框中的绿色箭头以调用分布式服务,此时程序的执行将会停在断点处,供开发人员调试
    image

总结

本文主要对NLayerApp的分布式服务所涉及的各个项目做了简单介绍,同时还给出了一个实践案例,对分布式服务的测试与调试进行了详细演示。分布式服务部分是客户端程序与NLayerApp应用程序进行交互的接口部分,不包含任何业务逻辑与任务协调操作,它只是一种通讯手段的技术实现。

NLayerApp的介绍也差不多快结束了,本系列文章将不再继续对其GUI部分做详细叙述了,因为GUI部分的开发与特定技术的结合非常紧密,比如WPF、Sliverlight以及ASP.NET MVC,有关这些内容,读者可以参考相关资料一起阅读学习,本系列文章就不再继续对WPF、Sliverlight以及ASP.NET MVC这些技术本身做进一步介绍了。

转载至:http://www.cnblogs.com/daxnet/archive/2011/07/19/2110322.html

发表在 C# | 留下评论

Microsoft NLayerApp案例理论与实践 – 总结

原本不打算写这篇的,因为之前基本上每篇文章都有总结部分,因此,在系列文章结束的时候,需要总结的内容并不多;但正如刚刚所说,经过几个月的努力,本系列文章也到了该结尾的时候,于是还是做个简要的总结吧。

我们从《项目简介与环境搭建》开始,了解了NLayerApp的项目概况、先决条件、运行环境以及部署方式,并对该项目所立足的理论基础做了一个非常简单的介绍;接下来的《多层架构与应用系统设计原则》、《DDD、分布式DDD及其分层》两篇文章从理论的角度对软件架构设计原则、分层架构、领域驱动设计、面向领域的多层分布式系统等方面进行了较为详细的介绍;《基础结构层(Cross-Cutting部分)》、《基础结构层(数据访问部分)》、《领域模型层》、《应用层》以及《分布式服务》则结合之前所述的理论依据,从基础结构层、领域模型层、应用层以及分布式服务这几个主要的层次入手,针对NLayerApp进行了更为详细的剖析。或许在某些方面,文章中并没有继续深入,但我想读者朋友应该可以在学习NLayerApp案例的过程中,结合这些文章即能够较快地了解到整个项目的详细结构、各层各组件之间的关联关系以及协作方式和序列。希望这个系列文章能够从软件架构设计与领域驱动的角度,给软件从业人员带来必要的帮助。

在接下来的这段时间里,我打算以企业应用架构理论与.NET实践相结合的方式,继续研究、学习并探讨与软件架构相关的话题,大致应该会包括以下几个部分(但不一定会以如下的顺序进行探讨与介绍):

  • 新版的面向DDD的开发框架Apworks及其应用(应该能够给出一个全新的基于CQRS架构的案例程序)
  • 企业级应用框架架构设计实践:首先对通用的基于.NET的框架设计要点进行介绍,并给出几个常用的设计模式、架构模式以及惯用法的.NET实现;然后,以Apworks框架为例,详细讲解Apworks框架各个组件部分的设计思路
  • 实战Microsoft Biztalk Server:打算介绍一些Biztalk Server在实际项目中的应用经验,比如:Biztalk Server是如何应用在大型企业级应用架构中的;网上也有不少有关Biztalk的介绍文章,所以我还在犹豫是否需要从最基础的内容开始,结合简单的演练案例进行介绍(读者朋友们可以提提建议)
  • Domain Specific Language(DSL)与Microsoft Visual Studio Visualization & Modeling SDK的应用

差不多就这些内容,读者朋友如果有任何建议,可以直接留言讨论。再次感谢大家对NLayerApp系列文章的关注。

转载至:http://www.cnblogs.com/daxnet/archive/2011/07/19/2110627.html

发表在 C# | 留下评论

介绍一款好用的基于.NET的配置文件设计工具

在进行框架开发的过程中,我们往往需要对配置文件的结构进行设计,以便产生一套完整的配置方案,供开发人员在使用框架时能对框架进行配置。对于某些大型的框架,其配置节点的结构可能相当复杂,比如某个配置节点(Configuration Element)可以有属性,还可以在其下挂载多个其它的配置节点或者多个配置节点集合(Configuration Element Collection)。如果使用手动编写代码的方式来维护与配置相关的代码,势必会出现大量的重复劳动,比如,需要给每个配置属性添加System.Configuration.ConfigurationPropertyAttribute特性,需要为各个配置节点集合采用相同的代码编写模式(例如重写其中的CreateElement等方法)。这样做不仅耗时而且容易出错。更进一步,Visual Studio支持智能感知技术,如果我们在配置文件编辑器上设置了所要用到的配置信息XSD Schema文件,我们就可以利用智能感知方便快速地编写配置文件。然而,如果我们的配置节点采用手工代码维护,那么在编写完代码之后,还需要另外编写一套XSD Schema文件,以使得开发人员在使用我们的框架时,能够享受到智能感知带来的便捷,这样做不仅工作量大,而且一旦配置信息结构变得复杂,我们就很难确保代码与XSD Schema之间的对应关系。

今天我向大家介绍一款个人觉得比较不错的基于.NET的配置文件设计工具:Configuration Section Designer。它是一款使用Microsoft Visualization & Modeling SDK开发的面向配置文件设计领域的领域特定语言(DSL,有关DSL的知识,我会在后续的博客文章中向大家介绍)。如果你使用的是Visual Studio 2005/2008,你可以去Configuration Section Designer的主页下载安装包。如果你使用的是Visual Studio 2010,那么你还可以使用VS2010的Extension Manager来找到这个设计工具。

下载与安装

可以到Configuration Section Designer的下载页面下载并安装该工具。如果你使用的是Visual Studio 2005/2008,你将得到一个EXE的安装程序;如果你使用的是Visual Studio 2010,你将得到一个VSIX的扩展包。我使用的是Visual Studio 2010,因此接下来都会以Visual Studio 2010进行介绍。

在得到VSIX扩展包后,双击直接打开运行就可以将其安装到Visual Studio 2010的开发环境中。

新建配置文件项目

首先你可以使用Visual Studio 2010随便创建一个项目(比如Class Library或者Console Application都可以),然后在这个项目上单击鼠标右键,选择Add ?> New Item菜单,这将打开Add New Item对话框,在Installed Templates ?> Visual C# Items节点下,找到ConfigurationSectionDesigner,更改名称后单击Add按钮。

image

在完成Designer Surface的创建之后,我们可以看到在项目中多了一个.csd的文件,在Toolbox中,也出现了与配置文件设计相关的工具:

image

看上去是不是有点像ADO.NET Entity Framework的设计器?不错,这就是Microsoft Visualization & Modeling SDK给我们带来的强大功能:它允许开发人员设计自己的领域特定语言(Domain Specific Language, DSL),并以VSIX等扩展包的方式集成到Visual Studio开发环境中。

功能介绍

本文不打算讲解如何使用Configuration Section Designer来设计配置文件,只对其中的一些非常不错的功能进行介绍。

自动化代码与文件的生成

对于一款DSL来说,自动化代码生成不算是什么强大的功能,但是这款工具不仅仅会产生代码,而且还会产生与之相关的XSD Schema以及配置文件样本(Sample Configuration File),能让开发者直观地看到最终效果:

image

配置文件样本测试

在设计配置文件的过程中,你可以直接双击产生的.csd.config(比如上面的ConfigurationSectionDesigner1.csd.config)文件,然后在里面进行编辑,来测试你的设计是否正确。注意这个编辑过程是自带有智能感知的:

image

Windows Forms设计器的支持

一个专业的开发框架在向用户提供配置相关的代码以及XSD Schema的同时,还应该为用户提供方便的配置文件编辑器(例如Microsoft Patterns & Practices Enterprise Library通常都会带有面向各种Application Block的配置编辑器)。试想我们将用Windows Forms及其Property Grid控件来设计一款配置文件编辑器,在将配置对象绑定到Property Grid时,Property Grid会通过反射将该对象下所有的属性都显示出来。然而对于配置编辑器而言,我们不仅需要控制配置对象中各个属性的显示方式,而且还需要对这些属性进行一些描述和归类。如果是手工维护框架中的配置代码,这个问题好解决:直接向每个属性手工添加诸如System.ComponentModel.DescriptionAttribute、System.ComponentModel.BrowsableAttribute等特性即可。但如果整个配置代码都是由某工具自动化生成的,那么你就不能直接在生成的代码上就行手工修改,而只能通过System.ComponentModel.DataAnnotations.AssociatedMetadataTypeTypeDescriptionProvider、System.ComponentModel.CustomTypeDescriptor以及System.ComponentModel.PropertyTypeDescriptor类来扩展MetadataType Description,然后使用System.ComponentModel.DataAnnotations.MetadataTypeAttribute特性以在用于描述源类型的元数据类型上进行特性设置。以下是这种实现方式的相关代码:

public class MyAssociatedMetadataTypeTypeDescriptionProvider : AssociatedMetadataTypeTypeDescriptionProvider
{

    public MyAssociatedMetadataTypeTypeDescriptionProvider(Type type)
        : base(type) { }

    public MyAssociatedMetadataTypeTypeDescriptionProvider(Type type, Type associatedMetadataType)
        : base(type, associatedMetadataType) { }

    private ICustomTypeDescriptor Descriptor { get; set; }

    public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
    {
        if (null == this.Descriptor)
            this.Descriptor = new MyCustomTypeDescriptor(base.GetTypeDescriptor(objectType, instance));
        return this.Descriptor;
    }
}

public class MyCustomTypeDescriptor : CustomTypeDescriptor
{

    public MyCustomTypeDescriptor(ICustomTypeDescriptor wrappedTypeDescriptor)
    {
        this.WrappedTypeDescriptor = wrappedTypeDescriptor;
    }

    private ICustomTypeDescriptor WrappedTypeDescriptor { get; set; }

    public override AttributeCollection GetAttributes()
    {
        return this.WrappedTypeDescriptor.GetAttributes();
    }

    public override PropertyDescriptorCollection GetProperties()
    {
        PropertyDescriptorCollection properties = this.WrappedTypeDescriptor.GetProperties();

        List<PropertyDescriptor> list = new List<PropertyDescriptor>();
        foreach (PropertyDescriptor descriptor in properties)
            list.Add(new MyPropertyDescriptor(descriptor));

        return new PropertyDescriptorCollection(list.ToArray(), true);
    }

    public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        return this.GetProperties();
    }
}

public class MyPropertyDescriptor : PropertyDescriptor
{

    public MyPropertyDescriptor(PropertyDescriptor wrappedPropertyDescriptor)
        : base(wrappedPropertyDescriptor)
    {
        this.WrappedPropertyDescriptor = wrappedPropertyDescriptor;
    }

    private PropertyDescriptor WrappedPropertyDescriptor { get; set; }

    public override void AddValueChanged(object component, EventHandler handler)
    {
        this.WrappedPropertyDescriptor.AddValueChanged(component, handler);
    }

    public override bool CanResetValue(object component)
    {
        return this.WrappedPropertyDescriptor.CanResetValue(component);
    }

    public override Type ComponentType
    {
        get
        {
            return this.WrappedPropertyDescriptor.ComponentType;
        }
    }

    public override bool IsReadOnly
    {
        get
        {
            return this.WrappedPropertyDescriptor.IsReadOnly;
        }
    }

    public override object GetValue(object component)
    {
        return this.WrappedPropertyDescriptor.GetValue(component);
    }

    public override Type PropertyType
    {
        get
        {
            return this.WrappedPropertyDescriptor.PropertyType;
        }
    }

    public override void RemoveValueChanged(object component, EventHandler handler)
    {
        this.WrappedPropertyDescriptor.RemoveValueChanged(component, handler);
    }

    public override void ResetValue(object component)
    {
        this.WrappedPropertyDescriptor.ResetValue(component);
    }

    public override void SetValue(object component, object value)
    {
        List<Attribute> attributes = new List<Attribute>();
        this.FillAttributes(attributes);

        foreach (Attribute attribute in attributes)
        {
            ValidationAttribute validationAttribute = attribute as ValidationAttribute;
            if (null == validationAttribute)
                continue;

            if (!validationAttribute.IsValid(value))
                throw new ValidationException(validationAttribute.ErrorMessage, validationAttribute, component);
        }

        this.WrappedPropertyDescriptor.SetValue(component, value);
    }

    public override bool ShouldSerializeValue(object component)
    {
        return this.WrappedPropertyDescriptor.ShouldSerializeValue(component);
    }

    public override bool SupportsChangeEvents
    {
        get
        {
            return this.WrappedPropertyDescriptor.SupportsChangeEvents;
        }
    }
}


// 以下是使用方式:
[MetadataType(typeof(ApplicationElementMetadata))]
public partial class ApplicationElement
{
    static ApplicationElement()
    {
        TypeDescriptor.AddProvider(
            new MyAssociatedMetadataTypeTypeDescriptionProvider(
                typeof(ApplicationElement)), typeof(ApplicationElement));
    }

}

public class ApplicationElementMetadata
{
    [Description("Indicates the provider of the Application.")]
    public string Provider { get; set; }
}

 

然而对于Configuration Section Designer而言,这种繁杂的实现方式已经不复存在。它本身就支持Component Model相关特性的设置,然后会在产生的代码中添加相应的特性描述,大大减轻了开发者的负担。

image

 

产生的代码如下:

image

 

Configuration Section Designer应该还有很多不错的功能,时间关系我也没有进行深入研究,有兴趣的朋友不妨下载一个Configuration Section Designer体验一下。

转载至:http://www.cnblogs.com/daxnet/archive/2011/09/16/2178377.html

发表在 C# | 留下评论

在Visual Studio 2010中创建多项目(解决方案)模板【一】

当我们使用Visual Studio来新建某个项目(Project)时,通常都会使用File –> New –> Project菜单来打开New Project(新建项目)对话框,里面列出了各种项目类型以供我们选择。大部分读者朋友都应该知道,这个对话框其实是列出了所有已经安装的项目模板,不仅如此,Visual Studio还允许用户通过File –> Export Template菜单将现有的项目导出为项目模板。

平时我们最为常见的是使用Export Template来创建单一项目的项目模板,此时使用Export Template功能就十分有效。当然,社区里也有一些工具(比如微软官方的Export Template Wizard工具)能够帮忙创建项目模板,甚至可以直接将模板压缩包文件编译成供Visual Studio 2010使用的扩展安装程序(VSIX),这样用户就可以通过VSIX直接将项目模板安装到Visual Studio 2010中。虽然社区已经提供了各种各样的创建项目模板的方法,而且网上有关项目模板创建的文章和论坛帖子也不少,但大多数都是在讨论单一项目模板的创建。本文打算从一个案例解决方案开始,边做边讨论,看看在Visual Studio 2010中是如何创建多项目(解决方案)模板的。image

案例解决方案 – RainbowCMS

为了演示的需要,我随手新建了一个非常简单的面向DDD的案例解决方案:RainbowCMS。这个解决方案并没有包含任何界面,也没有提供Web Service的接口,而仅仅是实现了一个非常简单的管理客户信息的应用服务。这个解决方案主要包含如下四个项目:

 

  • RainbowCMS.Application:此项目包含了RainbowCMS的应用服务,以及与之相关的数据传输对象
  • RainbowCMS.Domain:此项目包含了RainbowCMS的领域模型、仓储接口以及与DDD相关的一些接口。为了演示需要,我只在RainbowCMS中提供了非常简单的“客户-地址”模型
  • RainbowCMS.Domain.Repositories:此项目实现了针对“客户”聚合的仓储,它本身应该属于领域层的一个插件,所以最后实现的时候,是需要通过IoC注射到应用层的
  • RainbowCMS.Infrastructure:此项目是整个解决方案的基础结构层,包含了一些与技术相关的基础结构,比如IoCFactory

这个解决方案所包含的项目简单地将一个面向DDD的应用程序的各个部分组织在一起(展现层除外),它仅仅用于演示Visual Studio 2010中多项目解决方案模板的创建过程,因此请读者朋友不要过分地纠结其DDD架构的实现过程。

现在,我们开始基于RainbowCMS创建多项目解决方案模板。在开始之前,请先确保你的机器装有Visual Studio 2010 SDK

多项目解决方案模板的创建

在安装了Visual Studio 2010 SDK之后,我们就可以开始创建项目模板了。在Visual Studio 2010中选择File –> New –>Project菜单,在弹出的New Project对话框的Installed Templates下,选择Visual C#/Extensibility节点,可以看到类似如下的模板选项:

image

现在,让我们开始使用C# Project Template项目来创建多项目解决方案模板。

新建模板项目

在上面的New Project对话框中,选择C# Project Template,在Name文本框中输入项目名称:CMSProjectTemplate,然后单击OK按钮。此时将创建一个名为CMSProjectTemplate的解决方案:

image

在该解决方案中真正有意义的部分是CMSProjectTemplate.vstemplate文件,CMSProjectTemplate.ico则是今后用于显示在New Project对话框中的图标;而其它的所有文件都是用来表示一个完整的C#项目的相关文件。由于现在我们是在创建多项目解决方案的模板,所以其它的这部分文件(包括AssemblyInfo.cs、Class1.cs以及ProjectTemplate.csproj)都可以删掉。于是,我们的CMSProjectTemplate解决方案就变成了如下的结构:

image

更改CMSProjectTemplate.vstemplate文件

既然我们已经在解决方案中删掉了一部分内容,那么CMSProjectTemplate.vstemplate文件也要作相应的修改。首先,找到VSTemplate节点的Type属性,将其改为ProjectGroup,这一点很重要,否则今后产生的项目将不会被添加到解决方案资源管理器中;其次,将TemplateContent下所有的内容删掉,并添加一个ProjectCollection节点。更改以后的CMSProjectTemplate.vstemplate内容如下:

<?xml version="1.0" encoding="utf-8"?>
<VSTemplate Version="3.0.0" Type="ProjectGroup" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005">
  <TemplateData>
    <Name>CMSProjectTemplate</Name>
    <Description>&lt;No description available&gt;</Description>
    <Icon>CMSProjectTemplate.ico</Icon>
    <ProjectType>CSharp</ProjectType>
    <RequiredFrameworkVersion>2.0</RequiredFrameworkVersion>
    <SortOrder>1000</SortOrder>
    <TemplateID>36e8d4bf-48bc-42c9-91ab-e45348393288</TemplateID>
    <CreateNewFolder>true</CreateNewFolder>
    <DefaultName>CMSProjectTemplate</DefaultName>
    <ProvideDefaultName>true</ProvideDefaultName>
  </TemplateData>
  <TemplateContent>
    <ProjectCollection>
    </ProjectCollection>
  </TemplateContent>
</VSTemplate>

 

导出项目模板并添加到CMSProjectTemplate解决方案

打开RainbowCMS解决方案,然后选择File –> Export Template菜单,此时将打开Export Template Wizard对话框,在对话框中选择Project Template,表示我们要将所选的项目导出为项目模板,然后在下部的下拉框中,选择需要导出为模板的项目。在这里我们选择RainbowCMS.Application,然后单击Next按钮。

image

在Select Template Options页中,设置与模板有关的参数,比如模板名称和描述等,在此我们都使用默认参数,注意将Automatically import the template into Visual Studio复选框去掉,表示不需要将产生的模板直接应用到Visual Studio中。在单击Finish后,向导会直接将导出模板所在的目录以文件管理器的方式打开。

image

将产生的RainbowCMS.Application.zip文件解压到当前文件夹备用。

回到CMSProjectTemplate项目,在项目中创建一个CMSTemplate的目录,并在该目录下创建Application目录,通过复制/粘贴的方式将RainbowCMS.Application.zip中解压出来的文件添加到CMSProjectTemplate项目的CMSTemplate/Application目录下,之后按以下步骤操作:

  • 将所有*.cs文件的编译方式改为None,否则将出现编译错误
    image
  • 将RainbowCMS.Application.csproj重命名为Application.csproj,并在MyTemplate.vstemplate中,将第一个Project节点的File属性改为Application.csproj,TargetFileName属性改为$safeprojectname$.Application.csproj。注意:这里的$safeprojectname$是项目模板的一个内置的宏,表示一个“安全”的项目名称(也就是由Visual Studio处理过的,用户在New Project对话框中输入的那个项目名称)。此外,对于单一项目模板而言,直接使用这个$safeprojectname$是没有问题的,但如果是多项目解决方案的模板,那么要在各个项目中使用这个宏,就需要一些额外操作,这部分内容会在后面介绍。有关项目模板所使用的宏,请参见:模板参数
  • 将CMSTemplate\Application目录下Application.csproj以及所有C#文件中的RainbowCMS改为$safeprojectname$宏
  • 修改CMSProjectTemplate.vstemplate文件,在ProjectCollection节点中添加如下代码:
    <ProjectTemplateLink ProjectName="$safeprojectname$.Application">
      CMSTemplate\Application\MyTemplate.vstemplate
    </ProjectTemplateLink>
  • 在Application.csproj文件中修改RootNamespace和AssemblyName,将其改为$safeprojectname$.Application
  • 经过上面的修改,CMSProjectTemplate的解决方案内容如下:
    image

用以上相同的步骤,将RainbowCMS.Domain、RainbowCMS.Domain.Repositories以及RainbowCMS.Infrastructure项目导出成模板并添加到CMSProjectTemplate解决方案。在完成了这一系列操作之后,CMSProjectTemplate解决方案如下所示:

image

至此,我们已经将所需要的项目模板加入到了CMSProjectTemplate中,直接编译整个解决方案,就会在输出目录中出现一个ZIP文件,它就是Visual Studio的项目模板文件。将这个ZIP文件复制到<User_Documents>\Visual Studio 2010\Templates\ProjectTemplates\Visual C#目录下,然后在Visual Studio中使用File –> New –> Project菜单打开New Project对话框,我们就可以在Visual C#的类别下找到CMSProjectTemplate的项目模板:

image

选中这个模板,然后为我们新建的项目起个名字,比如CMSTest1,然后单击OK按钮,我们可以看到,一个新的解决方案被创建了,它有着下面的结构:

image

与我们本文开始的RainbowCMS解决方案相比,除了项目名字不同(此时是CMSTest1)以外,其它的结构完全相同。双击IoCFactory.cs文件将其打开,我们发现,IoCFactory类所在的命名空间有误,我们希望的是CMSTest1.Infrastructure,而产生的代码里却是CMSTest1.Infrastructure.Infrastructure:

namespace CMSTest1.Infrastructure.Infrastructure
{
    public static class IoCFactory
    {
        public static T GetObject<T>()
        {
            // TODO: Implement the IoC/DI logic here.
            return default(T);
        }
    }
}

 

打开CMSProjectTemplate项目下的CMSTemplate\Infrastructure.IoCFactory.cs文件,内容如下:

namespace $safeprojectname$.Infrastructure
{
    public static class IoCFactory
    {
        public static T GetObject<T>()
        {
            // TODO: Implement the IoC/DI logic here.
            return default(T);
        }
    }
}

 

看来在这里$safeprojectname$指代的是CMSTest1.Infrastructure,而不是CMSTest1。通常,我们希望$safeprojectname$在解决方案的各个项目中,都是指代用户输入的项目名称(即CMSTest1),而不是每个项目各取不同的值。这样做其实很重要:比如在指定一个项目对另一个项目的引用时,例如:CMSTest1.Domain如果需要引用CMSTest1.Infrastructure,那么就需要在Domain.csproj中的项目引用部分加入$safeprojectname$.Infrastructure,然而如果$safeprojectname$指代的是CMSTest1.Domain的话,你就无法在Domain.csproj中加入这一引用,因为它会被替换成CMSTest1.Domain.Infrastructure。要解决这个问题,我们需要使用Template Wizard。我将在下一篇文章中详细介绍Template Wizard的使用。

本文案例下载

转载至:http://www.cnblogs.com/daxnet/archive/2012/01/17/2324969.html

发表在 C# | 留下评论

在Visual Studio 2010中创建多项目(解决方案)模板【二】

上文中我给大家介绍了多项目解决方案模板的创建,在文章的最后我们遇到了一个问题,就是$safeprojectname$这个模板参数(宏)所指代的意义在各个项目中都不一样,而我们却希望它能够简单地指代用户所输入的项目名称。本文将从这个问题出发,讨论在Visual Studio 2010中是如何使用Template Wizard来设计复杂的多项目解决方案的。

Template Wizard的基本应用

创建Template Wizard项目

在CMSProjectTemplate解决方案下,新建一个C# Class Library,取名为CMSProjectTemplateWizard,在该项目上添加Microsoft.VisualStudio.TemplateWizardInterface以及EnvDTE的引用(注意:此时需要将EnvDTE的Embed Interop Types设置为False),然后新建一个名为RootWizardImpl的类,使其继承于Microsoft.VisualStudio.TemplateWizard.IWizard接口,然后实现该接口中的方法。RootWizardImpl类的代码如下:

public class RootWizardImpl : IWizard
{
    private string safeprojectname;
    private static Dictionary<string, string> globalParameters = new Dictionary<string, string>();

    public static IEnumerable<KeyValuePair<string, string>> GlobalParameters
    {
        get { return globalParameters; }
    }

    #region IWizard Members

    public void BeforeOpeningFile(EnvDTE.ProjectItem projectItem) { }

    public void ProjectFinishedGenerating(EnvDTE.Project project) { }

    public void ProjectItemFinishedGenerating(EnvDTE.ProjectItem projectItem) { }

    public void RunFinished() { }

    public void RunStarted(object automationObject, 
        Dictionary<string, string> replacementsDictionary, 
        WizardRunKind runKind, object[] customParams)
    {
        safeprojectname = replacementsDictionary["$safeprojectname$"];
        globalParameters["$safeprojectname$"] = safeprojectname;
    }

    public bool ShouldAddProjectItem(string filePath) { return true; }

    #endregion
}

在上面的代码中,我们仅实现了RunStarted方法,在这个方法中,我们首先通过replacementsDictionary将“根项目”(也就是对Visual Studio而言的那个单一项目)的$safeprojectname$的值取出,然后将其放到一个静态字典集合globalParameters中,这个globalParameters会在后面子项目的TemplateWizard中使用,以替代子项目中$safeprojectname$的值。

顺便说一下RunStarted方法的几个参数:

  • automationObject:DTE的自动化对象,它可以被转换成DTE接口的实例,以便在代码中操作Visual Studio IDE
  • replacementsDictionary:包含了所有内嵌的和自定义的模板参数(宏),这些参数值会在项目完成创建时,替换掉项目各个文件中所出现的与之对应的参数(宏)
  • WizardRunKind:指代Template Wizard的执行类型,比如是创建Item Template、Project Template还是Multiple-Project Template
  • customParams:包含了来自vstemplate文件的自定义参数。在vstemplate文件中,可以在WizardData XML节点下设置这些自定义的值

现在,让我们继续在CMSProjectTemplateWizard项目中新建一个名为ChildWizardImpl的类,同样让其继承于Microsoft.VisualStudio.TemplateWizard.IWizard接口,具体代码如下:

public class ChildWizardImpl : IWizard
{
    #region IWizard Members

    public void BeforeOpeningFile(EnvDTE.ProjectItem projectItem) { }

    public void ProjectFinishedGenerating(EnvDTE.Project project) { }

    public void ProjectItemFinishedGenerating(EnvDTE.ProjectItem projectItem) { }

    public void RunFinished() { }

    public void RunStarted(object automationObject, 
        Dictionary<string, string> replacementsDictionary, 
        WizardRunKind runKind, object[] customParams)
    {
        string safeprojectname = RootWizardImpl.GlobalParameters.Where(p => p.Key == "$safeprojectname$").First().Value;
        replacementsDictionary["$safeprojectname$"] = safeprojectname;
    }

    public bool ShouldAddProjectItem(string filePath) { return true; }

    #endregion
}

 

接下来,我们需要对CMSProjectTemplateWizard进行数字签名,可以直接在项目上直接单击鼠标右键,选择Properties,在打开的项目属性标签页上选择Signing,并为项目制定一个强名称密钥文件:

image

重新编译CMSProjectTemplateWizard,然后打开Visual Studio 2010 Command Prompt工具,在命令提示符中使用gacutil.exe将编译出来的程序集安装到GAC中:

image

现在我们已经创建了一个Template Wizard项目,接下来,我们需要调整CMSProjectTemplate的设置,使其能够使用已创建的Template Wizard

在CMSProjectTemplate中使用Template Wizard

打开CMSProjectTemplate.vstemplate文件,在文件的底部TemplateContent节点之后加入WizardExtension节点,设置节点的内容如下:

<WizardExtension>
  <Assembly>CMSProjectTemplateWizard, Version=1.0.0.0, Culture=neutral, PublicKeyToken=52319e57efa35eb8</Assembly>
  <FullClassName>CMSProjectTemplateWizard.RootWizardImpl</FullClassName>
</WizardExtension>

 

逐一打开CMSProjectTemplate\CMSTemplate下的所有子目录,修改每个目录下的MyTemplate.vstemplate文件,在文件的底部TemplateContent节点之后加入WizardExtension节点,设置节点的内容如下:

<WizardExtension>
  <Assembly>CMSProjectTemplateWizard, Version=1.0.0.0, Culture=neutral, PublicKeyToken=52319e57efa35eb8</Assembly>
  <FullClassName>CMSProjectTemplateWizard.ChildWizardImpl</FullClassName>
</WizardExtension>

 

重新编译CMSProjectTemplate项目,并将编译输出的ZIP文件复制到<User_Documents>\Visual Studio 2010\Templates\ProjectTemplates\Visual C#目录下。

重新测试CMSProjectTemplate

现在让我们重新新建一个CMSProjectTemplate的项目,在Visual Studio 2010中单击File –> New –> Project菜单,在弹出的对话框中选择CMSProjectTemplate,并输入项目名称然后单击OK按钮:

image

在Visual Studio 2010完成了项目的创建后,我们得到如下的解决方案:

image

编译CMSTest1解决方案,我们发现,我们的CMSTest1解决方案已经被成功编译:

image

双击打开IoCFactory.cs文件,我们发现,代码中已经使用了正确的命名空间,整个解决方案的$safeprojectname$已经保持一致:

namespace CMSTest1.Infrastructure
{
    public static class IoCFactory
    {
        public static T GetObject<T>()
        {
            // TODO: Implement the IoC/DI logic here.
            return default(T);
        }
    }
}

至此,我们事实上已经成功地创建了一个多项目解决方案的模板,用户已经可以开始使用这个模板来新建一个类似RainbowCMS的解决方案了。

Template Wizard的高级应用

现在,让我们看看Template Wizard的几个高级应用的例子以及使用中需要注意的问题。

场景一:通过Template Wizard向CMSProjectTemplate传递自定义参数

这个应用场景比较简单,假设我们需要通过Template Wizard向CMSProjectTemplate传递一个名为$nowyear$的参数,表示当前日期的年份,基本步骤如下:

  • 在RootWizardImpl的RunStarted方法中,向replacementsDictionary中添加一个$nowyear$的项,值为DateTime.Now.Year.ToString()
  • 在RootWizardImpl的RunStarted方法中,同样向globalParameters中添加一个$nowyear$的项,值为DateTime.Now.Year.ToString()
  • 在ChildWizardImpl的RunStarted方法中,通过RootWizardImpl从GlobalParameters中取得$nowyear$的值,并将其赋给replacementsDictionary

现在就可以在CMSProjectTemplate的任意地方使用$nowyear$参数,当项目被创建时,该参数会被当前日期的年份替换。

场景二:为用户提供“创建解决方案后编译”的选项

在CMSProjectTemplateWizard中,新建一个Windows Form,然后在这个Form上添加一个复选框,设置其文本为“Build the solution after it is created.”,表示当用户选中这个复选框时,在完成解决方案创建之后,需要Visual Studio 2010立即对该解决方案进行编译。这个Form的布局大致如下:

image

修改窗体的后台代码,添加一个BuildSolutionRequired属性,代码如下:

public bool BuildSolutionRequired
{
    get { return this.chkBuild.Checked; }
}

 

向CMSProjectTemplateWizard项目添加EnvDTE80的引用,修改RootWizardImpl类,将其改为:

public class RootWizardImpl : IWizard
{
    private bool buildSolutionRequired;
    private string safeprojectname;
    private EnvDTE80.DTE2 dteObject;

    private static Dictionary<string, string> globalParameters = new Dictionary<string, string>();

    public static IEnumerable<KeyValuePair<string, string>> GlobalParameters
    {
        get { return globalParameters; }
    }

    #region IWizard Members

    public void BeforeOpeningFile(EnvDTE.ProjectItem projectItem) { }

    public void ProjectFinishedGenerating(EnvDTE.Project project) { }

    public void ProjectItemFinishedGenerating(EnvDTE.ProjectItem projectItem) { }

    public void RunFinished()
    {
        EnvDTE80.Solution2 solution = (EnvDTE80.Solution2)dteObject.Solution;
        if (buildSolutionRequired)
            solution.SolutionBuild.Build();
    }

    public void RunStarted(object automationObject, 
        Dictionary<string, string> replacementsDictionary, 
        WizardRunKind runKind, object[] customParams)
    {
        try
        {
            dteObject = (automationObject as EnvDTE80.DTE2);
            safeprojectname = replacementsDictionary["$safeprojectname$"];
            globalParameters["$safeprojectname$"] = safeprojectname;
            frmOptions options = new frmOptions();
            if (options.ShowDialog() == DialogResult.OK)
            {
                buildSolutionRequired = options.BuildSolutionRequired;
            }
        }
        catch (Exception ex) { MessageBox.Show(ex.ToString()); }
    }

    public bool ShouldAddProjectItem(string filePath) { return true; }

    #endregion
}

 

重新编译CMSProjectTemplateWizard,并将其重装到GAC,然后尝试新建一个CMSProjectTemplate的项目,Visual Studio在创建项目之前会给出一个对话框,提示用户是否需要立即编译:

image

细心的朋友会发现,结合场景一和场景二的应用,我们就可以为用户提供一个动态参数输入的界面,而在项目模板中使用这个参数。

场景三:动态创建解决方案文件夹(Solution Folder)

通常,我们都会在Template Wizard执行完成之后,动态创建解决方案文件夹(Solution Folder)。假设我们需要在解决方案中添加一个名为ReferencedProjects文件夹,我们可以在RootWizardImpl.RunFinished方法中添加如下代码:

public void RunFinished()
{
    EnvDTE80.Solution2 solution = (EnvDTE80.Solution2)dteObject.Solution;
    Project refProjectsFolderProject = solution.AddSolutionFolder("ReferencedProjects");
}

场景四:在解决方案文件夹下引用已经存在的项目文件

在场景三中,我们已经在解决方案下创建了一个ReferencedProjects文件夹,现在更进一步,将一个已存在于C:\Test目录下的C#项目文件Test.csproj添加到这个文件夹下。基于场景三中的代码,我们修改RunFinished方法如下:

public void RunFinished()
{
    EnvDTE80.Solution2 solution = (EnvDTE80.Solution2)dteObject.Solution;
    Project refProjectsFolderProject = solution.AddSolutionFolder("ReferencedProjects");
    EnvDTE80.SolutionFolder refProjectsSolutionFolder = 
    	(EnvDTE80.SolutionFolder)refProjectsFolderProject.Object;
    string csprojFileName = @"C:\Test\Test.csproj";
    refProjectsSolutionFolder.AddFromFile(csprojFileName);
}

场景五:Project GUID问题的解决

这个问题描述起来有点点复杂,总的来说,虽然我们可以在CMSProjectTemplate项目中,在所包含的csproj文件中将ProjectGuid节点的值设置为$guid1$等,但在最终产生的项目文件上,我们发现,Visual Studio 2010会自动重新生成一个GUID来覆盖我们所指定的这个。换句话说,即使是在RootWizardImpl.RunFinished方法中,也得不到这个最终的Project GUID。通常情况下,这不是什么大问题,因为一般我们也不太关心这个ProjectGuid究竟用什么值,因为项目之间的引用也是通过项目名称实现的。比如在我们的CMSProjectTemplate中就不存在这样的问题。然而有些第三方的项目类型或许就会使用Project GUID来实现项目引用,比如大名鼎鼎的Windows Installer XML Toolset(WiX),它就是根据Project GUID来决定其所关联的项目的,这样就出现问题了:在WiX项目的模板中,我们可以给定其引用的项目的GUID,但在最后生成的解决方案中,被引用的这个项目的GUID发生了变化,导致WiX项目无法对所需的项目进行引用,用户需要手动地重新添加项目引用,这样做就达不到自动化项目创建的目的。

这个问题我上网研究了很长时间,网上也没有找到合适的办法,很多国外技术社区的朋友也在一直抱怨为什么Visual Studio 2010在创建解决方案的时候需要重新产生Project GUID。最后经过我的反复试验,我找到了解决这个问题的办法。既然我们无法修改被引用项目的Project GUID,那么我们就直接在WiX项目上动手,在WiX项目中将它所设置的Project GUID替换为被引用项目的最终Project GUID。如何确定这个被引用项目的最终的Project GUID呢?只需要在解决方案资源管理器中找到这个被引用的项目,然后执行Save操作,项目的Project GUID就会被确定下来,然后再使用文本读取等手段获得这个最终的Project GUID即可。详细代码如下:

using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;
using System.Xml;
using EnvDTE;
using Microsoft.VisualStudio.TemplateWizard;

public void RunFinished()
{
  // 获取Solution对象
  EnvDTE80.Solution2 solution = (EnvDTE80.Solution2)dteObject.Solution;

  Project webProject = null;
  Project wixProject = null;
  foreach (Project p in solution.Projects)
  {
      if (p.Name == string.Format("{0}.Web", safeprojectname))
      {
          webProject = p;
      }
      if (p.Name == string.Format("{0}.Wix", safeprojectname))
      {
          wixProject = p;
      }
  }

  // 保存web项目,使得其Project GUID能够被最终确定下来.
  webProject.Save();
  // 保存需要修改的WiX项目,以确保“保存项目”对话框不会弹出.
  wixProject.Save();

  // 在解决方案资源管理器中定位WiX项目
  Window solutionExplorerWindow = dteObject.ToolWindows.SolutionExplorer.Parent as Window;
  solutionExplorerWindow.Activate();
  UIHierarchyItem solutionHier = dteObject.ToolWindows.SolutionExplorer.UIHierarchyItems.Item(1);
  UIHierarchyItem wixProjectHier = null;
  foreach (UIHierarchyItem item in solutionHier.UIHierarchyItems)
  {
      if (item.Name == string.Format("{0}.Wix", safeprojectname))
      {
          wixProjectHier = item;
          break;
      }
  }

  if (wixProjectHier != null)
  {
      // 在解决方案资源管理器中将WiX项目选中
      wixProjectHier.Select(vsUISelectionType.vsUISelectionTypeSelect);
      // 将WiX项目从解决方案中卸载(Unload)
      dteObject.ExecuteCommand("Project.UnloadProject");
      // 调用ReplaceProjectGuid方法,修改WiX项目中对web项目
      // 的引用Guid
      ReplaceProjectGuid(webProject, wixProject);
      // 稍等片刻...
      System.Threading.Thread.Sleep(500);
      // 重新加载WiX项目
      dteObject.ExecuteCommand("Project.ReloadProject");
  }
}

private void ReplaceProjectGuid(Project webProject, Project wixProject)
{
    var webProjectFullName = webProject.FullName;
    var webProjectText = File.ReadAllText(webProjectFullName);

    int pos = webProjectText.IndexOf("<ProjectGuid>", StringComparison.InvariantCultureIgnoreCase);
    var guid = webProjectText.Substring(pos + "<ProjectGuid>".Length, 38);

    var wixProjectFullName = wixProject.FullName;
    XmlDocument xmlDoc = new XmlDocument();
    XmlNamespaceManager namespaceMgr = new XmlNamespaceManager(xmlDoc.NameTable);
    namespaceMgr.AddNamespace("ns", "http://schemas.microsoft.com/developer/msbuild/2003");
    xmlDoc.Load(wixProjectFullName);

    XmlNode node = xmlDoc.SelectSingleNode("//ns:Project//ns:ItemGroup[3]//ns:ProjectReference[2]//ns:Project", namespaceMgr);
    node.InnerText = guid;
    
    xmlDoc.Save(wixProjectFullName);
}

 

总结

至此,我们已经成功地借助Template Wizard创建了一个多项目解决方案的模板,我们还学习了Template Wizard的一些高级应用。但我们的CMSProjectTemplate还没有全部完成,我们还需要为其提供一个更好听的名字、更好看的图标,而且我们还希望能够通过Visual Studio 2010 Extension来实现一个安装包,以便用户能够直接安装并使用我们的模板。这部分内容我会在下一篇文章中重点介绍。

本文案例下载

 

转载至:http://www.cnblogs.com/daxnet/archive/2012/01/18/2325928.html

发表在 C# | 留下评论

在Visual Studio 2010中创建多项目(解决方案)模板【三】

前文回顾:

在Visual Studio 2010中创建多项目(解决方案)模板【一】:多项目解决方案模板的创建

在Visual Studio 2010中创建多项目(解决方案)模板【二】:Template Wizard的使用

本文主要讨论多项目(解决方案)模板的部署相关问题,包括:

  • 为多项目解决方案模板设置模板名称
  • 修改多项目解决方案模板的图标
  • 创建Visual Studio 2010扩展的安装包VSIX文件

为多项目解决方案模板设置模板名称

模板名称的设置非常简单,,只需要修改CMSProjectTemplate.vstemplate文件中的Name XML节点的内容即可。例如,我们可以为我们的模板起名为:Customer Management System Solution:

<Name>Customer Management System Solution</Name>

 

修改多项目解决方案模板的图标

模板图标的修改也非常简单,在文件系统中找一个ICO的图标文件,将CMSProjectTemplate项目目录下的CMSProjectTemplate.ico文件替换掉即可。例如我使用下面的图标作为模板的图标:

image

现在编译CMSProjectTemplate项目,并将产生的ZIP文件拷贝到Visual C#的ProjectTemplate目录下,重新打开New Project对话框,我们可以看到下面的效果:

image

 

创建Visual Studio 2010扩展的安装包VSIX文件

现在,我们可以使用VSIX来为最终用户提供一个安装项目模板的安装包,到时候用户只需要双击这个VSIX文件即可将所需的项目模板以插件的形式安装到Visual Studio中。

首先,在CMSProjectTemplate解决方案中,新建一个VSIX Project的项目,我们取名为CMSProjectTemplateVSIX:

image

在source.extension.vsixmanifest文件的设计界面,设置如下属性:

  • Product Name:Customer Management System Project Template
  • Author:<填写你自己的姓名,或者公司名>
  • Description:<填写一些描述信息>

其它内容你可以选填,至于License Terms,你可以找一个txt或者rtf文件,用来描述许可协议。填写完后,设计界面大致如下:

image

然后,在设计界面的Content部分,单击Add Content按钮,此时将弹出Add Content对话框,在Select a content type下拉框中,选择Project Template,在Select a source选项中选择CMSProjectTemplate项目,然后单击OK按钮:

image

用相同的方法,添加Template Wizard:

image

完成这两项内容的添加以后,设计界面的Content部分大致如下:

image

OK,现在保存并编译CMSProjectTemplateVSIX项目,完成编译之后,我们在输出目录中找到了VSIX文件:

image

双击CMSProjectTemplateVSIX.vsix文件,将出现如下对话框:

image

单击Install按钮完成Visual Studio 2010扩展的安装。安装完成后,重新启动Visual Studio 2010,点击Tools –> Extension Manager菜单,我们可以在打开的Extension Manager对话框中找到刚刚安装的扩展包:

image

用户可以根据自己的需要对其进行禁用或者卸载。

总结

本系列文章从一个案例解决方案开始,逐步介绍了如何使用Visual Studio 2010 SDK来创建一个多项目的解决方案模板项目,并介绍了其中的一些高级应用。希望这样的文章能够真正地帮助到有这方面需求的读者朋友。

本文案例下载

CMSProjectTemplate(完整版)

参考文献

转载至:http://www.cnblogs.com/daxnet/archive/2012/01/19/2326758.html

发表在 C# | 留下评论

在.NET下使用Task Parallel Library提高程序性能

.NET 4.0中的Task Parallel Library(TPL)已经不是什么新鲜事了,相信很多朋友也阅读过不少有关TPL的书籍资料。而另一方面,能够将TPL合理地运用在实际项目开发过程中,以提高程序的执行效率,这种情况也并不多见。本文就以实际项目中的一个程序功能为例,简要讨论一下TPL的应用。在此我不打算对TPL的相关基础知识做过多讨论,这些内容在网上应该有不少的文章资料可供参考;同时读者朋友还可以阅读一些有关TPL的经典书籍,以便加深对TPL的理解。文章最后我会推荐几本不错的有关.NET 4.0下TPL的书籍资料。

案例:批量对象的XML序列化

在某个项目中,需要对一大批相同类型的对象进行XML序列化操作,在序列化工作完成后,程序会把序列化所得的XML字符串根据对象的ID值保存到一个字典(Dictionary)的对象中,以便后续的程序逻辑能够使用这些序列化后的XML。为了简化起见,我定义了一个Customer类来模拟这些对象的类型(实际项目中的对象类型要比这个Customer复杂一些),这个Customer类仅包含两个属性:ID和Name。下图大致描述了这个处理过程:

image

现在让我们先定义这个Customer类,以便为接下来的实验作准备。Customer类的定义如下:

public class Customer
{
    public long ID { get; set; }
    public string Name { get; set; }

    public override string ToString()
    {
        return Name;
    }
}

 

下面,我们分别使用传统的方式和基于TPL的并行处理方式来实现这个程序,然后比较一下这两种方式产生的效果差异。

传统的实现方式

传统的实现方式很简单,基本思路就是对每一个Customer对象,使用XmlSerializer对其进行序列化操作,然后把产生的XML字符串保存到字典中。代码如下:

static IEnumerable<KeyValuePair<long, string>> SerializeCustomers(Customer[] customers)
{
    var dict = new Dictionary<long, string>();
    var xmlSerializer = new XmlSerializer(typeof(Customer));
    foreach (var customer in customers)
    {
        using (var ms = new MemoryStream())
        {
            xmlSerializer.Serialize(ms, customer);
            dict.Add(customer.ID, Encoding.ASCII.GetString(ms.ToArray()));
        }
    }
    return dict;
}

 

基于TPL的并行处理方式

在采用这种方式之前,需要对我们的应用场景进行分析。今后在项目中打算使用TPL之前,都应该进行这样的分析。主要目的就是为了讨论目前我们所面对的场景,是否可以使用并行计算。目前我们的应用场景是可以采用TPL的并行处理方式的。因为首先,针对每个Customer对象的序列化操作都相对独立,没有先后顺序之分,即各操作之间是可替换的,比如计算a+b+c,可以先计算a+b(也就是(a+b)+c),也可以先计算b+c(也就是a+(b+c));其次,虽然在最后整合结果的时候需要访问跨线程的共享资源,也就是在最后整合结果的时候产生了资源的依赖关系,但对于整个计算的过程,各个任务都是可以互不干扰地执行的。在运用TPL的时候,我觉得应该尽可能地降低各个任务之间的依赖关系,因为TPL中的任务有可能会被分配到不同的线程去执行,如果任务之间有资源的相互依赖的话,线程同步将降低任务执行的效率。

以下是此案例的TPL版本:

static IEnumerable<KeyValuePair<long, string>> ParallelSerializeCustomers(Customer[] customers)
{
    var dict = new Dictionary<long, string>();
    var xmlSerializer = new XmlSerializer(typeof(Customer));
    object lockObj = new object();
    Parallel.ForEach(customers, () => new Dictionary<long, string>(),
        (customer, loopState, single) =>
            {
                using (var ms = new MemoryStream())
                {
                    xmlSerializer.Serialize(ms, customer);
                    single.Add(customer.ID, Encoding.ASCII.GetString(ms.ToArray()));
                }
                return single;
            }, 
        (single) =>
            {
                lock (lockObj)
                {
                    single.ToList().ForEach(p => dict.Add(p.Key, p.Value));
                }
            });
    return dict;
}

 

在ParallelSerializeCustomers方法中,采用了foreach循环的并行版本:Parallel.ForEach方法。这个方法与foreach类似,会逐个轮询给定的IEnumerable对象中的没一个值,不过Parallel.ForEach方法会将这个轮询的过程分配到多个Task上执行,因此对于Parallel.ForEach,执行过程的中断(break)以及异常处理都与foreach完全不同。在这个例子中,我们使用的是Parallel.ForEach方法的其中一个重载版本,在这个方法重载中,首先我们将需要轮询的IEnumerable对象(也就是这里的customers数组)传递给该方法;之后有一个Func<TLocal>的委托参数,这个委托参数的作用是为了对Task执行线程范围内的局部变量进行初始化,在这里我们直接使用Lambda表达式返回了一个新建的Dictionary<long, string>对象,表示需要对线程范围内的局部变量(其实就是第三个参数中的那个single变量)初始化成一个新的Dictionary<long, string>实例;第三个参数也是一个委托,用于对当前的枚举对象执行真正的处理逻辑,然后将处理结果返回;第四个参数则是用来整合每个任务的处理结果,以得到最终结果。不难看出,在整合最终结果的时候,多个线程需要同时访问dict变量,因此需要使用lock关键字以保证线程同步。

执行效果对比

以下是在一台具有4核CPU的计算机上,处理十万(100000)个Customer对象的执行效果,可见基于TPL的实现效率要比传统的实现方式高很多。值得一提的是,传统方式所产生的dict是有序的,而基于TPL的方式所产生的dict则是无序的,但这并不影响结果,因为程序并不会关心dict中的值是否有序。

image

 

以下是传统实现方式下,CPU的利用率。我们可以看到,基本上CPU的利用率只能达到20%-30%左右,大部分CPU资源都没有利用到:

image

 

以下是基于TPL方式下,CPU的利用率,基本上能达到85%以上(估计剩下的部分由于IO的原因,所以没有达到更高的CPU利用率):

image

 

参考书籍

案例代码下载

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

转载至:http://www.cnblogs.com/daxnet/archive/2012/02/16/2354703.html

发表在 C# | 留下评论

在Apworks框架中解除NHibernateContext与NHibernateRepository的依赖关系

在以前的Apworks框架中,Apworks的核心组件(Apworks.dll)定义了所有与仓储/仓储上下文相关的接口,而在另外的程序集中,实现了这些接口并提供了针对某个ORM框架的仓储/仓储上下文的具体实现。当然,目前我也只是开发了针对NHibernate的仓储实现,也就是那个Apworks.Repositories.NHibernate程序集。这样做的目的,就是为了使得Apworks的核心组件能够脱离具体的第三方组件而独立存在,避免由于第三方组件存在的缺陷而导致核心组件需要频繁更新。这种做法参考了Martin Fowler在其PoEAA一书中描述的Separated Interface模式。当然,本文的意图不在于讨论如何将这个模式应用到实际框架的设计和开发过程中,这个内容我会在后续的博客中详细讨论。现在我们来讨论一下这个Apworks.Repositories.NHibernate程序集的设计问题。

问题

早在Apworks 2.0发布之前,我就意识到这个问题了,之后也有网友针对这个问题发表过评论。在现有的设计中,NHibernateContext在通过GetRepository方法返回仓储实例的时候,是直接新建了一个NHibernateRepository的对象然后返回的,这对普通的仓储应用并不会带来太多的影响,比如通过仓储获取一个聚合,或者将某个聚合保存到仓储中等。然而对于那些需要扩展仓储的情形而言,这种设计就是致命的:除了修改Apworks.Repositories.NHibernate程序集的源代码以外,我想,应该没有别的办法来通过NHibernateContext以获得一个定制的仓储实例。以下的代码充分证明了这一点:

public IRepository<TAggregateRoot> GetRepository<TAggregateRoot>() 
    where TAggregateRoot : class, IAggregateRoot
{
    string key = typeof(TAggregateRoot).AssemblyQualifiedName;
    if (repositories.ContainsKey(key))
    {
        return repositories[key] as IRepository<TAggregateRoot>;
    }
    else
    {
        var repository = new NHibernateRepository<TAggregateRoot>(this);
        lock (sync)
        {
            repositories.Add(key, repository);
        }
        return repository;
    }
}

 

现在,我们需要对这部分实现进行修改,使得NHibernateRepository的实现能够像NHibernateContext那样,能够方便地在配置文件中进行配置,说得具体一些,能够通过依赖注入来消除NHibernateContext和NHibernateRepository之间的耦合。例如,在后续的应用程序开发过程中,或许我们会要对仓储进行扩展,比如加入一些分页的功能或者一些特定的查询等。在这种情况下,NHibernateContext也同样能够满足我们的需求。假设我们需要将一个FooRepository应用到应用程序中,我们或许会这样写代码:

public interface IFooRepository<T> : IRepository<T>
    where T : class, IAggregateRoot
{
    IEnumerable<T> GetWithPaging(ISpecification<T> spec, int pageNumber, int pageSize);
}

public class FooRepository<T> : NHibernateRepository<T>, IFooRepository<T>
    where T : class, IAggregateRoot
{
    public IEnumerable<T> GetWithPaging(ISpecification<T> spec, int pageNumber, int pageSize)
    {
        //...
    }
}

 

在使用FooRepository的时候,则有可能是这样写代码:

IFooRepository<Customer> repository = context.GetRepository<Customer>() as IFooRepository<Customer>;

 

最后,只需要在IoC容器(在此以Microsoft Unity为例)中注册一下这个仓储,即可实现仓储的替换:

<register type="Apworks.Repositories.IRepository`1[[MyNamespace.Domain.Customer, MyNamespace.Domain]], Apworks"
  mapTo="MyNamespace.Repositories.FooRepository`1[[MyNamespace.Domain.Customer, MyNamepsace.Domain]], MyNamespace.Repositories" />

 

事实上,我们需要解决的问题,并不是如何去调整仓储的设计,因为从接口的层面上看,这部分内容并没有什么问题。我们需要解决的问题是,如果通过IoC容器将仓储实例注入应用程序后,如何保证这些实例是在同一个Repository Context中进行工作的。这很重要,因为Repository Context担当了Unit Of Work的任务,它需要保证在其管辖的范围内,所有的仓储操作都是在同一个事务中完成的。不仅如此,它还能够允许多个仓储实例共享同一个数据库连接,减少了数据库连接次数。

上面也已经提到,以前是直接创建NHibernateRepository的实例,并通过构造函数将当前Repository Context的实例传给新创建的NHibernateRepository,这样就保证了所有通过Repository Context创建的仓储,都共享了同一个Context。但根据我们现在的设计,虽然仓储在构造函数上依赖IRepositoryContext接口,但如果通过IoC容器来解析获得仓储实例,就会使得IoC容器在解析IRepositoryContext时,会自动创建一个新的Context实例,而不会重用已有的实例。最终出现的结果就是:各个仓储都使用着自己的Context,各自为政,互不相干,更别提事务性的保证了。

解决方案

有关IoC容器

要解决Context共享的问题,还是得从IoC容器部分入手。比如,研究一下IoC容器是否能够在解析Repository时,将已有的Context实例注入到Repository中,使其共享同一个Context,而不是在每次解析Repository时都重新创建一个Context。Microsoft Unity是具有这样的功能的,它具有一种被称之为Resolver Override的功能,能够在解析某个类型的时候,用已存在的另一个类型的实例来覆盖原本应该由IoC容器解析的类型实例。从最初对Apworks的设计来看,它本身是不会依赖于任何第三方的依赖注入框架的,这点在本文开始的时候就已经说明了,因此,我们还需要考察一些常见的第三方依赖注入框架,看它们是否也像Unity那样,具有Resolver Override的功能。

至少,Castle Windsor是支持的,它可以通过向Resolve方法传入匿名类型对象来实现。于是我猜想,类似Resolver Override这样的功能,应该是大部分依赖注入框架所应该具备的功能,我也没有进一步去研究了。总之,我们需要对Apworks的ObjectContainer接口部分开始进行修改,使其也同样具有Resolver Override的功能,这样我们才能在后续解析NHibernateRepository的时候,将已有的NHibernateContext实例注射进去。

首先需要修改的是Apworks.IObjectContainer接口,向其添加两个方法,这两个方法其实是成对的,其中一个是另一个的泛型版本。这两个方法都会接受一个匿名类型的参数,以获得需要重写的实例:

T GetService<T>(object overridedArguments) where T : class;
object GetService(Type serviceType, object overridedArguments);

 

然后修改ObjectContainer抽象类和Apworks.ObjectContainers.Unity.UnityObjectContainer类的代码,使得它们能够正确地实现接口中新定义的这两个方法。ObjectContainer抽象类中的实现还是很简单直观的,就是针对这两个方法分别定义一个DoGetService的受保护(protected)方法,这么做的理由是因为ObjectContainer需要为AOP拦截提供便利;然后再在UnityObjectContainer中实现所需的受保护方法。在UnityObjectContainer中,我们重载了ObjectContainer抽象类中的DoGetService非泛型方法,并通过反射以实现Unity对Resolver Override功能的支持。代码如下:

protected override object DoGetService(Type serviceType, object overridedArguments)
{
    List<ParameterOverride> overrides = new List<ParameterOverride>();
    Type argumentsType = overridedArguments.GetType();
    argumentsType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
        .ToList()
        .ForEach(property =>
        {
            var propertyValue = property.GetValue(overridedArguments, null);
            var propertyName = property.Name;
            overrides.Add(new ParameterOverride(propertyName, propertyValue));
        });
    return container.Resolve(serviceType, overrides.ToArray());
}

 

说明一下,就基于Castle Windsor框架的ObjectContainer的实现而言,我们只需要向WindsorContainer.Resolve方法传入这个overridedArguments对象就可以了,而不需要通过反射来做这部分转换。

至此,对IoC容器的修改就完成了。接下来就是使用这个更新了的容器来实现对NHibernateContext和NHibernateRepository的解耦。

RepositoryContextManager

现在,Apworks.Repositories命名空间下有了一个新成员:RepositoryContextManager,其主要任务就是管理RepositoryContext,并向外界提供仓储的实例。从某种意义上讲,它更像是RepositoryContext的代理。以前,当我们需要获得仓储实例时,我们需要使用IRepositoryContext.GetRepository方法来获得,而现在,由于Resolver Overrides的引入,我们可以直接通过IoC容器来获得仓储实例,只是在做解析的时候,需要把已有的RepositoryContext实例注入到解析的过程中。从这个角度讲,RepositoryContextManager的功能其实也是对这一过程的封装,不仅简化了代码的编写,而且还降低了出错的风险,因为我们很容易忘记在解析仓储实例的时候,忘记把Context的实例也一并传入。

RepositoryContextManager在构造函数中,就通过IoC容器获得了Context的实例,由于它继承了DisposableObject,并实现了IUnitOfWork接口,这就使得RepositoryContextManager的使用更像原有的RepositoryContext。比如,在操作仓储时,以前我们是这样写代码的:

using (IRepositoryContext ctx = IoCFactory.GetService<IRepositoryContext>())
{
    IRepository<Customer> customerRepository = ctx.GetRepository<Customer>();
}

 

而现在我们可以直接这样写:

using (RepositoryContextManager mgr = new RepositoryContextManager())
{
  IRepository<Customer> customerRepository = mgr.GetRepository<Customer>();
}

 

代码上虽然看上去不会有太大差别,但后面的实现机制却有了显著的变换:通过使用RepositoryContextManager,我们解耦了Context和Repository(在我们的例子中,确切地说,是NHibernateContext和NHibernateRepository)。在看完RepositoryContextManager.GetRepository方法的实现代码后,我想你一定会恍然大悟的:

public IRepository<T> GetRepository<T>()
    where T : class, IAggregateRoot
{
    IRepository<T> repository = AppRuntime
        .Instance
        .CurrentApplication
        .ObjectContainer
        .GetService<IRepository<T>>(new { context = this.context });
    
    return repository;
}

 

在这个方法中,使用了我们新定义的IoC容器接口函数,在解析IRepository接口的时候,将当前Context的实例注射到解析过程中。而“context = this.context”这句话中的第一个“context”,正是Repository抽象类中构造函数的第一个参数名称。

有关AOP拦截

如果我们不使用AOP拦截,那么至此问题已经得到圆满的解决了。然而,AOP拦截对于构建一个高复用性、高延展性的企业级应用是多么的重要。Apworks是支持AOP拦截的,而且它也不会允许由于一些框架上的变动而导致AOP拦截在某些情况下不起作用。因此,框架中代码的变动,需要去迎合AOP拦截功能。

细心的读者在阅读RepositoryContextManager源代码的时候,就会注意到,如果我们启用了Apworks的AOP拦截功能,那么事实上在GetRepository方法中,通过IoC容器注入的Context实例,已经不再是我们的NHibernateContext实例了,而是由Castle Dynamic Proxy框架(Apworks使用这个框架做AOP拦截)产生的一个实现了IRepositoryContext接口、对NHibernateContext对象进行代理的代理类。结构类似如下:

image

 

于是,这个动态产生的_Castle_RepositoryContext类被不幸地注射到了NHibernateRepository的构造函数中。而在NHibernateRepository中,显然是无法通过as关键字将_Castle_RepositoryContext转换为NHibernateContext的,因为两者没有继承关系,因此,也就无法获得NHibernateContext中的这个session对象:

public NHibernateRepository(IRepositoryContext context)
            : base(context)
{
    if (context is NHibernateContext)
    {
        NHibernateContext nhContext = context as NHibernateContext;
        this.session = nhContext.Session;
    }
    else
        throw new RepositoryException(Resources.EX_INVALID_CONTEXT_TYPE);
}

 

此时context is NHibernateContext的判定就是False,直接抛出了异常。

要解决这个问题,我们需要重新设计NHibernateContext,我们需要添加一个新的接口,使得NHibernateContext实现这个新的接口,同时,我们需要将这个接口织入动态代理类中,使得这个类也同样可以通过接口获得我们需要的数据。于是,我们的设计大致如下:

image

在这种设计下,虽然context is NHibernateContext的判定还是False,但我们已经可以通过将_Castle_RepositoryContext转换为INHibernateContext,进而获得session的实例。当然,我们更希望Apworks不仅仅是针对NHibernateContext这个特例来处理这样的接口织入,而且应该能够处理更通用的场景。因此,我们可以新建一个Attribute,在使用Castle Dynamic Proxy产生代理类之前,判断被代理的类型是否有这个Attribute,并通过Attribute的值来获得需要织入的接口类型,然后将这个接口类型织入代理类即可。在Apworks中,Apworks.Interception.AdditionalInterfaceToProxyAttribute就是这样一种Attribute:

[AttributeUsage(AttributeTargets.Class, AllowMultiple=true, Inherited=false)]
public class AdditionalInterfaceToProxyAttribute : System.Attribute
{
    #region Public Properties
    /// <summary>
    /// Gets or sets the type of the interface that needs to be intercepted
    /// when the proxy object is created.
    /// </summary>
    public Type InterfaceType { get; set; }
    #endregion

    #region Ctor
    /// <summary>
    /// Initializes a new instance of <c>AdditionalInterfaceToProxyAttribute</c>.
    /// </summary>
    /// <param name="intfType">The type of the interface that needs to be intercepted
    /// when the proxy object is create.</param>
    public AdditionalInterfaceToProxyAttribute(Type intfType)
    {
        this.InterfaceType = intfType;
    }
    #endregion
}

 

然后修改ObjectContainer抽象类的GetProxyObject私有方法,使得它能够处理上面所述的这些逻辑:

private object GetProxyObject(Type targetType, object targetObject)
{
    IInterceptor[] interceptors = AppRuntime.Instance.CurrentApplication.Interceptors.ToArray();

    if (interceptors == null ||
        interceptors.Length == 0)
        return targetObject;

    if (targetType.IsInterface)
    {
        object obj = null;
        ProxyGenerationOptions proxyGenerationOptionsForInterface = new ProxyGenerationOptions();
        proxyGenerationOptionsForInterface.Selector = interceptorSelector;
        Type targetObjectType = targetObject.GetType();
        if (targetObjectType.IsDefined(typeof(BaseTypeForInterfaceProxyAttribute), false))
        {
            BaseTypeForInterfaceProxyAttribute baseTypeForIPAttribute = targetObjectType
              .GetCustomAttributes(typeof(BaseTypeForInterfaceProxyAttribute), false)[0] 
                as BaseTypeForInterfaceProxyAttribute;
            proxyGenerationOptionsForInterface.BaseTypeForInterfaceProxy = baseTypeForIPAttribute.BaseType;
        }
        if (targetObjectType.IsDefined(typeof(AdditionalInterfaceToProxyAttribute), false))
        {
            List<Type> intfTypes = targetObjectType.GetCustomAttributes(typeof(AdditionalInterfaceToProxyAttribute), false)
             .Select(p =>
             {
                 AdditionalInterfaceToProxyAttribute attrib = p as AdditionalInterfaceToProxyAttribute;
                 return attrib.InterfaceType;
             }).ToList();
            obj = proxyGenerator.CreateInterfaceProxyWithTarget(targetType, 
              intfTypes.ToArray(), 
              targetObject, 
              proxyGenerationOptionsForInterface, 
              interceptors);
        }
        else
            obj = proxyGenerator.CreateInterfaceProxyWithTarget(targetType, 
              targetObject, proxyGenerationOptionsForInterface, interceptors);
        return obj;
    }
    else
        return proxyGenerator.CreateClassProxyWithTarget(targetType, 
          targetObject, proxyGenerationOptions, interceptors);
}

 

总结

本文详细介绍了在Apworks中解耦NHibernateContext与NHibernateRepository的具体方法,并对这个过程中遇到的问题进行了分析。虽然所介绍的内容是基于Apworks这一框架的,而并不是所有的读者朋友对这个框架都比较熟悉,但本文在一定层面上提供了解决实际问题的思路,比如如何在不改变现有框架行为的情况下,使得新的功能能够被集成进来,希望这些思路能够帮助到正在这条道路上进行探索,并遇到实际困难的朋友。

转载至:http://www.cnblogs.com/daxnet/archive/2012/03/01/2375181.html

发表在 C# | 留下评论

ASP.NET MVC实用技术:开篇

ASP.NET MVC是一款基于ASP.NET的MVC模式的实现框架。通过使用ASP.NET MVC框架,开发人员能够非常方便地完成应用程序前台页面的开发工作。优秀的前台展示,对于大型企业级应用而言,是非常重要的组成部分,而ASP.NET MVC则为实现这一重要组成部分提供了技术和平台支持。目前,ASP.NET MVC已经到了4.0 Beta的版本,但我仍然打算以ASP.NET MVC 3为基础,通过几篇文章的篇幅,介绍一些ASP.NET MVC的实用技术,比如:如何实现自定义的认证机制、如何实现多主题效果的支持等。这些内容在网上也或多或少地提供了一些解决方案,但有些也不算太完整,有些又写的很含糊。因此,这几篇连载的文章会对每个实用技术进行完整的介绍,并在每篇文章结尾部分给出案例源代码及其使用方式,以供读者朋友们参考。

首先说明一下,这几篇文章不会对ASP.NET MVC中的概念进行描述,也不会太多地去深究某些技术实现细节,这些文章应该可以看成是几篇连载的“菜谱(Cook Book)”,读者朋友通过阅读并实践文中所述的内容,就能够获得所需的解决方案。虽然不能从根本上“解惑”,但也能在实践上为读者指明方向。此外,本人并不擅长.NET的前台技术,所以这几篇文章中提供的解决方案也不一定是最好的,如果读者朋友们有更好的想法或者思路,欢迎大家踊跃留言。

在介绍这些实用技术以前,首先让我们了解一下在企业级应用程序架构中,ASP.NET MVC的应用模式,这对我们今后介绍各种技术有一定的帮助,也算是能够让我们在架构的这个问题上达成一致。之后,我们再看看标准的ASP.NET MVC应用程序的数据库问题。

ASP.NET MVC的应用模式

在讨论ASP.NET MVC的应用模式时,对于MVC中的View和Controller的理解,我想应该不会存在什么太大的异议:View主要负责管理页面的显示布局和效果,并将Model中的数据展现在页面上;Controller则负责Model数据的获取与组织,并将数据以某种形式绑定到View上,或者从View上获取数据并对其进行必要的操作。现在需要讨论的是MVC中的这个“M”,从概念上讲,它应该是指View Model,因为它是View的数据源,但事实上在不同的应用程序中,它可能会被赋予不同的含义,于是也就产生了三种常见的ASP.NET MVC应用模式。比如,在实际项目中,我们有可能直接将Domain Model用作View Model,以便Controller能够直接操作Domain Model以完成业务逻辑处理;我们有可能把用于与分布式服务进行通信的数据传输对象(Data Transfer Objects)用作View Model,以便Controller能够通过分布式服务来获取或者提交数据,进而在分布式服务中对这些数据进行处理;我们还有可能在数据传输对象(Data Transfer Objects)与View Model之间再做一层映射,以解耦View Model与数据传输对象,因为在某些情况下View Model与数据传输对象仍然存在“阻抗失衡。针对这三种不同的应用模式,应用程序也会有着架构设计和部署上的差异。下面我们对这三种不同的应用模式进行详细讨论。

Domain Model as View Model

对于中小型应用程序而言,这种应用模式比较常见,在这种模式中,Domain Model被用作View Model,直接成为View的数据源,被显示在页面上。这有点像“数据源架构模式(Data Source Architectural Patterns)”中的Active Record模式。在Active Record模式中,对象用来表示数据库中的一张表或者一个视图,同时它又包含一些业务逻辑,于是,它混合了领域对象和数据访问对象的职责。在ASP.NET MVC中,如果采用Domain Model as View Model的模式,那么情况也就非常相似,因为这里的Model同时包含了领域模型和视图模型的职责。当然,除非是特殊情况,比如应用程序本身规模比较小,否则Domain Model as View Model并非一个合理的应用模式,它违背了软件设计的SRP(Single Responsibility Principle)的原则。

Domain Model as View Model的实现比较简单,只需要将Domain Model写在ASP.NET MVC Web应用程序中就可以了。为了能够参与View Model的职责,我们通常会在Domain Model上加入一些验证特性,比如使用RequiredAttribute来标注当前的属性是一个必填字段,以便基于javascript的客户端验证机制能够在必要的时候将错误信息提示给用户。在数据存储部分,通常可以选用ORM,将Domain Model直接持久化到数据库;也可以使用数据访问对象(Data Access Objects),配合使用ADO.NET来实现数据访问。常见的数据访问模式可以参考:Table Data GatewayRow Data GatewayActive Record以及Data Mapper

从物理部署的角度看,首先需要一台数据库服务器作为数据持久化基础架构,然后是一台IIS Web服务器,用来运行ASP.NET MVC Web应用程序,客户端浏览器通过访问这个IIS站点来完成系统交互。这是一个典型的三层结构,我用下图简要地描述了Domain Model as View Model的应用模式(说明:虽然将物理机器的部署和应用程序的逻辑结构画在一起并不是那么自然,毕竟逻辑视图和部署视图是两个完全不同的表述,但为了让读者能够看到在不同的物理部署节点上,运行着哪些逻辑组件,我还是勉强将它们画在了一张图中,因此如果在该图中如果有什么不合理的地方,还请读者指正):

image

 

Data Transfer Object as View Model

这种应用模式往往可以跟面向领域的架构模式相结合,提供比较合理的企业级应用程序解决方案。在这种应用模式中,领域模型不充当View Model的角色,它甚至与View Model毫无关系,只是作为一个业务组件运行在另一台物理服务器上。根据领域驱动设计(DDD)的实践指导,应用层通过仓储来管理领域对象的生命周期、使用这些领域对象和领域服务来完成所需的业务逻辑、并在领域对象与数据传输对象之间进行映射操作。当我们使用WCF作为服务供应者(Service Provider)时,通常会将WCF的Data Contract作为数据传输对象(Data Transfer Object),因此,应用层会完成Data Contract和领域对象之间的转换,然后通过WCF服务向外部提供服务接口。

作为向最终用户提供界面服务的ASP.NET MVC Web应用程序,它会通过服务引用(Service Reference)来使用由WCF公布的服务。在这个过程中,.NET会在ASP.NET MVC Web应用程序中创建一系列的WCF客户端代理类型,其中包括Client Channel、Data Contracts等。于是,MVC中的Controller会调用WCF服务以获得Data Contract对象,之后将这些Data Contract对象作为View的数据源绑定到View上。因此,整个过程事实上是使用数据传输对象作为View Model。类似地,我也针对这种应用模式画出了整个应用程序的结构细节,读者可以与上图进行比较:

image

在这种应用模式中,ASP.NET MVC不再参与任何业务逻辑,它仅仅是为用户界面提供服务,这样就完全解耦了用户界面层与业务层之间的关联关系。也就是说,我们可以不选用ASP.NET MVC技术,而选择其它的展现层技术(比如WPF或者Windows Forms)来设计和开发应用程序的展现部分。而在业务逻辑部分,基本上可以以DDD为指导进行设计与开发,这样做不仅可以将大部分分析与设计的精力放在领域模型上,而且还能够很自然地(或者说更合理地)应用一些企业架构模式。

这里其实会有一个技术问题,就是在ASP.NET MVC部分,Data Contract代理类型都是在Service Reference的过程中自动产生的,如果将这些Data Contract,也就是数据传输对象,直接作为View Model绑定到View上,那么似乎那些用于客户端验证的Attributes就无法添加到这个View Model上。事实上我觉得MS应该已经意识到了这种应用的可行性,所以已经有现成的解决方案了。这个解决方案得益于两件事情:C#的partial class,以及MetadataTypeAttribute。

比如,在自动生成的代理类中,有如下这个Data Contract:

[DataContract]
public partial class Customer {
  [DataMember]
	public string Name { get; set; }
}

那么我们可以使用下面的方法为Customer的Name属性添加验证规则:首先,针对Customer定义一个Metadata类,使其包含与Customer相同名称的属性(当然,只需要包含那些你需要添加验证特性的属性即可),然后在这个Metadata类的属性上,添加验证特性,如下:

public class CustomerMetadata {
	[Required(ErrorMessage="Name must be specified.")]
	public string Name { get; set; }
}

之后,使用partial关键字重定义Customer类,并在这个partial类上使用MetadataTypeAttribute,将CustomerMetadata“绑定”到Customer类即可:

[System.ComponentModel.DataAnnotations.MetadataTypeAttribute(typeof(CustomerMetadata))]
partial class Customer { }

Data Transfer Object as View Model的应用模式同时也向我们展示了.NET技术在领域驱动设计实践中的应用。接下来的几篇文章,都会以这种应用模式为背景,对ASP.NET MVC的一些实用技术进行介绍。

Mapped DTO as View Model

Mapped DTO,也就是根据View的需求,通过映射技术,将数据传输对象组装成View所需的View Model。其实与Data Transfer Object as View Model相比,它只是在View Model和Data Transfer Object之间又增加了一层映射处理,应用程序的其它部分与DTO as View Model完全相同。这种映射处理可以使用一些辅助的第三方框架(比如AutoMapper等)完成。在这里,我也就不打算对这种应用模式进行详细介绍了。

ASP.NET MVC应用模式大致也就是上述几种,这些讨论都是以MVC中的“M”为中心展开的。为了能够开始接下来的实用技术介绍,在此还需要简单地讨论一下ASP.NET MVC应用程序的数据库部分。

ASP.NET MVC应用程序的数据库

每当我们新建一个ASP.NET MVC应用程序的时候,Visual Studio模板都会帮我们把整个项目都建好,并会默认地使用SQL Server Express作为这个应用程序的后台数据库。如果你的机器上装有SQL Server Express,那么在你第一次运行ASP.NET MVC应用程序后,你应该就会在App_Data下找到一个aspnetdb.mdf的数据库文件,所有与ASP.NET MVC应用相关的数据表都会保存在这个数据库文件中。

当然,在大多数情况下,你有可能不会将SQL Server Express用作上线系统的数据库,或许你更倾向于使用SQL Server 2008 Enterprise Edition作为你的后台数据库,那么你就需要定制ASP.NET MVC应用程序。首先,打开Visual Studio 2010 Command Prompt,在命令行输入aspnet_regsql然后回车,这将打开ASP.NET SQL Server Setup Wizard:

image

在这个界面上单击Next按钮,在Select a Setup Option选项卡中,选择Configure SQL Server for application services选项,然后单击Next:

image

在Select Server and Database选项卡中,选择你要使用的数据库名称,然后单击Next:

image

在Confirm Your Settings选项卡中直接单击Next按钮,这样,向导就会将所需的数据库对象同步到你所选择的数据库中,然后在完成页面单击Finish按钮:

image

回到SQL Server管理控制台,展开我们刚刚创建的数据库,我们可以看到,所有所需的数据库对象都已经创建好了:

image

现在,回到ASP.NET MVC应用程序,在web.config中把数据库连接字符串直接改掉就可以了。

如果你的应用程序采用了上述“Data Transfer Object as View Model”的模式,那么你的后台数据库又将是另外一个景象:它或许连这些ASP.NET MVC所需的数据库对象都没有,于是你只能够自定义Membership Provider,然后在ASP.NET MVC中使用这个自定义的Membership Provider。在Tiny Library CQRS案例中已经采用了自定义的Membership Provider,有兴趣的读者可以先去了解一下,不过这部分内容也会在后续的篇章中详细介绍。

总结

本文为《ASP.NET MVC实用技术》系列文章的开篇,首先对ASP.NET MVC的应用模式进行了简单介绍,然后浏览了一下与ASP.NET MVC程序数据库相关的一些内容。在接下来的文章中,我会以ASP.NET MVC 3为基础,详细介绍一些实用技术,并会在每篇文章末尾给出完整源代码案例,供读者参考使用。

扩展阅读

有兴趣的读者可以参考以下资料,以对ASP.NET MVC的应用模式作进一步了解:

 

转载至:http://www.cnblogs.com/daxnet/archive/2012/03/16/2400418.html

发表在 C# | 留下评论

ASP.NET MVC实用技术:自定义AuthorizeAttribute与ActionLink的隐藏

在有些情况下,我们希望界面上的Action Link不仅仅是限制未授权用户的进一步访问,而是对于这些用户直接隐藏。比如,以普通用户登录时,只能在页面上看到一些常规的链接,而以管理员身份登录时,除了能看到这些常规链接外,还能够看到网站管理的链接。本文将介绍如何使用自定义的AuthorizeAttribute来实现这样的功能。

为了方便介绍,在这里不打算使用那些复杂的权限管理子系统或者权限验证机制,我们就做一个非常简单的假设:如果输入的用户名是“daxnet”,则表示这个账户是一个管理员账户,否则,它就是一个普通用户账户。在实际应用过程中,读者朋友可以采用自己的一套权限验证逻辑来判断某个账户是否为管理员。

WCF Service

在《ASP.NET MVC实用技术:开篇》一文,我已经介绍过了三种不同的ASP.NET MVC应用模式,其中第二种“Data Transfer Object as View Model”是在企业级应用中最为常见的一种方式,本文(及以后的系列文章)都会使用这种应用模式进行介绍。所以,在这里我们也还是需要建立一个WCF Service,用来向客户端返回登录用户权限认证的结果。

新建一个WCF Service,在其中定义一个enum类型,用来表示登录用户的账户类型。在这里我们只讨论两种类型:RegularUser,表示普通用户账户;SiteAdmin,表示网站管理员账户。这个enum类型定义如下:

/// <summary>
/// Represents the type of the account.
/// </summary>
[Flags]
public enum AccountType
{
    /// <summary>
    /// Indicates that the account is a regular user.
    /// </summary>
    [EnumMember]
    RegularUser = 1,
    /// <summary>
    /// Indicates that the account is the site administrator.
    /// </summary>
    [EnumMember]
    SiteAdmin = 2
}

使用FlagsAttribute来标记这个AccountType枚举,是为了今后能够更方便地处理更多类型的账户,事实上在我们这个案例中,并没有太大的实际意义。

然后,新建一个Service Contract,为了简化案例,这个Service Contract只包含一个操作,就是根据传入的用户账户名称,返回AccountType。

[ServiceContract(Namespace="http://aspnetmvcpractice.com")]
public interface IAccountService
{
    [OperationContract]
    AccountType GetAccountType(string userName);
}

之后在WCF Service中实现这个接口,根据我们上面的约定,当用户名为“daxnet”的时候,就返回SiteAdmin,否则就返回RegularUser,因此这个实现类还是非常简单的:

public class AccountService : IAccountService
{
    #region IAccountService Members
    public AccountType GetAccountType(string userName)
    {
        if (userName == "daxnet")
            return AccountType.SiteAdmin;
        else 
            return AccountType.RegularUser;
    }
    #endregion
}

至此,我们完成了WCF Service部分的开发,接下来,需要在ASP.NET MVC中使用这个WCF Service来完成用户的验证操作。在通常情况下,我们会在ASP.NET MVC的应用程序上直接添加WCF Service的引用,这样做其实也没有什么太大的问题,不过我还是比较习惯另外新建一个Class Library,然后将WCF Service Reference添加到这个Class Library上,这样做的好处是,可以把所有与ASP.NET MVC扩展相关的内容都集中起来,而且这种扩展相关的类型和方法都有可能需要用到WCF Service提供的服务,这样也不至于将ASP.NET MVC应用程序的结构弄得很乱。在这个案例中,我们新建一个名为WebExtensions的Class Library,在这个Library中使用刚刚创建好的WCF Service来实现我们的自定义授权特性。

Web Extensions

CustomAuthorizeAttribute

在新建的这个Class Library中直接添加WCF Service Reference,这将在这个Library中产生一系列的代理类型,以及一个app.config文件。不要去关注这个app.config文件,因为它在这个Class Library中并不起什么作用;但是也不要去删除这个文件,因为后面我们还是需要用到它里面的内容的。

在Class Library中,新建一个CustomAuthorizeAttribute类,使这个类继承于AuthorizeAttribute。我们会在后面将这个Attribute用在action上,以限制未授权用户对页面的访问。在这个类中,重载AuthorizeCore方法,它的处理逻辑如下:首先判断当前账户是否被认证,如果没有,则返回false;然后调用WCF Service来获取当前账户的类型,并跟给定的类型进行比较,如果类型相同,则返回true,否则返回false。假设这个给定的账户类型是通过CustomAuthorizeAttribute类的构造函数传入的,那么,当我们在某个action上应用[CustomAuthorizeAttribute(AccountType.SiteAdmin)]这个特性的时候,只要访问这个action的用户账户不是SiteAdmin,程序就会自动跳转到登录页面,请求用户以网站管理员的身份登录。CustomAuthorizeAttribute类的代码如下:

public class CustomAuthorizeAttribute : AuthorizeAttribute
{
    private readonly AccountType requiredType;

    public CustomAuthorizeAttribute(AccountType comparedWithType)
    {
        this.requiredType = comparedWithType;
    }

    internal bool PerformAuthorizeCore(System.Web.HttpContextBase httpContext) { return this.AuthorizeCore(httpContext); }

    protected override bool AuthorizeCore(System.Web.HttpContextBase httpContext)
    {
        if (httpContext == null)
            throw new ArgumentNullException("httpContext");

        if (!httpContext.User.Identity.IsAuthenticated)
            return false;

        if (this.requiredType == (AccountType.SiteAdmin | AccountType.RegularUser))
            return true;

        using (AccountServiceClient client = new AccountServiceClient())
        {
            var calculatedAccountType = client.GetAccountType(httpContext.User.Identity.Name);

            switch(this.requiredType)
            {
                case AccountType.RegularUser:
                    if ((calculatedAccountType & AccountType.RegularUser) == AccountType.RegularUser)
                        return true;
                    else
                        return false;
                case AccountType.SiteAdmin:
                    if ((calculatedAccountType & AccountType.SiteAdmin) == AccountType.SiteAdmin)
                        return true;
                    else
                        return false;
                default:
                    return base.AuthorizeCore(httpContext);
            }
        }
    }
}

在这个类中有一个internal的方法:PerformAuthorizeCore,它的作用就是向程序集的其它方法暴露AuthorizeCore的执行逻辑,以避免相同的逻辑需要在程序集内部的其它类型中重复实现。这个PerformAuthorizeCore的方法会在自定义的HtmlHelper扩展方法中使用,目的就是为了能够对未授权的账户隐藏Action Link。

HtmlHelper Extension

现在我们来扩展HtmlHelper类,使得其中的ActionLink方法能够支持对未授权账户的隐藏。同样也是在当前这个Class Library中,新建一个静态类,命名为MvcExtensions,然后使用下面的代码实现这个类:

public static class MvcExtensions
{
    private static bool Visible(HtmlHelper helper, AccountType accountType)
    {
        return new CustomAuthorizeAttribute(accountType).PerformAuthorizeCore(helper.ViewContext.HttpContext);
    }
    /// <summary>
    /// Returns an anchor element (a element) that contains the virtual path of the specified action.
    /// </summary>
    /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
    /// <param name="linkText">The inner text of the anchor element.</param>
    /// <param name="actionName">The name of the action.</param>
    /// <param name="controllerName">The name of the controller.</param>
    /// <param name="accountTypeRequired">The required account type.</param>
    /// <returns>The anchor element (a element) that contains the virtual path of the specified action.</returns>
    public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, 
        string linkText, 
        string actionName, 
        string controllerName, 
        AccountType accountTypeRequired)
    {
        MvcHtmlString link = MvcHtmlString.Empty;
        if (Visible(htmlHelper, accountTypeRequired))
            link = htmlHelper.ActionLink(linkText, actionName, controllerName);
        return link;
    }
}

这个ActionLink方法首先将link设置为MvcHtmlString.Empty,表示为一个空的string,然后调用私用静态方法Visible,来判断当前用户是否应该看到这个ActionLink,如果Visible返回的是true,则直接调用HtmlHelper中已有的ActionLink重载方法,否则直接返回MvcHtmlString.Empty。在Visible方法中,我们可以看到,所执行的逻辑正是CustomAuthorizeAttribute中的AuthorizeCore方法。

接下来要做的,就是在ASP.NET MVC应用程序中使用这些扩展方法和自定义特性。

ASP.NET MVC应用程序

在ASP.NET MVC应用程序上添加对上述Class Library的引用,然后我们打开Views\Shared\_Layout.cshtml文件,在这个Razor View中添加对所需命名空间的引用:

image

然后,根据需要,我们向主菜单中添加两个ActionLink:Regular Users Only和Site Admins Only,前者仅允许普通用户访问,后者仅允许站点管理员访问。在此所使用的ActionLink就是在上文中我们自定义的那个重载:

image

 

接下来在HomeController中定义两个action:RegularUserVisible和SiteAdminVisible,并将CustomAuthorizeAttribute应用在这两个action上。事实上这个步骤与隐藏Action Link并没有太大关系,只是确保用户无法通过在浏览器中输入URL而直接访问到这两个页面。

image

 

最后别忘了把Class Library下app.config中有关system.serviceModel的配置复制到ASP.NET MVC应用程序的web.config中。

运行程序

现在让我们来启动程序,看看会产生什么效果。首先启动WCF Service,然后直接运行ASP.NET MVC应用程序,得到如下界面:

image

 

现在点击“Log On”链接,以daxnet账户登录,我们得到了如下的效果,可以看到页面上显示了“Site Admins Only”的链接选项:

image

 

退出登录,再以“acqy”账户登录,我们又得到了如下效果,看到页面上显示了“Regular Users Only”的选项:

image

 

本文案例源代码下载

下载链接

单击此处下载本文案例源代码

有关数据库配置

本文使用的是SQL Server Enterprise Edition作为ASP.NET MVC的后台数据库,如果你打算选用SQL Server Express作为数据库,请修改本文案例中web.config里的连接字符串,并使用《ASP.NET MVC实用技术:开篇》一文中所介绍的方法重建你的数据库结构。根据本文案例需要,你需要在ASP.NET MVC应用程序启动以后,新建两个用户账户:daxnet以及另一个任意名称的账户。当你正确地配置好了ASP.NET MVC的数据库以后,你可以在Solution Explorer中单击ASP.NET Configuration按钮来配置你的ASP.NET MVC站点,以添加所需的用户账户:

image

转载至:http://www.cnblogs.com/daxnet/archive/2012/03/23/2413260.html

发表在 C# | 留下评论