ASP.NET MVC3、WCF和Entity Framework的多层架构示例
本文旨在介绍一种在.NET中解耦、单元测试友好、部署灵活、实现高效且验证灵活的多层架构。
目录
- 概述
- 使用代码
- 所有层的概述
- 示例多层应用程序的主要成就
- 示例应用程序中的Visual Studio 2010项目
- 文件夹结构及其与层的匹配
- 主要文件夹和项目摘要
- 几个主要组件组和项目的详细说明
- 讨论
- 图1所示多层架构的可能配置结果和分析
- 除了多层架构的通用优点外,示例多层架构的其他优点
- WCF实现与WCF主机的分离
- 库项目 GH.Northwind.Persistence 的目的
- 为什么不使用Domain Service类功能作为业务层?
- 为什么客户端不直接调用WCF Data Service,而是通过业务层调用?
- 业务实体类和代码生成器
- 数据验证
- 示例多层架构应用程序的一些配置结果
- 结论
概述
多层软件架构可以解决以下客户端/服务器系统问题:可伸缩性、安全性、容错性等。在我们之前的文章《多层架构和技巧》中,我们介绍了多层架构的基本概念和一些实践技巧。在本文中,我们将详细介绍一个使用ASP.NET MVC3、WCF和Entity Framework的多层架构示例。在Java中,通常有一种预定义的方式来实现多层架构:J2EE架构,它使用session bean over entity bean作为业务层和持久层,Java bean、servlet或JSP作为客户端表示层,Java Swing、HTML或applet作为客户端。因此,对于J2EE,不同的应用程序非常可能以非常相似的方式实现。然而,在.NET中,尽管有许多工具和功能可用,但没有像J2EE那样预定义的方式来指导如何实现多层架构。结果,有太多不一致的现有方法。有些好;有些坏。本文旨在介绍一种在.NET中解耦、单元测试友好、部署灵活、实现高效且验证灵活的多层架构。我们在这里实现的是将.NET中一些知名且优秀的工具和功能结合起来,并提出一个可行的解决方案。由于一篇文章涵盖的内容太多,我们将主要集中在我们示例解决方案中的多层架构的业务层和持久层,但也会简要提及其他层。为了更好地理解本文,建议您先阅读我们之前关于多层架构基础的文章这里。与我们之前的文章一样,本文也基于一个团队对多层架构的所有层都拥有完全控制权的假设。
使用代码
代码在Visual Studio 2010中创建。在构建和运行此项目之前,需要执行以下步骤。1) 在SQL Server中安装Northwind数据库;我使用的是SQL Server Express 2008R2;其他版本也应该可以。在Sql Server Management Studio中打开\GH.3Tier.Demo\instnwnd.sql脚本文件,然后执行它。2) 在三个应用程序项目GH.Northwind.Web、GH.Northwind.Business.Host和GH.Northwind.EntityFramework.Host的配置文件中,使用您自己的SQL Server名称更新NorthwindEntities数据库connectionString的数据源。3) 如果项目GH.Northwind.Web的配置文件中的参数“N-Tier”和“UseWcfDataservice”的组合结果,以及项目GH.Northwind.Business.Host中的参数“UseWcfDataservice”需要WCF服务或WCF数据服务,那么我们需要在Visual Studio的解决方案资源管理器中右键单击项目GH.Northwind.Business.Host,然后单击“在浏览器中查看”来本地启动它。我们也可以通过类似的方式本地启动WCF数据服务项目GH.Northwind.EntityFramework.Host;有关一些可能的组合结果,请参见后面的表2。目前,我们只在ASP.NET MVC3中实现了Product实体的CRUD操作。
免责声明
示例代码仅基于我个人的学习和研究成果,并非基于任何实际项目。此示例中使用的Northwind数据库是微软著名的公共数据库,它最初包含在每个Microsoft Access数据库和一些旧的SQL Server中。代码仅用于解释整体架构,因此非常原始且远未完成。如果您想在项目中使用这些代码,您需要进行丰富和修改以满足您的需求。此外,代码尚未进行单元测试,如果您遇到任何错误,请随时纠正。例如,在实际应用中,如果您的业务确定只坚持一种持久化技术,那么您可能只需要实现一种持久化层,无论是DataServiceContext、ObjectContext还是DbContext。在我们的示例演示项目中,我们实现了所有DataServiceContext、ObjectContext和DbContext,以解释我们可以轻松地切换持久化技术,而无需修改业务层。另一个例子是,如果您不希望库GH.Northwind.Business与客户端表示层部署在同一台Web服务器上以保证安全,您可以删除它在项目GH.Northwind.Client.Common中的引用和代码。这样,您可以确保始终可以将客户端表示层和业务层部署在不同的机器上以保证安全。在我们的示例项目中,我们通过允许业务层部署在与客户端表示层相同的计算机上来解释我们的部署灵活性;这种部署在许多实际情况中也存在。此外,示例项目中的CRUD操作非常原始,只有基本功能,您可以对其进行丰富,添加一些高级功能,如多对多关系和多对一关系处理等。
所有层的概述
下面是本文将讨论的.NET多层架构模型的图
图1:.NET中的多层架构
每层中的椭圆形组件可能共存或单独存在。我们将在下面总结所有层。
客户端层:此层直接与用户交互。可能存在多种不同类型的客户端共存,例如WPF、Window Form、HTML网页等。
客户端表示层:包含客户端所需的表示逻辑,例如IIS Web服务器中的ASP.NET MVC。它还将不同的客户端适配到业务层。此层绘制了三个组件:通用库、ASP.NET、WPF客户端库。通用库包含所有类型客户端的可重用通用代码。ASP.NET库和WPF客户端库分别是Web客户端和WPF客户端的库。如果添加了新型客户端,我们可能需要添加额外的库来仅支持新型客户端,例如Windows Form客户端。
业务层:处理并封装所有业务域和逻辑;也称为域层。此层有两个组件:WCF库和WCF主机应用程序;所有业务逻辑和实现都在WCF库中;WCF主机应用程序仅处理WCF部署。在2层架构配置中,客户端表示层将直接调用WCF库;而在完整的N层架构配置中,客户端表示层将直接调用WCF主机应用程序。此层的业务接口通常是一个面向业务的Facade层,它向客户端公开业务操作,其程度正好满足客户端的需求。因此,它也充当客户端的代理层,以保护业务域和逻辑。客户端通常将业务层视为一个黑盒子:向业务层发送请求,请求完成并得到响应;客户端通常不知道如何满足其请求。
持久层:处理业务数据到数据层的读/写,也称为数据访问层(DAL)。此层有三个组件:Entity Framework库、持久化库和可选的WCF数据服务。持久化库是一个通用库,用于促进和简化业务层中Entity Framework或其他持久化技术的使用;它还将业务层与Entity Framework解耦。业务层将直接调用持久化库,而不是Entity Framework。根据应用程序配置,持久化库随后将直接调用Entity Framework库,或直接调用在Entity Framework库之上创建的WCF数据服务。如果数据库作为多种不同类型应用程序的数据中心,WCF数据服务将是一个不错的选择;否则,我们可能需要避免使用WCF数据服务以提高性能。
数据层:外部数据源,例如数据库服务器、CRM系统、ERP系统、大型机或其他遗留系统等。数据层用于存储应用程序数据。数据库服务器是目前最流行的。我们在图2中列出了三种现代数据库供选择。
示例多层应用程序的主要成就
我们在示例应用程序中做了以下主要工作:
- 一个层仅通过接口依赖于另一个层,而不是具体的类。
- 通过简单更新配置文件中的两个
bool
参数值,即可轻松切换不同的层架构:“UseWcfDataService”和“N-Tier”。我们通过将WCF实现与WCF主机分离到不同的项目中,以及客户端以非代理方式访问WCF服务来实现这一点。 - 为了在整个应用程序中使用它们,我们使用T4模板在轻量级库项目GH.Northwind.Business.Entities中自动生成并维护唯一版本的实体类,而不是原始的Entity Framework项目,这样我们就可以在任何地方重新生成和重用这些自动生成的实体类,而不是一个充斥着重量级Entity Framework的混乱地方。
- 业务层不直接调用Entity Framework,而是通过持久化库调用。这样做有助于我们实现业务层与持久化层之间的最大程度解耦,并允许我们轻松地交换持久化技术,而不会对业务层产生任何副作用。此外,它促进并简化了业务层中Entity Framework的使用。示例应用程序演示了我们可以通过仅更新配置文件中布尔参数“
UseWcfDataService
”的值,轻松地在WCF数据服务和DbContext(或:ObjectContext)之间切换持久化技术。 - 对于我们示例应用程序中的数据验证,我们使用自动生成的元数据类进行简单属性验证,并使用接口IValidatableObject进行类级别的交叉验证。目前,我们将所有验证逻辑放在一个地方:项目GH.Northwind.Business.Entities,这样我们只需要实现和维护一个版本的验证逻辑。如果出于安全原因,在某些情况下,我们可以将所有验证逻辑移到自己的库项目中。所有层共享此版本的验证逻辑。我们可以根据实际需求轻松地在许多层中调用这些验证。目前,我们在HTML网页客户端层和ASP.NET MVC3客户端表示层中调用这些验证。
- 对于此WCF业务服务层的错误处理,我们实现了IErrorHandler行为来捕获所有异常,并将它们转换为FaultException,以便在发生异常后服务通道不会被破坏,并且可以再次使用。此外,FaultException可以发送到客户端以帮助调试。
- 尽可能尝试自动生成代码:所有具有wcf标签的实体类和具有注释标签的元数据类都由代码生成器自动生成。我们甚至通过Domain Service类向导自动生成了业务接口的草稿版本。
- 我们在客户端使用非代理方式访问WCF服务和WCF数据服务。这样做可以让我们使用WCF服务也使用的共享公共库中的数据契约(主要是自动生成的实体类)和服务接口。因此,我们避免了由于代理方式的服务引用而触发的服务接口和可能的重复数据契约代码;还避免了需要不时更新服务引用以同步WCF服务端的更改。当然,代理方式的服务引用也可以使用公共库中的共享数据契约,但对于代理方式来说,重复的服务接口和偶尔的服务引用更新仍然是不可避免的。
示例应用程序中的Visual Studio 2010项目
文件夹结构及其与层的匹配
现在,让我们看看Visual Studio 2010项目中的内容。我们试图使源代码文件夹结构清晰简单;下面是Visual Studio解决方案资源管理器中整个应用程序的文件夹结构。
图2:示例应用程序的Visual Studio解决方案资源管理器中的文件夹结构。
我们上面的文件夹结构与图1中的层匹配方式如下:
客户端层:用户计算机浏览器中显示的HTML页面;这些HTML页面由Web服务器上的客户端表示层项目GH.Northwind/Clients/GH.Northwind.Web生成。到目前为止,我们只有一个客户端类型。
客户端表示层:子文件夹GH.Northwind/Clients下的所有2个项目。
业务层:子文件夹GH.Northwind/Business下的所有4个项目。
持久层:子文件夹GH.Northwind/Persistence下的所有3个项目。
数据层:MS SQL Server 2008 R2
主要文件夹和项目摘要
1) 项目 GH.Common:包含所有通用组件,这些组件可重用于任何应用程序,不仅仅是Northwind;它包含子文件夹:LogService、ServiceLocator、Framework等。LogService当前包含一个日志接口及其默认实现;您可以轻松插入任何类型的日志提供程序,例如log4Net、NLog、Enterprise Library日志、.NET内置的Trace/Debug等。ServiceLocator实现了Ninject包的一个简单服务定位器。Framework包含与所有应用程序架构相关的顶层类。
2) 子文件夹 GH.Northwind:包含与Northwind应用程序相关的所有项目。如果有一个新的应用程序用于类注册,我们可以添加一个新的文件夹GH.ClassRegistration作为GH.Northwind的同级文件夹来包含与类注册应用程序相关的所有项目。
在GH.Northwind文件夹下,我们有三个子文件夹
- GH.Northwind.Business.Entities:一个库项目,包含整个Northwind应用程序使用的所有业务POCO实体;业务验证目前也放在这个文件夹中。
- GH.Northwind.Business.Interfaces:一个库项目,包含客户端层使用的所有业务操作接口。
- GH.Northwind.Business:一个核心WCF业务服务库项目,实现了GH.Northwind.Business.Interfaces项目中定义的所有业务操作和逻辑。
- GH.Northwind.Business.Host:一个WCF服务主机应用程序项目,主要用于GH.Northwind.Business项目的部署目的。目前配置为Web服务部署。
子文件夹 Clients: 包含与客户端表示层和客户端层相关的所有项目。项目GH.Northwind.Client.Common包含所有类型客户端的所有通用组件;项目GH.Northwind.Client.Web是Web客户端的客户端表示层。客户端层可以具有所有类型的客户端,例如ASP、WPF或Windows Form。在这里,我们只将一种客户端作为演示:ASP.NET MVC3 Web客户端。
子文件夹 Persistence: 包含3个与持久层相关的项目:
- GH.Northwind.EntityFramework:一个库项目,包含Entity Framework。
- GH.Northwind.EntityFramework.Host:一个WCF数据服务项目,使用GH.Northwind.EntityFramework项目中的Entity Framework。
- GH.Northwind.Persistence:一个常规类库,作为业务层和Entity Framework之间的桥梁。
3) 子文件夹 Tests:所有测试都应该放在这个文件夹中,包括单元测试和功能测试。代码应该有测试;测试应该是自动化的。目前,这个文件夹是空的;您可以将任何测试添加到这个文件夹中。我们将所有测试分组在一起,以便更容易地控制、管理和自动化测试。此外,通过这种方式,我们可以得到更干净的产品代码,而不被测试代码所干扰。
几个主要组件组和项目的详细说明
1) 项目 GH.Common 中的 Framework 文件夹:包含驱动每一层的所有顶层框架类;这些顶层框架可用于许多应用程序,而不仅仅是GH.Northwind。因此,我们有一个薄框架但厚应用程序的架构。此文件夹中有三个子文件夹
a) 子文件夹 Persistence: 业务层和Entity Framework之间的抽象适配器层。IPersistence
是一个顶层通用接口,仅包含基本的数据库CRUD操作用于演示,不包含一些高级数据库功能,例如数据库多对多和多对一关系处理。IPersistence
将业务层与实际的持久化技术(如Entity Framework)解耦;它还促进和简化了业务层中持久化技术的使用。以下是此接口
public interface IPersistence<T>
{
void Insert(T entity, bool commit);
void Update(T entity, bool commit);
void Delete(T entity, bool commit);
void Commit();
IQueryable<T> SearchBy(Expression<Func<T, bool>> predicate);
IQueryable<T> GetAll();
}
PersistSvr
是一个静态类,仅用于IPersistence
接口的用户友好使用目的;PersistSvr
通过服务定位器获取其PersistenceProvider
。因此,切换到另一个持久化提供程序就像通过服务定位器注册一个不同的持久化提供程序一样简单。GH.Common.Framework.Persistence文件夹中有三个子文件夹:DataServiceContext用于WCF数据服务,ObjectContext
用于默认Entity Framework,DbContext
用于Entity Framework Code First。这三者非常相似。这些文件夹中的文件PersistenceBase.cs以各自的方式实现了IPersistence
接口。不同的人可能以不同的方式实现此持久化层。我们的目标是最小化GH.Northwind.Persistence项目中具体子类的额外代码,稍后将对此进行描述。以下是子文件夹DbCxt
中DbContext
的PersistenceBase.cs文件的示例实现:
public class PersistenceBase<T> : IPersistence<T> where T : BusinessEntityBase
{
protected String _entitySetName = String.Empty;
public ILogger<PersistenceBase<T>> Logger { get; set; }
public static ILogger<PersistenceBase<T>> Log
{
get { return Log<PersistenceBase<T>>.LogProvider; }
}
public static DbContext DataContext
{
get { return DataCxt.Cxt; }
}
#region IPersistence<T> Members
public virtual void Insert(T entity, bool commit)
{
InsertObject(entity, commit);
}
public virtual void Update(T entity, bool commit)
{
UpdateObject(entity, commit);
}
public virtual void Delete(T entity, bool commit)
{
DeleteObject(entity, commit);
}
public virtual void Commit()
{
SaveChanges();
}
public Expression<Func<T, bool>> predicate { get; set; }
public virtual IQueryable<T> SearchBy(Expression<Func<T, bool>> predicate)
{
return EntitySet.Where(predicate);
}
public virtual IQueryable<T> GetAll()
{
return EntitySet;
}
#endregion
protected virtual T FindMatchedOne(T toBeMatched)
{
throw new ApplicationException("PersistenceBase.EntitySet: Shouldn't get here.");
}
protected virtual IQueryable<T> EntitySet
{
get { throw new ApplicationException("PersistenceBase.EntitySet: Shouldn't get here."); }
}
protected virtual String EntitySetName
{
get { throw new ApplicationException("PersistenceBase.EntitySetName: Shouldn't get here."); }
}
protected void InsertObject(T entity, bool commit)
{
DataContext.Entry(entity).State = EntityState.Added;
try
{
if (commit) SaveChanges();
}
catch (Exception e)
{
Log.Error(e);
throw;
}
}
protected void UpdateObject(T entity, bool commit)
{
try
{
DbEntityEntry entry = DataContext.Entry(entity);
DataContext.Entry(entity).State = EntityState.Modified;
if (commit) SaveChanges();
}
catch (InvalidOperationException e)
// Usually the error getting here will have a message:
// "an object with the same key already exists in the
// ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key"
{
T t = FindMatchedOne(entity);
if(t==null) throw new ApplicationException("Entity doesn't exist in the repository");
try
{
DataContext.Entry(t).State = EntityState.Detached;
(EntitySet as DbSet<T>).Attach(entity);
DataContext.Entry(entity).State = EntityState.Modified;
if (commit) SaveChanges();
} catch(Exception exx)
{
//Roll back
DataContext.Entry(entity).State = EntityState.Detached;
(EntitySet as DbSet<T>).Attach(t);
Log.Error(exx);
throw;
}
}
catch (Exception ex)
{
Log.Error(ex);
throw;
}
}
protected void DeleteObject(T entity, bool commit)
{
T t = FindMatchedOne(entity);
(EntitySet as DbSet<T>).Remove(t);
try
{
if (commit) SaveChanges();
}
catch (Exception e)
{
Log.Error(e);
throw;
}
}
protected void SaveChanges()
{
try
{
DataContext.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
// Update original values from the database (Similar ClientWins in ObjectContext.Refresh)
var entry = ex.Entries.Single();
entry.OriginalValues.SetValues(entry.GetDatabaseValues());
DataContext.SaveChanges();
}
} // End of function
}
上述实现非常直接。这里要提到的一点是受保护的函数UpdateObject
。在此函数中,我们捕获“InvalidOperationException
”异常,该异常通常是由于传入的实体参数当前未被EntityFramework的ObjectStateManager
跟踪造成的。然后,我们分离当前跟踪的实体,并附加传入的实体。另一件要提及的事情是函数SaveChange;在此函数中,我们通过从数据库加载新值来处理乐观并发异常。对于ObjectContext
和WCF数据服务,我们需要以各自的方式处理这些问题。
b) 子文件夹 Business:包含业务层的超类。目前,此文件夹中有一个抽象类BusinessEntityBase,用于作为所有业务实体的超类。它实现了三个接口:IValidatableObject
、IDataErrorInfo
、INotifyPropertyChanged
。IValidatableObject
用于类级别的业务验证。IDataErrorInfo
和INotifyPropertyChanged
用于具有MVVM模式的WPF客户端,用于用户输入数据验证和ViewModel与View之间的数据绑定,有关详细信息,请参阅Microsoft MSDN文章Implementing the MVVM Pattern。目前在我们的示例解决方案中,只有一个ASP.NET客户端,因此接口IDataErrorInfo
和INotifyPropertyChanged
在我们的示例解决方案中实际上没有被使用;我们暂时将其注释掉了。
c) Subfolder Client: 应包含客户端的所有顶层类。目前仅包含客户端命令相关的类,这些类在本示例项目中尚未被使用。
2) \GH.Northwind\Persistence 文件夹中的项目 GH.Northwind.Persistence:是Northwind业务层和Northwind Entity Framework之间的具体适配器层。包含Northwind应用程序的三种不同情况下的持久化子类:WCF数据服务、ObjectContext和DbContext。这里的子类非常简单,并从GH.Common.Framework.Persistence文件夹中的超类PersistenceBase继承。由于其简单性,我们将所有与业务相关的类放在一个文件中。请参见\GH.Northwind\Persistence\DbCxt文件夹中的NorthwindPrst.cs文件。
namespace GH.Northwind.Persistence.DbCxt
{
public class CustomerPrst : PersistenceBase<Customer>
{
protected override IQueryable<Customer> EntitySet
{
get { return (DataContext as NorthwindEntities).Customers; }
}
protected override String EntitySetName
{
get { return _entitySetName ?? (_entitySetName =
Util.GetMemberNameExtra((NorthwindEntities g) => g.Customers)); }
}
protected override Customer FindMatchedOne(Customer toBeMatched) {
return EntitySet.DefaultIfEmpty(null).First(o => o.CustomerID == toBeMatched.CustomerID); }
}
public class ProductPrst : PersistenceBase<Product>
{
protected override IQueryable<Product> EntitySet
{
get
{
predicate = p => p.ProductID == 1;
return (DataContext as NorthwindEntities).Products;
}
}
protected override String EntitySetName
{
get { return _entitySetName ?? (_entitySetName =
Util.GetMemberNameExtra((NorthwindEntities g) => g.Products)); }
}
protected override Product FindMatchedOne(Product toBeMatched) {
return EntitySet.DefaultIfEmpty(null).First(o => o.ProductID == toBeMatched.ProductID); }
}
public class OrderPrst : PersistenceBase<Order>
{
protected override IQueryable<Order> EntitySet
{
get { return (DataContext as NorthwindEntities).Orders; }
}
protected override String EntitySetName
{
get { return _entitySetName ?? (_entitySetName =
Util.GetMemberNameExtra((NorthwindEntities g) => g.Orders)); }
}
protected override Order FindMatchedOne(Order toBeMatched) {
return EntitySet.DefaultIfEmpty(null).First(o => o.OrderID == toBeMatched.OrderID); }
}
public class Order_DetailPrst : PersistenceBase<Order_Detail>
{
protected override IQueryable<Order_Detail> EntitySet
{
get { return (DataContext as NorthwindEntities).Order_Details; }
}
protected override String EntitySetName
{
get
{
return _entitySetName ??
(_entitySetName = Util.GetMemberNameExtra((NorthwindEntities g) => g.Order_Details));
}
}
protected override Order_Detail FindMatchedOne(Order_Detail toBeMatched) {
return EntitySet.DefaultIfEmpty(null).First(o => o.OrderID ==
toBeMatched.OrderID && o.ProductID == toBeMatched.ProductID); }
}
public class SupplierPrst : PersistenceBase<Supplier>
{
protected override IQueryable<Supplier> EntitySet
{
get { return (DataContext as NorthwindEntities).Suppliers; }
}
protected override String EntitySetName
{
get
{
return _entitySetName ??
(_entitySetName = Util.GetMemberNameExtra((NorthwindEntities g) => g.Suppliers));
}
}
protected override Supplier FindMatchedOne(Supplier toBeMatched) {
return EntitySet.DefaultIfEmpty(null).First(o => o.SupplierID == toBeMatched.SupplierID); }
}
public class CategoryPrst : PersistenceBase<Category>
{
protected override IQueryable<Category> EntitySet
{
get { return (DataContext as NorthwindEntities).Categories; }
}
protected override String EntitySetName
{
get
{
return _entitySetName ??
(_entitySetName = Util.GetMemberNameExtra((NorthwindEntities g) => g.Categories));
}
}
protected override Category FindMatchedOne(Category toBeMatched) {
return EntitySet.DefaultIfEmpty(null).First(o => o.CategoryID == toBeMatched.CategoryID); }
}
}
您可以看到,我们只需要从超类中重写两个属性和一个函数:属性EntitySet、EntitySetName和函数FindMatchedOne
。对于EntitySetName,我们尝试避免硬编码名称,而是通过Lambda表达式解决。为什么?首先,硬编码容易出错。此外,硬编码的错误在编译时无法检测到,直到运行时。通过强类型Lambda表达式解决方案,由于更改或拼写错误引起的问题可以在编译期间检测到。函数FunctionMatchedOne
用于根据传入参数toBeMatched的相同标识来查找匹配的记录;该标识可能是一个复合键,例如表Order_Detail
中的OrderId和ProductId。
对于DataServiceContext,我们尝试使用非代理方式访问数据服务。带有命名空间HelpOnly的服务引用代理是一个虚拟代理,仅用于通过Lamda函数获取EntitySetName。如果您不喜欢这个虚拟代理,可以删除它,然后硬编码EntitySetName,但会有上面段落中提到的一些缺点。
3) \GH.Northwind\Persistence 文件夹中的项目 GH.Northwind.EntityFramework:这是一个WCF库项目,包含Northwind数据库之上的Entity Framework。请阅读此处了解如何通过Entity Data Model Wizard添加Entity Framework模型。为了简化演示,我们只选择了6个表:Products、Customers、Orders、Order_Details、Categories和Supplier。基本上,创建一个空的WCF库项目,然后使用上述向导添加ADO.NET Entity Classes。Entity Framework向导的默认实体类有点重量级。然后,我们可以通过支持WCF的DbContext生成器将其进一步转换为轻量级的DbContext POCO实体类;稍后我们将提供详细的步骤来实现这一点。最后,我们需要将这些实体类的T4模板复制并粘贴到项目GH.Northwind.Business.Entities中,以便整个应用程序使用。以下是实现这些任务的步骤
步骤1: 单击并打开GH.Northwind.EntityFramework项目中的文件GHNorthwindModels.edmx,然后您将看到一些表图。右键单击图区域,然后单击“添加代码生成项”,然后单击“在线模板”,然后选择“ADO.NET C# DbContext Generator With WCF Support”,单击“添加”然后“确定”,将生成两个文件Model1.Context.tt和Model1.tt。将其重命名为NorthwindModels.Context.tt和NorthwindModels.tt,单击NorthwindModels.tt,您可以看到所有模型都在这里。将NorthwindModels.tt复制并粘贴到项目GH.Northwind.Business.Entities中。
步骤2: 在项目GH.Northwind.Business.Entities中的文件NorthwindModels.tt中更改以下行,从
string inputFile = @"GHNorthwindModels.edmx";
改为
string inputFile = @"..\..\Persistence\\GH.Northwind.EntityFramework\\GHNorthwindModels.edmx";
现在粘贴的NorthwindModels.tt指向了实体文件正确的路径。
步骤3: 在项目GH.Northwind.Business.Entities中右键单击NorthwindModels.tt,单击“运行自定义工具”,然后单击NorthwindModels.tt文件,您将看到生成了几个实体文件。它们的命名空间与在GH.Northwind.EntityFramework项目中生成的实体命名空间不同。从现在开始,我们将在整个应用程序中引用GH.Northwind.Business.Entities项目中的实体,即使是在GH.Northwind.EntityFramework项目中。
步骤4: 在项目GH.Northwind.EntityFramework中,向文件NorthwindModels.Context.cs中添加一行“using GH.Northwind.Business.Entities”,以便Entity Framework引用GH.Northwind.Business.Entities项目中的实体。每次重新运行代码生成器工具后,“using GH.Northwind.Business.Entities”添加的行将被擦除。您需要手动再次添加。保持使用一个版本将避免在许多其他地方出现实体冲突。
步骤5: 删除GH.Northwind.EntityFramework项目中的原始文件NorthwindModels.tt,因为我们不再需要它。
以上步骤适用于DbContext。对于ObjectContext,步骤相同,但请确保使用代码生成器“ADO.NET POCO Entity Generator With WCF Support”。
4) \GH.Northwind\Persistence 文件夹中的项目 GH.Northwind.EntityFramework.Host:这是一个WCF服务应用程序项目,具有一个基于GH.Northwind.EntityFramework项目中Entity Framework的WCF数据服务。请阅读此处了解如何添加基于Entity Framework的WCF数据服务。
确保将项目GH.Northwind.EntityFramework添加为此项目的引用。此外,将下面的连接字符串设置从GH.Northwind.EntityFramework配置文件复制到GH.Northwind.EntityFramework.Host配置文件;否则,将无法工作
<connectionStrings>
<add name="NorthwindEntities"
connectionString="metadata=res://*/GHNorthwindModels.csdl|res://*/GHNorthwindModels.ssdl|
res://*/GHNorthwindModels.msl;provider=System.Data.SqlClient;provider
connection string="data source=WHU-DT\SQLEXPRESS10;initial
catalog=Northwind;integrated security=True;multipleactiveresultsets=True;
App=EntityFramework"" providerName="System.Data.EntityClient" />
</connectionStrings>
通常,WCF数据服务与Entity Framework中的ObjectContext
可以顺利工作。现在我们在Entity Framework中使用DbContext。到目前为止,WCF数据服务不能直接与DbContext
一起工作。但是,经过一些额外的修改,它也能很好地工作。下面是基于此处的修改后的NorthwindDataService.svc:
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class NorthwindDataService : DataService<ObjectContext>
{
// This method is called only once to initialize service-wide policies.
public static void InitializeService(DataServiceConfiguration config)
{
// Allow full access rights on all entity sets
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
// return more information in the error response message for troubleshooting
config.UseVerboseErrors = true;
}
protected override ObjectContext CreateDataSource()
{
var ctx = new NorthwindEntities();
var oContext = ctx.ObjectContext;
oContext.ContextOptions.ProxyCreationEnabled = false;
return oContext;
}
}
上述修改中棘手的部分是使用DbContext.ObjectContext
,因为DbContext
是建立在ObjectContext
之上的。此外,DbContext.ObjectContext.ContextOptions.ProxyCreationEnabled
被关闭,因为DbContext
的实体类是POCO;POCO需要关闭代理创建才能使WCF数据服务正常工作。
5) 项目 GH.Northwind.Business.Entities、GH.Northwind.Business.Interfaces 和 GH.Northwind.Business:我们将实体和接口放在它们各自的项目中,以便它们可以单独分发,而无需将业务实现分发给业务服务用户。如上一节所述,项目GH.Northwind.Business.Entities中的实体类是通过支持WCF的DbContext生成器自动生成的;实体元数据也是通过稍后将描述的代码生成器自动生成的。GH.Northwind.Business.Interfaces中的接口是面向业务的,而不是面向数据的。但是,这里的接口可以包括一些业务实体的CRUD操作,如果这些CRUD操作是面向业务的。除了某些CRUD操作外,业务接口还需要包含客户端所需的所有业务操作。对于Products、Customers、Orders、Order_Details、Categories和Suppliers表,我们可以预测它们将需要CRUD操作,但Orders和Order_Details的CRUD操作需要结合在一起。
创建实体的CRUD接口是很繁琐的,特别是当实体数量很多时。应该有现有的代码生成器可以帮助我们。除了使用第三方代码生成器外,在这里我们将利用.NET框架中的Domain Service类功能来帮助我们。首先在Project GH.Northwind.EntityFramework中创建一个Domain Service类,基于现有的Entity Framework模型。确保在添加Domain Service类之前先构建GH.Northwind.EntityFramework项目,否则您将无法在Domain Service类向导中看到模型类。有关如何通过向导创建Domain Service类的详细信息,请参阅此处。向导完成后,将生成一个文件:DomainService1.cs。接下来,我们需要通过Visaul Studio中的自动方式从该文件中提取有用的信息。a) 打开文件DomainService1.cs,将鼠标焦点放在类名DomainService1上,然后右键单击,选择“重构”->“提取接口”,然后勾选“放在另一个文件中”,然后选择要提取的函数。这里我们选择Customers、Products、Orders、Categories和Suppliers表的CRUD操作,但不选择Order_Detail表。现在我们有了一个接口文件:IDomainService1.cs。将此接口重命名为INorthwindSvr
,同时通过添加[ServiceContract]
和[OperationContract]
进行修改,更新其命名空间并将其移动到项目GH.Northwind.Business.Interfaces,这将由客户端和业务层使用。
最后,确保从项目GH.Northwind.EntityFramework中删除文件DomainService1.cs和IDomainService1.cs。然后,我们需要进一步修改和扩展项目GH.Northwind.Business.Interfaces中的接口INorthwindSvr以满足此演示的业务需求;其更新版本如下:
[ServiceContract]
public interface INorthwindSvr
{
[OperationContract]
List<Customer> GetCustomers();
[OperationContract]
void InsertCustomer(Customer customer, bool commit);
[OperationContract]
void UpdateCustomer(Customer currentCustomer, bool commit);
[OperationContract]
void DeleteCustomer(String customerId, bool commit);
[OperationContract]
List<Order> GetOrders();
[OperationContract]
List<Order_Detail> GetOrderDetailForAnOrder(int orderId);
[OperationContract]
List<Order> GetOrderForACustomer(String customerId);
[OperationContract]
void CreateOrder(Order order, Order_Detail[] details);
[OperationContract]
void UpdateOrder(Order currentOrder, Order_Detail[] details, bool commit);
[OperationContract]
void DeleteOrder(int orderId, bool commit);
[OperationContract]
void DeleteAnOrderDetailFromAnOrder(int orderId, int orderDetailId, bool commit);
[OperationContract]
List<Product> GetProducts();
[OperationContract]
Product GetProductById(int id);
[OperationContract]
void InsertProduct(Product product, bool commit);
[OperationContract]
void UpdateProduct(Product currentProduct, bool commit);
[OperationContract]
void DeleteProduct(int productId, bool commit);
[OperationContract]
List<Category> GetProductCategories();
[OperationContract]
List<Supplier> GetSuppliers();
[OperationContract]
void Commit();
}
在项目GH.Northwind.Business中,由于库项目GH.Northwind.Persistence的存在,实现类NorthwindSvr非常简单。此外,还有一个快速的方法可以根据接口创建函数存根:在Visual Studio 2010编辑器中打开GH.Northwind.Business项目中的NorthwindSvr.cs文件,右键单击文件NorthwindSvr.cs中的INorthwindSvr,然后单击“实现接口”->“实现接口”,接口函数存根会自动在NorthwindSvr.cs文件中创建。借助GH.Common.Framework中的静态类PersistSvr,我们可以快速实现这些函数存根。
我们需要初始化NorthwindSvr的持久化提供程序。我们将初始化放在类NorthwindSvr的静态构造函数中,以便在首次访问业务类NorthwindSvr时调用它。我们还使用配置参数“UseWcfDataService”来指定是否使用WCF数据服务。如果不使用,则直接使用ObjectContext
或DbContext
。以下是初始化代码:
static NorthwindSvr() {
string useWcfDataService = System.Configuration.ConfigurationManager.AppSettings["UseWcfDataService"];
if (useWcfDataService.ToLower() == "true")
{
string dataServiceUrl = ConfigurationSettings.AppSettings["WcfDataServiceUrl"];
DataServiceCxtFrameWkNamespace.DataCxt.Cxt = new DataServiceContext(new Uri(dataServiceUrl));
ServiceLocator<IPersistence<Customer>>.RegisterService<DataServiceCxtNamespace.CustomerPrst>();
ServiceLocator<IPersistence<Product>>.RegisterService<DataServiceCxtNamespace.ProductPrst>();
ServiceLocator<IPersistence<Order>>.RegisterService<DataServiceCxtNamespace.OrderPrst>();
ServiceLocator<IPersistence<Order_Detail>>.RegisterService<DataServiceCxtNamespace.Order_DetailPrst>();
}
else {
if (typeof(NorthwindEntities).IsSubclassOf(typeof(ObjectContext))) {
// Below are commented out since now NorthwindEntities is DbContext;
// Uncomment them out if NorthwindEntities is ObjectContext;
/*ObjectCxtFrameWkNamespace.DataCxt.Cxt = new NorthwindEntities();
ServiceLocator<IPersistence<Customer>>.RegisterService<ObjectCxtNamespace.CustomerPrst>();
ServiceLocator<IPersistence<Product>>.RegisterService<ObjectCxtNamespace.ProductPrst>();
ServiceLocator<IPersistence<Order>>.RegisterService<ObjectCxtNamespace.OrderPrst>();
ServiceLocator<IPersistence<Order_Detail>>.RegisterService<ObjectCxtNamespace.Order_DetailPrst>();*/
}
else if (typeof(NorthwindEntities).IsSubclassOf(typeof(DbContext)))
{
DbCxtFrameWkNamespace.DataCxt.Cxt = new NorthwindEntities();
ServiceLocator<IPersistence<Customer>>.RegisterService<DbCxtNamespace.CustomerPrst>();
ServiceLocator<IPersistence<Product>>.RegisterService<DbCxtNamespace.ProductPrst>();
ServiceLocator<IPersistence<Order>>.RegisterService<DbCxtNamespace.OrderPrst>();
ServiceLocator<IPersistence<Order_Detail>>.RegisterService<DbCxtNamespace.Order_DetailPrst>();
}
else {
throw new NotSupportedException("NorthwindSvr: static Constructor: " +
typeof(NorthwindEntities).ToString() + " isn't a supported type.");
}
}
}
元数据类和接口IValidatableObject
是进行客户端表示层数据验证的好方法,适用于ASP.NET MVC和WPF。有许多方法和代码生成器可以自动生成这些元数据文件。我们使用这里的代码生成器来自动生成它们。之后更新命名空间;将标签MaxLength
替换为StringLength
,因为.NET 4的System.ComponentModel.Annotation
中不包含MaxLength
。此外,对于每个部分实体类,我们手动将超类BusinessEntityBase
添加到GH.Common项目中的Framework中。然后,我们将所有元数据文件复制到项目GH.Northwind.Business.Entities中的MetaAndPartial子文件夹中。超类BusinessEntityBase
实现了接口IValidatableObject
。通常,对于单属性验证,注释标签可以轻松处理。如果存在跨多个实体属性的验证规则,那么我们可以覆盖实体子类中的此接口。在ASP.NET MVC3和Entity Framework Code First DbContext
中,可以将元数据类验证标签和接口IValidatableObject配置为在运行时自动触发验证。以下是CustomerMeta类的示例:
在此文件中,我们重写了接口IValidableObject
的Validate函数,用于涉及Address和Phone的验证。
[MetadataType(typeof(CustomerMetadata))]
public partial class Customer : BusinessEntityBase
{
override public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Address == String.Empty && Phone == String.Empty)
{
yield return new ValidationResult("Address and phone cannot be empty in the same time.",
new[] { "Address", "Phone" });
}
else
{
yield break;
}
}
}
public partial class CustomerMetadata
{
[DisplayName("Customer")]
[Required]
[StringLength(5)]
public string CustomerID { get; set; }
[DisplayName("Company Name")]
[Required]
[StringLength(40)]
public string CompanyName { get; set; }
[DisplayName("Contact Name")]
[StringLength(30)]
public string ContactName { get; set; }
[DisplayName("Contact Title")]
[StringLength(30)]
public string ContactTitle { get; set; }
[DisplayName("Address")]
[StringLength(60)]
public string Address { get; set; }
[DisplayName("Phone")]
[StringLength(24)]
public string Phone { get; set; }
// More are omitted here
}
对于此WCF业务服务层的错误处理,我们在GH.Northwind.Business项目中实现了IErrorHandler
行为,以捕获所有异常并将它们转换为FaultException
,以便在发生异常后服务通道不会被破坏,并且可以再次使用。此外,FaultException
可以发送到客户端以帮助调试。
6) 项目 GH.Northwind.Business.Host:这是一个WCF服务应用程序,仅包含部署配置;其所有操作来自项目GH.Northwind.Business。目前,我们使用BasicHttpBinding
主机来托管此WCF服务。我们可以轻松地将其更改为其他类型的托管,例如Windows服务。或者,我们可以创建一个新的Windows服务主机WCF项目来访问GH.Northwind.Business。此外,我们可以在现有主机项目中添加Windows服务主机支持,以实现双主机支持项目。
7) \GH.Northwind\Clients 文件夹中的项目 GH.Northwind.Client.Common:客户端表示层的一部分,包含所有类型客户端所需的通用组件。我们应该尽可能多地将所有客户端的通用代码放在这个库中,以最大化所有类型客户端的代码可重用性。到目前为止,由于本文主要关注业务层和持久层,我们没有在这个项目中包含太多内容。布尔配置参数“N-Tier”用于指定客户端使用三层还是两层。如果N-Tier为true,则客户端将通过WCF应用程序项目GH.Northwind.Business.Host访问业务WCF服务;否则,它将直接访问GH.Northwind.Business。
实现客户端表示层的更好方法是使用命令模式,以获得更好的封装性和所有类型客户端的可移植性。我们可能会在未来探索这一点。在这里,为了简单起见,我们仅使用类NorthwindHandler
来公开业务接口INorthwindSvr
给客户端。我们使用非代理方式访问WCF业务服务。以下是类NorthwindHandler
的简单内容:
public static INorthwindSvr GetNorthwindService()
{
if (ConfigurationSettings.AppSettings["N-Tier"].ToLower() == "true")
{
string northwindBusinessUrl = ConfigurationSettings.AppSettings["NorthwindSvrBusinessUrl"];
BasicHttpBinding binding = new BasicHttpBinding();
binding.MaxReceivedMessageSize = 1000000;
binding.ReaderQuotas.MaxDepth = 200;
ChannelFactory<INorthwindSvr> channelFactory = new ChannelFactory<INorthwindSvr>(
binding,
new EndpointAddress(new Uri(northwindBusinessUrl)));
INorthwindSvr northwindSvr = channelFactory.CreateChannel();
return northwindSvr;
}
else
{
return new NorthwindSvr();
}
}
8) \GH.Northwind\Clients 文件夹中的项目 GH.Northwind.Web:是一种客户端:一个ASP.NET MVC3应用程序,用于演示目的。该项目属于客户端表示层,它将生成HTML网页作为Web客户端。目前,我们仅在此ASP.NET MVC3客户端中实现了Product实体的CRUD操作。 请参见Global.asax中的Start函数Start,如下所示:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
NorthwindController.NorthwindSvr = NorthwindHandler.GetNorthwindService();
System.Web.Mvc.ModelBinders.Binders.Add(typeof(CustomerWithOrderModel), new CustomerWithOrderBinder());
System.Web.Mvc.ModelBinders.Binders.Add(typeof(AllProductsModel), new AllProductsBinder());
System.Web.Mvc.ModelBinders.Binders.Add(typeof(SuppliersCategoriesModel), new SuppliersCategoriesBinder());
}
在代码中,我们将一个INorthwindSvr
实例注入到Northwind
控制器类中。如果我们想对客户端进行单元测试,我们可以注入INorthwindSvr
的一个测试实现。我们还添加了三个自定义模型绑定器CustomerWithOrderBinder
、AllProductsBinder
和SuppliersCategoriesBinder
,以便轻松地从HTTP会话中存储和检索其匹配模型的实例。以下是控制器类NorthwindController
:
public class NorthwindController : Controller
{
private readonly ILogger<NorthwindController> Log = Log<NorthwindController>.LogProvider;
private static INorthwindSvr _northwindSvr;
public static INorthwindSvr NorthwindSvr
{
get
{
if (_northwindSvr == null)
throw new ApplicationException("NorthwindController.NorthwindSvr: NorthwindSvr is null.");
return _northwindSvr;
}
set { _northwindSvr = value; }
}
public ActionResult Index()
{
return View();
}
public ActionResult AllProducts(AllProductsModel allProductsModel)
{
try
{
allProductsModel.Products = NorthwindSvr.GetProducts();
return View(allProductsModel.Products);
}
catch (FaultException<BusinessServiceException> e)
{
Log.Error("Web.NorthwindController.AllProducts(...) Error from Business service layer: " + e.Message);
throw;
}
catch (FaultException ex)
{
Log.Error("Web.NorthwindController.AllProducts(...) Error from Business service layer: " + ex.Message);
throw;
}
catch (Exception exx)
{
Log.Fatal("Web.NorthwindController.AllProducts(...) Error: " + exx.Message);
throw;
}
}
//
// GET: /NorthwindSvr/Details/5
public ActionResult DetailsProduct(int? id, AllProductsModel allProductsModel, SuppliersCategoriesModel model)
{
Product p = null;
if (id != null) p = allProductsModel.ProductById(id.Value);
else
{
p = allProductsModel.ProductById((int)TempData["id"]);
}
if (p.Supplier == null) p.Supplier = model.SupplierList.Where(s => s.SupplierID == p.SupplierID).DefaultIfEmpty(null).First();
if (p.Category == null) p.Category = model.CategoryList.Where(c => c.CategoryID == p.CategoryID).DefaultIfEmpty(null).First();
return View(p);
}
//
// GET: /NorthwindSvr/Create
public ActionResult CreateProduct(SuppliersCategoriesModel model)
{
ViewBag.SuppliersCategoriesModel = model;
return View(new Product());
}
//
// POST: /NorthwindSvr/Create
[HttpPost]
public ActionResult CreateProduct(Product p, AllProductsModel allProductsModel)
{
NorthwindSvr.InsertProduct(p, true);
TempData["id"] = p.ProductID;
allProductsModel.Products.Insert(allProductsModel.Products.Count, p);
return RedirectToAction("DetailsProduct");
}
//
// GET: /NorthwindSvr/Edit/5
public ActionResult EditProduct(int id, AllProductsModel allProductsModel,
SuppliersCategoriesModel suppliersCategoriesModel)
{
Product p = allProductsModel.ProductById(id);
ViewBag.SuppliersCategoriesModel = suppliersCategoriesModel;
return View(p);
}
//
// POST: /NorthwindSvr/Edit/5
[HttpPost]
public ActionResult EditProduct(Product p, SuppliersCategoriesModel suppliersCategoriesModel)
{
NorthwindSvr.UpdateProduct(p, true);
ViewBag.SuppliersCategoriesModel = suppliersCategoriesModel;
return View(p);
}
//
// GET: /NorthwindSvr/Delete/5
public ActionResult DeleteProduct(int id)
{
Product p = NorthwindSvr.GetProductById(id);
return View(p);
}
//
// POST: /NorthwindSvr/Delete/5
[HttpPost]
public ActionResult DeleteProduct(int id, FormCollection collection)
{
NorthwindSvr.DeleteProduct(id, true);
return RedirectToAction("AllProducts");
}
}
以下是Product CRUD操作的一些网页截图:
图3:列出所有产品。
图4:创建产品。
图5:编辑产品。
图6:产品详情。
图7:删除产品。
讨论
图1所示多层架构的可能配置结果和分析
对于图1所示多层架构的配置结果,通过更新应用程序配置文件中的两个参数“N-Tier”和“UseWcfDataService”,我们可以得出以下可能的组合。(在下表中,不在括号中的组件由其正上方的层直接调用;括号中的组件将被同一层中没有括号的组件直接使用):
案例编号。 | 客户端 | 客户端表示层 | 业务层 | 持久层 | 数据库 | 层 |
1 | HTML 网页 | ASP .NET (通用库) | WCF 主机 (WCF 库) | 持久化库 (Entity Framework 库) | SQL Server | 4层,完整的N层 |
2 | HTML 网页 | ASP .NET (通用库) | WCF 主机 (WCF 库) | 持久化库 (WCF 数据服务, Entity Framework) | SQL Server | 5层,完整的N层 |
3 | HTMP网页 | ASP .NET (通用库) | WCF 库 | 持久化库 (Entity Framework 库) | SQL Server | 3层,不完整的3层 |
4 | HTMP网页 | ASP .NET (通用库) | WCF 库 | 持久化库 (WCF 数据服务, Entity Framework 库) | SQL Server | 4层,不完整的N层 |
5 | WPF | WPF客户端库 (通用库) | WCF 主机 (WCF 库) | 持久化库 (Entity Framework 库) | SQL Server | 3层,完整的3层 |
6 | WPF | WPF客户端库 (通用库) | WCF 主机 (WCF 库) | 持久化库 (WCF 数据服务,Entity Framework 库) | SQL Server | 4层,完整的N层 |
7 | WPF | WPF客户端库 (通用库) | WCF 库 | 持久化库 (WCF 数据服务, Entity Framework 库) | SQL Server | 3层,不完整的3层 |
8 | WPF | WPF客户端库 (通用库) | WCF 库 | 持久化库 (Entity Framework 库) | SQL Server | 2层,非N层 |
表1:示例多层架构的可能配置组合
在以上所有情况中,除了使用WCF数据服务的情况外,在大多数情况下,业务层和持久层在同一个进程中运行。如果使用WCF数据服务作为持久层的一部分,它将与Entity Framework在单独的层中运行,但持久化库仍将在与业务层相同的进程中运行。由此可见,层和层可能不完全匹配。
我们需要在表1中提到另一种非常常见的特殊情况:对于情况3、4、7,WCF库在业务层中被客户端表示层直接调用,因此客户端表示层和业务层只能运行在同一个进程中。对于这些情况,我们仍然可以得到3或4层的结果,因为整个应用程序可以在3或4台计算机上运行。我们认为这些是不完整的N层架构,因为客户端表示层和业务层不能运行在独立的机器(层)上;完整的N层架构应该能够使客户端表示层和业务层运行在独立的机器(层)上。完整的N层架构在处理可伸缩性、安全性和容错性问题方面具有最佳能力。这种不完整的3层架构的主要缺点是它失去了独立部署的业务层的全部优点。这些优点包括对业务层进行不同的安全强制执行、业务层的独立可伸缩性以及所有客户端的中央业务层等。然而,如果一个独立的业务层提到的所有优点在某些情况下都不重要,那么我们可以采用这种不完整的N层架构来提高性能并节省成本。这种不完整的N层架构确实经常发生。作为软件工程师,我们可能都经历过。本文中的示例多层架构可以通过仅更新配置文件轻松地在不完整的N层架构和完整的N层架构之间切换。
从上表1可以看出,我们的N层架构具有高度的配置灵活性;借助我们良好的层设计和实现,可以轻松实现任何类型的层架构。
示例多层架构除了通用优点之外的其他优点
除了我们在上一篇文章中提到的多层架构的通用优点之外,我们的多层架构示例还具有以下额外优点:
- 每个层都可进行单元测试:我们通过使用服务定位器和依赖注入来实现这一点。层之间的依赖关系基于接口,而不是具体类,因此我们可以轻松地插入接口的测试实现类来进行测试。
- 部署灵活:我们将WCF实现放入一个库中,并将WCF主机放入一个单独的WCF应用程序项目中来实现这一点。在我们的示例代码中,配置文件中只需要两个布尔参数来控制我们是否要将其部署为N层或2层架构;我们是否要直接使用Entity Framework,还是使用Entity Framework之上的WCF数据服务。
- 开发高效:所有实体都通过支持WCF标签的代码生成器自动生成;元数据类和注释标签也自动生成。我们甚至通过.NET 4中的Domain Service功能自动生成了业务端CRUD操作接口的草稿版本。此外,对于持久层,主要的实现位于项目GH.Common的Framework/Persistence文件夹中,这使得项目GH.Northwind.Persistence中的持久化实现更加简单和容易。
- 各层具有良好的解耦和封装。我们使用持久层来适配业务层与EntityFramework,以便这两层尽可能解耦。通过这种方式,业务层可进行单元测试。
- 验证逻辑可维护且灵活。我们将所有验证逻辑放在一个地方:项目GH.Northwind.Business.Entities,这样我们只需要实现和维护一个版本的验证逻辑。所有层共享一个版本的验证逻辑。我们可以根据实际需求轻松地在许多层中调用这些验证。目前,我们在ASP.NET MVC3客户端表示层中调用这些验证。
- 通过避免重复代码,实现更易维护、可重用、清晰且无冲突的代码。首先,我们避免使用服务引用代理来访问WCF服务。服务引用代理很容易复制相同的代码片段。相反,我们使用非代理“ChannelFactory”来访问WCF服务和WCF数据服务,通过重用共享公共库中的服务接口和数据契约。其次,我们将自动生成的实体类及其元数据类放入一个单独的项目GH.Northwind.Business.Entities中,然后在整个应用程序中维护和使用它们。
WCF实现与WCF主机的分离
在业务层,我们将实现库与部署主机分离;在持久层。我们将Entity Framework库与WCF数据服务主机分离。通过这样做,我们实现了部署的灵活性。例如,我们可以轻松地将主机项目更改为另一种部署类型,而无需更改库项目。此外,我们可以在没有独立主机的情况下直接使用该库。
库项目 GH.Northwind.Persistence 的目的
业务层不直接调用Entity Framework或WCF数据服务,而是通过库GH.Northwind.Persistence调用。为什么?这样做有以下优点:a) 实现业务层与Entity Framework或WCF数据服务之间更好的解耦,因此可以轻松地交换不同类型的持久化技术,而无需更改业务服务层;b) 使业务层可进行单元测试:业务层仅依赖于持久化接口,因此,我们可以通过注入持久化层的可测试版本,轻松地对业务层进行单元测试,而无需实际连接到数据库;我们应该尽量避免在单元测试中使用数据库,因为数据库对于单元测试来说速度很慢。单元测试应该快速。c) 促进和简化了业务层中持久化技术的使用。通过检查项目GH.Northwind.Business和项目GH.Northwind.Persistence中的代码,您可以轻松地看到这一点。
为什么不使用Domain Service类功能作为业务层?
我们利用Domain Service类功能来自动生成业务接口。但是,为什么不直接使用Domain Service类作为业务层呢?直接使用Domain Service类有几个缺点:a) 混乱且难以使用,因为它不是人们熟悉的常规WCF。自动生成的Domain Service类是大类,充满了许多函数和属性,且不友好。b) Domain Service类创建了两个紧密耦合:业务层与持久层(Entity Framework)的紧密耦合,客户端层与业务层之间的紧密耦合。这些紧密耦合带来了许多单元测试、升级、维护等方面的问题。Domain Service类混合了业务逻辑和持久化操作;客户端直接使用Domain Service类。业务层不通过接口依赖于持久化操作;客户端层不通过接口依赖于业务层。因此,您将无法单独对客户端层进行单元测试,而无需业务层参与;您将无法单独对业务层进行单元测试,而无需持久层参与。此外,由于这些耦合,更新一个层将对其他层带来更大的副作用。
为什么客户端不直接调用WCF Data Service,而是通过业务层调用?
持久层中的WCF数据服务用作可选的持久化选择。客户端不直接调用此WCF数据服务,而是通过业务层接口INorthwindSvr
调用。为什么客户端不直接调用WCF数据服务?首先,WCF数据服务主要面向数据,用于CRUD持久化操作,而客户端需要一个业务操作提供程序;INorthwindSvr
是面向业务的。其次,在INorthwindSvr
中,我们可以轻松地修改现有的CRUD函数,添加验证、业务逻辑、额外参数等,以实现业务目的;我们还可以添加额外的业务操作函数。总之,我们可以在INorthwindSvr
中做任何我们想做的事情;我们对INorthwindSvr
拥有完全的控制权。然而,所有这些上面提到的在WCF数据服务中都是有限的。例如,在INorthwindSvr
中,我们可以删除函数insertOrderDetail
,但添加一个更面向业务的函数:AddProductToOrder(Product p, Order o)
。纯粹的WCF数据服务无法简单地实现这一点。总之,为了获得多层架构客户端的全面且可控的业务操作,我们需要一个独立的业务层供客户端使用,而不是让客户端直接调用纯粹的数据中心化的WCF数据服务。
业务实体类和代码生成器
整个应用程序中只维护和使用一个版本的业务实体poco类,位于项目GH.Northwind.Business.Entities中,为什么?这样做使我们能够实现代码一致性、无冲突、最小的维护工作量,因为只有一个代码版本没有重复。我们真正要避免的是每个层使用自己的实体类版本,这会引发许多问题。
代码生成器用于尽可能多地生成我们的代码。对于少量的业务实体和操作,手动编写代码而无需代码生成器是可以的。但是,当业务实体和操作的数量变大时,代码生成器将以一致且无错误的代码为我们节省大量工作。手动编码往往更不一致且容易出错。如果我们为不同层的不同目的需要不同版本的自动生成类,我们可以使用C#中的部分类功能和System.ComponentModel.DataAnnotations
命名空间中的元数据功能来扩展和修饰这些自动生成的类。例如,对于自动生成的业务实体类,在ASP.NET MVC客户端中,我们可以使用元数据类功能来验证用户输入。我们还可以使用部分类功能将新属性、新继承、新函数等添加到我们现有的实体类中。由于它们与原始自动生成的实体类存储在不同的文件中,因此如果原始实体类被重新生成,元数据类和部分类将保持不变。
数据验证
对于数据验证,我们使用元数据类和接口IValidatableObject
。对于单个属性的简单验证,可以使用带有System.ComponentModel.DataAnnotations
中标签的元数据类。对于跨多个属性的类级别验证,可以使用接口IValidatableObject
。通过部分类功能,我们让所有自动生成的实体类都继承自GH.Common.Framework.Business
中的类BusinessEntityBase
;类BusinessEntityBase
实现了接口IValidatableObject
,其中有一个虚拟函数Validate。具有复杂验证规则的任何实体类都可以覆盖此虚拟函数。
哪个层应该触发验证逻辑?对于带有元数据类的简单验证,它可以根据情况在客户端层、客户端表示层、业务层或Entity Framework中进行检查。对于带有接口IValidatableObject
的类级别验证,它可以很少在客户端层中进行检查,但可以在客户端表示层、业务层或Entity Framework中进行检查。客户端验证的性能效率更高;服务器端验证更可靠。我们可以在多个层中进行验证,以同时实现效率和可靠性。例如,在ASP.NET MVC3中,可以通过自动生成的JavaScript jQuery验证控件在客户端网页中检查元数据类验证。然而,网页中的客户端验证很容易被故意绕过,因此我们需要在客户端表示层、业务层或Entity Framework中进行相同的服务器端验证以确保可靠性。无论在哪里检查验证,我们只在我们的示例多层应用程序的项目GH.Northwind.Business.Entities中实现和维护一个版本的验证逻辑;所有层共享一个版本的验证逻辑。ASP.NET MVC3支持使用元数据类和接口IValidatableObject
进行自动验证;Entity Framework Code First DbContext
也通过Fluent API支持这些验证,方法是将DbContext.Configuration.ValidateOnSaveEnabled
设置为true;对于带WPF MVVM模式的客户端,这些注释属性验证也可用于验证用户输入数据,有关详细信息,请参阅文章Attributes-based Validation in a WPF MVVM Application。目前在我们的示例项目中,ASP.NET MVC3将使用元数据类和接口IValidatableObject
进行验证检查。
示例多层架构应用程序的一些配置结果
上表中列出了项目配置文件中一些配置参数组合的结果
注意:GH.Northwind.Web中配置参数“N-Tier”为True表示客户端通过WCF应用程序项目GH.Northwind.Business.Host访问业务层;否则,客户端直接访问GH.Northwind.Business库。配置参数“UseWcfDataService”为True表示GH.Northwind.Persistence库通过WCF数据服务项目GH.Northwind.EntityFramework.Host访问Entity Framework;否则,GH.Northwind.Persistence库直接访问GH.Northwind.EntityFramework库。此外,只有可执行项目中的配置文件才有效;库中的配置文件无效。因此,请确保更新可执行项目的配置文件。
参数“N-Tier” 在GH.Northwind.Web中 | GH.Northwind.Web中参数“UseWcfDataService” | GH.Northwind.Business.Host中参数“UseWcfDataService” | 应用程序组件流 |
True(将调用GH.Northwind.Business.Host) | 不适用(现在,GH.Northwind.Business.Host项目中的UseWcfDataService参数将生效) | True | 网页 ⇔ GH.Northwind.Web ⇔ GH.Northwind.Client.Common ⇔ GH.Northwind.Business.Host ⇔ GH.Northwind.Business GH.Northwind.EntityFramework.Host ⇔ GH.Northwind.EntityFramework (完整N层:5层) |
True (将调用GH.Northwind.Business.Host) | 不适用(现在,GH.Northwind.Business.Host项目中的UseWcfDataService参数将生效) | 假 | 网页 ⇔ GH.Northwind.Web ⇔ GH.Northwind.Client.Common ⇔ GH.Northwind.Business.Host ⇔ GH.Northwind.Business GH.Northwind.EntityFramework (完整N层:4层) |
False(将不调用GH.Northwind.Business.Host;但将直接调用库GH.Northwind.Business) | True | 不适用(现在,GH.Northwind.Web项目中的UseWcfDataService参数将生效,因为将不调用GH.Northwind.Business.Host) | 网页 ⇔ GH.Northwind.Web ⇔ GH.Northwind.Client.Common ⇔ GH.Northwind.Business ⇔ GH.Northwind.EntityFramework.Host ⇔ GH.Northwind.EntityFramework (不完整N层:4层) |
False(将不调用GH.Northwind.Business.Host;但将直接调用库GH.Northwind.Business) | 假 | 不适用(现在,GH.Northwind.Web项目中的UseWcfDataService参数将生效,因为将不调用GH.Northwind.Business.Host) | 网页 ⇔ GH.Northwind.Web ⇔ GH.Northwind.Client.Common ⇔ GH.Northwind.Business ⇔ GH.Northwind.EntityFramework (不完整N层:3层) |
表2:我们示例多层架构的一些配置组合 Application.
示例多层架构部署灵活性的应用
我们示例项目中不同层架构的部署灵活性可以有以下可能的用途:
- 有时可以加快开发进程。例如,在开发阶段,我们可以主要使用上表中最后一行所示的不完整3层配置来加快开发速度,如果一个团队处理所有层;开发速度加快是因为在测试两层配置中的应用程序时,我们不需要每次都启动服务。在大多数情况下,如果库工作正常,WCF主机项目也应该工作正常。但并非总是如此,因为WCF服务或数据服务有其自身的限制和规则,这会对库接口提出要求。因此,偶尔我们也需要通过WCF主机服务在完整N层配置下测试我们的应用程序。
- 在初始开发阶段,如果我们不确定最终产品将如何部署,或者如果部署需求会随着项目增长或用户数量的增加而变化,那么我们可以使用我们示例项目中的灵活部署策略,只需更新配置文件即可应对未来任何可能的部署需求。例如,最初,我们可能选择不使用WCF数据服务。然而,随着应用程序用户越来越多,我们觉得将数据访问工作量分担到单独的计算机上非常有必要,或者我们有其他类型的应用程序需要共享一个中央数据CRUD操作服务,那么WCF数据服务将是一个可选解决方案。通过我们的方式,我们只需要将配置参数“UseWcfDataService”设置为true,然后将此WCF数据服务托管在另一台计算机上。
- 如果我们要为不同场景或不同客户在不同层架构中部署相同的应用程序,部署灵活性将非常有帮助。例如,对于同一个应用程序,一个客户想要一个完整的N层架构用于Internet使用,而另一个客户只希望为Intranet使用一个不完整的3层架构。
结论
- 本文介绍了一种高度解耦、单元测试友好、部署灵活、实现高效且验证灵活的.NET多层架构。我们在这里实现的是将.NET中一些知名且优秀的工具和功能结合起来,并提出一个可行的解决方案。
- 我们在示例多层应用程序中实现了以下主要内容:
- 一个层仅通过接口依赖于另一个层,而不是具体的类。
- 通过简单更新配置文件中的两个参数值,可以轻松切换不同的层架构。我们通过将WCF实现与WCF主机分离到不同的项目中,以及客户端以非代理方式访问WCF服务和WCF数据服务来实现这一点。
- 为了在整个应用程序中使用它,我们在一个轻量级库项目 GH.Northwind.Business.Entities 中自动生成并维护实体类的一个版本,而不是使用原始的 Entity Framework 项目。这样,我们就可以在需要的地方重新生成和重用这些自动生成的实体类,而不是在一个充斥着重量级 Entity Framework 东西的地方。
- 业务层不直接调用 Entity Framework,而是通过持久化库。这样做有助于我们在业务层和持久化层之间实现最大的解耦,并允许我们轻松地更换持久化技术,而不会对业务层产生任何副作用。此外,它还促进和简化了 Entity Framework 在业务层的用法。示例应用程序表明,通过更新配置文件中的参数值,我们可以轻松地在 WCF 数据服务和 DbContext(或:ObjectContext)之间切换持久化技术。
- 在我们的示例应用程序中,对于数据验证,我们使用自动生成的元数据类来对单个属性进行简单验证,并使用接口
IValidatableObject
进行跨越多个属性的类级别验证。目前,我们将所有验证逻辑集中在一个地方:GH.Northwind.Business.Entities 项目,这样我们只需要实现和维护一个版本的验证逻辑。如果出于安全原因,在某些情况下,我们可以将所有验证逻辑移至其自己的库项目中。所有层都共享此版本的验证逻辑。根据我们的实际需求,我们可以在许多层中轻松调用这些验证。目前,我们在 HTML 网页客户端层和 ASP .NET MVC3 客户端表示层调用这些验证。 - 对于此 WCF 业务服务层的错误处理,我们实现了
IErrorHandler
行为来捕获所有异常,并将它们转换为 FaultException,这样服务通道就不会因为异常而中断,并在异常发生后可以再次使用。此外,FaultException
也可以发送到客户端以帮助调试。 - 尽量实现代码的自动生成:所有带有 WCF 标记的实体类和带有注释标记的元数据类都是由代码生成器自动生成的。我们甚至使用域服务类向导自动生成了业务接口的草稿版本。
- 我们在客户端使用非代理方式访问 WCF 服务和 WCF 数据服务。这样做可以让我们使用共享通用库中的数据契约(主要是自动生成的实体类)和服务接口,从而避免代码重复并无需更新代理方式的服务引用。
- 除了 N 层体系结构在我们上一篇文章中的普遍优势外,我们的 N 层体系结构示例还具有以下附加优势:
- 各层可进行单元测试:我们通过使用服务定位器和依赖注入来实现这一点。各层仅通过通用接口而非具体类来依赖另一层。因此,在单元测试中,我们可以注入接口的有效测试实现来进行测试。
- 部署灵活:我们通过将服务库项目与服务宿主项目分离来实现这一点。
- 实现效率。所有带有 WCF 标记的实体和带有注释标记的元数据类都是由代码生成器自动生成的。我们甚至使用域服务类自动生成了业务接口的草稿版本。
- 各层实现良好的解耦和封装。我们使用持久化层来适配和解耦业务层与 EntityFramework。通过这种方式,业务层可以进行单元测试。
- 验证逻辑可维护且灵活。我们将所有验证逻辑集中在一个地方:GH.Northwind.Business.Entities 项目,这样我们只需要实现和维护一个版本的验证逻辑。所有层共享一个版本的验证逻辑。根据我们的实际需求,我们可以在许多层中轻松调用这些验证。目前,我们在 ASP .NET MVC3 客户端表示层调用这些验证。
- 通过避免代码重复,实现更易于维护、可重用且更清晰的代码。首先,我们避免使用服务代理来访问 WCF 服务;相反,我们使用非代理方式访问 WCF 服务以最小化代码重复。其次,我们将自动生成的实体类及其元数据验证类放入一个单独的轻量级项目中,并在整个应用程序中维护和使用它们。
- 如果客户端表示层和业务层无法在单独的机器(层)上运行,但层数仍然多于 2。我们将此架构归类为不完整的 N 层架构;一个完整的 N 层架构应该能够让客户端表示层和业务层在单独的机器(层)上运行。不完整 N 层架构的主要缺点是它失去了单独部署的业务层的全部优势。这些优势包括在业务层上强制执行不同的安全性、业务层的独立可伸缩性以及所有客户端的中央业务层等。
- GH.Common 项目下的 subfolder framework 包含所有驱动各层的顶层框架类;这些顶层框架可以用于许多应用程序,不仅仅是 GH.Northwind。因此,我们拥有一个框架精简但应用程序内容丰富的架构。
- WCF 数据服务纯粹面向 CRUD 数据操作;业务层是一个面向业务的代理层。为了在 N 层体系结构中为客户端提供全面可控的业务操作,我们需要一个独立的业务层供客户端使用,而不是让客户端直接调用纯数据中心的 WCF 数据服务。
- .NET 中的域服务类功能会产生两种紧密耦合:业务层与持久化层(Entity Framework)之间的紧密耦合,以及客户端层与业务层之间的紧密耦合。这些紧密耦合会给单元测试、升级、维护等带来许多问题。域服务类混合了业务逻辑和持久化操作;客户端直接使用域服务类。业务层不通过接口依赖于持久化操作;客户端层不通过接口依赖于业务层。因此,在 N 层体系结构中,域服务类功能并不是实现解耦的业务层的理想选择。
- 在我们的示例项目中,任何层体系结构的部署灵活性可以有以下可能的用法:
- 有时可以加快开发过程,因为我们可以使用 2 层配置进行开发,这样在测试应用程序时就不需要每次都启动服务。
- 在初始开发阶段,如果我们不确定最终产品的部署方式,或者部署需求会随着项目的增长或用户数量的变化而改变,那么可以使用我们示例项目的灵活部署策略,只需更新配置文件即可应对未来任何可能的部署需求。
- 如果我们希望在不同场景或面向不同客户的情况下,将同一应用程序部署到不同的层体系结构,那么部署灵活性将非常有帮助。