使用 ASP.NET MVC3 和 Entity Framework 4.1 开发 Web 应用程序
在本文中,我将提供一个使用 ASP.NET MVC3 和 Entity Framework4.1 开发的演示应用程序。
引言
ASP.NET MVC 和 Entity Framework 是 Microsoft 平台上的 Web 应用程序与数据库后端结合的良好选择。在本文中,我将提供一个使用 ASP.NET MVC3 和 Entity Framework 4.1 开发的演示应用程序。该演示还采用了以下设计原则、模式和最佳实践,以使其更具灵活性和可扩展性。
- 关注点分离
- POCO
- Code-First
- 注册模式
- 仓储模式
- 持久化无关
- 依赖注入
- 单元测试
为了打开和运行演示应用程序,您需要安装 SQL Server/SQL Server Express 和 Northwind 示例数据库,以及安装了 ASP.NET MVC3 和 Entity Framework 4.1 的 Visual Studio 2010。
演示应用程序架构
ASP.NET MVC 已经有非常好的分层定义。在我的演示中,我将 Model 层移至其自己的程序集中,以便在其他应用程序中重用。此外,Model 是一个丰富的 Model,它不仅包含数据和行为,还通过服务承担更多职责。应用程序架构如下所示:
Demo.Web
程序集包含 ASP.NET MVC 视图和控制器。Demo.Web.Tests
程序集包含控制器的测试用例。Demo.Models
程序集包含域模型、业务模型和数据接口。Demo.Data
程序集包含使用 Entity Framework 的数据访问逻辑。Demo.Base
程序集包含整个应用程序的实用工具。
设计原则、模式和最佳实践
我不会详细介绍我的演示应用程序的每一个细节。源代码展示了完整的图景。我将只关注这里使用的设计原则、模式和最佳实践。
关注点分离
ASP.NET MVC 已经很好地遵循了关注点分离原则,它将 UI 放入 View 层,将交互放入 Controller 层,并将域实体放入 Model 层。在这个演示应用程序中,我唯一的改进是将 Model 层移到一个单独的程序集中,并添加了一个 Data 层来包含使用 Entity Framework 的数据访问逻辑。

POCO
我使用 Visual Studio 2010 扩展管理器安装的 ADO.NET C# POCO Entity Generator,根据 Entity Data Model (edmx) 生成了我最初的 POCO 域实体 Customer 和 Order。生成的代码默认在 Data 层,我将其从 Data 层移到了 Model 层,并完全删除了代码生成器模板文件。域实体将被手动增强和修改,除非发生重大的表结构更改。
public partial class Customer
{
#region Primitive Properties
public virtual string CustomerID
{
get;
set;
}
[Required]
public virtual string CompanyName
{
get;
set;
}
public virtual string ContactName
{
get;
set;
}
public virtual string ContactTitle
{
get;
set;
}
public virtual string Address
{
get;
set;
}
public virtual string City
{
get;
set;
}
public virtual string Region
{
get;
set;
}
public virtual string PostalCode
{
get;
set;
}
public virtual string Country
{
get;
set;
}
public virtual string Phone
{
get;
set;
}
public virtual string Fax
{
get;
set;
}
#endregion
#region Navigation Properties
public virtual ICollection<Order> Orders
{
get
{
if (_orders == null)
{
var newCollection = new FixupCollection<Order>();
newCollection.CollectionChanged += FixupOrders;
_orders = newCollection;
}
return _orders;
}
set
{
if (!ReferenceEquals(_orders, value))
{
var previousValue = _orders as FixupCollection<Order>;
if (previousValue != null)
{
previousValue.CollectionChanged -=
FixupOrders;
}
_orders = value;
var newValue = value as FixupCollection<Order>;
if (newValue != null)
{
newValue.CollectionChanged += FixupOrders;
}
}
}
}
private ICollection<Order> _orders;
#endregion
#region Association Fixup
private void FixupOrders(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (Order item in e.NewItems)
{
item.Customer = this;
}
}
if (e.OldItems != null)
{
foreach (Order item in e.OldItems)
{
if (ReferenceEquals(item.Customer, this))
{
item.Customer = null;
}
}
}
}
#endregion
}
Code-First
Entity Framework 中的 Code-First 与 Model-First 和 Database-First 不同。坦率地说,我遵循 Code-First 的理念,但在项目初始设置时并没有严格按照 Code-First 的步骤。相反,我从数据库创建了 Entity Data Model,然后用它生成了原始的域实体代码,并将代码移到了 Model 层。因此,Code-First 方法只是为了让我能够完全隔离未来域实体代码的更改与数据库的更改。
注册模式
Model 层有一个特殊的 public static
对象 Registry
,它是一个广为人知的对象,让其他对象通过它查找常用对象和服务。这是《企业应用模式架构》(PoEAA) 书中注册模式的一种实现。在我的演示应用程序中,Registry
对象允许域实体和上层找到 Repository 服务——Context
和 RepositoryFactory
。
public static class Registry
{
public static IDependencyLocator DependencyLocator;
public static IContext Context
{
get
{
return DependencyLocator.LocateDependency<IContext>();
}
}
public static IRepositoryFactory RepositoryFactory
{
get
{
return DependencyLocator.LocateDependency
<IRepositoryFactory>();
}
}
}
仓储模式
在 Hibernate 应用程序中,有一种称为数据访问对象 (DAO) 模式的模式,Repository 模式是 Entity Framework 中名称不同的对应模式。我在 NerdDinner 中阅读了 Repository 模式的实现代码,它并没有足够好地将数据访问逻辑分离到自己的层中。在我的演示中,Repository 接口都驻留在 Model 层,以提供数据服务访问点,然后使用依赖注入将特定的 Data 层实现注入 Model 层。这种实现的目的是为了促进以 Model 为中心的概念,并允许切换 Data 层以实现不同的实现或进行单元测试。
持久化无关
在 Model 层定义数据访问接口并将 Data 层放入自己的程序集中,这鼓励了这里的持久化忽略。Model 层和上层不需要真正关心模型是如何被数据层加载、更新或保存的。此外,我可以根据需要将 Data 层切换到不同的 ORM 框架,如 NHibernate,或一个 Mock 实现。
依赖注入
依赖注入在我的演示中起着至关重要的作用。所有服务模块都需要与 Model 层进行延迟绑定,并通过依赖注入。此外,IoC 容器管理服务对象的生命周期。一个例子是 Context
对象。我在 Unity 配置中将生命周期类型设置为 PerThreadLifetimeManager
。这使得在单个请求中创建一个且仅创建一个 context 对象,而不同的请求则拥有不同的 context
对象。我想提到的另一件事是 ASP.NET MVC3 有自己的方式通过实现 DependencyResolver
接口来为控制器提供依赖注入。我尝试过,但最终没有采纳它,因为我的应用程序是以 Model 为中心,所以 Registry
是定位所有服务比控制器构造函数更好的地方。我在演示中使用的 IoC 容器是 Unity。MEF IoC 容器目前越来越受欢迎,但它通过使用属性对域实体具有更多的侵入性,并且不允许配置依赖项,这让我犹豫是否要使用它。
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
RegisterUnityContainer();
}
private void RegisterUnityContainer()
{
var container = new UnityContainer();
UnityConfigurationSection section =
(UnityConfigurationSection)ConfigurationManager.GetSection("unity");
section.Configure(container);
Registry.DependencyLocator = new UnityDependencyLocator(container);
}
单元测试
我通常使用测试项目进行自动集成测试,而不是纯单元测试。我这样做的原因是允许测试更接近用户验收测试,以避免考虑应用程序中每个类的所有可能测试场景。在此演示中,CustomerControllerTest
将有一个测试用例 Index
来测试 CustomerController Index
操作。还有一个项目 Demo.DataMock
,它是 Data 层的模拟。我可以使用模拟框架(例如 MOQ)来模拟测试用例中的 Data 层,但我决定不这样做,因为我不希望测试用例中有一个永久的 Data 层模拟,我想要在开发和构建过程中进行 Data 层模拟,另一方面,在签入我的更改之前,我也希望我的测试能够针对真实的数据层运行,以确保在“生产”环境中所有测试用例都能通过。我知道在真实数据库上运行很慢,但请记住,更贴近真实使用的测试可以帮助我发现更多可能的 bug,而运行真实数据库的次数在每次签入或发布到 QA 之前只是一次。
<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
<alias alias="IContext" type="Demo.Models.DataInterfaces.IContext, Demo.Models" />
<alias alias="Context" type="Demo.Data.Context, Demo.Data" />
<alias alias="IRepositoryFactory"
type="Demo.Models.DataInterfaces.IRepositoryFactory, Demo.Models" />
<alias alias="RepositoryFactory" type="Demo.Data.RepositoryFactory, Demo.Data" />
<container>
<register type="IContext" mapTo="Context">
<lifetime type="PerThreadLifetimeManager" />
</register>
<register type="IRepositoryFactory" mapTo="RepositoryFactory">
<constructor>
<param name="context" />
</constructor>
</register>
</container>
</unity>
<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
<alias alias="IContext" type="Demo.Models.DataInterfaces.IContext, Demo.Models" />
<alias alias="Context" type="Demo.DataMock.Context, Demo.DataMock" />
<alias alias="IRepositoryFactory"
type="Demo.Models.DataInterfaces.IRepositoryFactory, Demo.Models" />
<alias alias="RepositoryFactory" type="Demo.DataMock.RepositoryFactory, Demo.DataMock" />
<container>
<register type="IContext" mapTo="Context">
<lifetime type="PerThreadLifetimeManager" />
</register>
<register type="IRepositoryFactory" mapTo="RepositoryFactory">
<constructor>
<param name="context" />
</constructor>
</register>
</container>
</unity>