ASP.NET MVC + ADO.NET EF 项目实战(一):应用程序布局设计
什么叫上下文?
在你设计一个方法的时候,无法直接从方法参数或实例成员(字段或属性)获得的所有信息都是上下文。例如:
- 当前用户是谁?
- 刚才提供操作的数据库连接实例从哪里拿到?
- 这个方法从哪个 View 或者哪个 Controller 调用的?
当然,在方法体中获得上下文最终还是要靠方法参数或实例成员。
在MVC中有大量的上下文信息,例如:
- ControllerContext
- ViewContext
- ModelBindingContext
- ExceptionContext
- ActionExcutingContext
- ActionExcutedContext
- AuthorizationContext
- ResultExcutingContext
- ResultExcutedContext
这些上下文通过单一的参数提供了丰富的运行时信息。
实体上下文放到哪里?
除了MVC的上下文外,还有一个重要的上下文就是 ADO.NET EF的实体上下文,通常派生自System.Data.Objects.ObjectContext,都是由IDE自动生成的。这个上下文承载了数据库连接,需要通过IDisposable来释放连接。多数情况下,我们这样使用:
using(MyEntities context = new MyEntities())
{
…… 在这里写入代码
}
如果在一次页面生命周期内只使用一次实体上下文这样处理是非常合适的,但是事实上不都是这样。更多的时候可能需要临时对实体进行一个小的访问,例如获得一个当前用户的显示名,通过这种方式访问就代价太大了。
我们知道,这个上下文可以存放到HttpContext里。在HttpContext的所有容器中,只有Items是最合适的,因为这个属性的存续期在后台页面对象释放后就结束了。当然,被释放时也不会执行IDisposable的Dispose方法。我们仍然需要在Global.asax中捕捉EndRequest事件。但是奇妙的是:在ASP.NET MVC Application中不能使用event方式来捕捉,只能手工写Application_EndRequest方法。
什么是一次Model、二次Model和Form Model?
Model一共分为三种:
- 直接数据库实体映射实例,如Product(产品)
- 为View的呈现提供服务的包装对象,如ProductInfo(产品信息)
- 为Post回传提供服务的包装对象,如ProductForm(产品属性值)
第一种类型Model的特点是非常浓缩,几乎没有冗余,通过复杂的关系进行组合,通常需要通过多个不同类型的实例进行组合来表达一个完整的有意义的场景。例如,一个产品信息可能包含产品名称、产品类别、该产品所有的规格型号以及每种规格型号的参数、单价等。虽然ADO.NET EF提供了获取组合属性的能力,但不能处理多层次,并且不能对加载过程进行控制。所以,需要专门定义一些Model对这一组Model进行包装。如果把原始的模型称做“一次Model”,则可以把这个包装对象称做“二次Model”。
页面上收集到的Form信息,通过三种方式传递到Controller(以登录为例):
- 每个信息项一个参数:public ActionResult Login(string userName, string password){…}
- 一个单一的名值对参数:public ActionResult Login(FormCollection formCollection){…}
- 一个单一的包装对象:public ActionResult Login(LoginInfo info){…}
第一种方式不利于重构。当需要加入一个参数时,必须修改Action的签名。而且也无法令Controller把值传递到View。第二种方式不利于设计时纠错,因为FormCollection中的值不是强类型的。所以,我们通常都会采用第三种方式。虽然ADO.NET EF对象可以直接作为Form Model,并且有BindAttribute对属性与Form值进行定制化的绑定,但是不够灵活,如果一个Form组合跨多个一次Model类型,则根本无法处理。所以我们有必要专门定义一个Model给View使用。我们不妨称之为“Form Model”。
业务逻辑放到什么地方?
MVC是一种“古老”的设计模式,提供了非常自然的分层方式,这也是为什么利于单元测试的原因。除了MVC这些“主层”以外,BLL可以算是一个“亚层”。那么,我们把BLL放到什么地方最合适?
BLL需要完全可见Model层,同时也需要一些上下文信息。例如,我们至少从我们刚才描述的论题中发现,需要从HttpContext的Items中获得实体上下文。有些时候,我们还需要将用户的一些登录信息缓存到HttpContext中,如果用户的登录信息非常复杂的话,仅仅依靠HttpContext.User.Identity.Name每次去抓取未必很合算。我的习惯是把和这个用户相关的信息组合到一个大的Model对象中,并把这个对象的实例 存放到HttpContext.Cache中。如果有任何变化,释放这个Cache项即可。
所以,对于业务逻辑的位置你可以有两个选择:
- 放到 Model 下,再建立一个“上下文提供器接口”,由 Model 借助上下文来独立处理。
- 放到 Controller 下,直接使用 Controller 提供的上下文来进行处理。
第一种方式不依赖Controller,解耦彻底,非常灵活,更易于测试。但是需要付出一定的成本,调用栈会稍深一点,还需要劳神处理到Controller与BLLContext间的关系。第二种方式解耦不够彻底,但非常简捷,比较适合 Controller 与 Model 不必彻底解耦的小型项目。有意思的是:ASP.NET MVC Application模板所生成的AccountController采用的就是第二种方式。
ADO.NET EF仅影响ASP.NET MVC的Model层。在Model层中除了EDMX自动生成的一次Model外,我们还需要建立大量的二次Model和Form Model。当然,从提升内聚度考虑,所有的业务逻辑方法都在这些Model中定义,特别是,可以利用partial类和扩展方法这两种手段加入业务逻辑。