最近学习了下 ASP.NET MVC,比较之前的 WebForm 没有了 IsPostBack 的判断,事件处理也被 Action 取代。MVC 中 WebForm中大量的事件处理中UI绑定,混杂的 js 注入, style 修改没有了;服务端控件不用了,结局是 View 被释放了,Controller可以被单元测试了,拿着 ViewModel 可以快速替换 View。(说句实话要不是有 Razor 这样的页面引擎加上 VS IDE 的强力智能感知,ASP.NET MVC 和 JSP 没有区别,说不定还会有人把 strust 标签,spring 标签拿来在 .NET 上封装一遍)
再加上现在的 EF,Model层以及DAL实现很自然的交给了 EF等ORM框架。加上现在的成熟的 Repository Pattern 和 UnitOfWork Pattern 实践上的Service分层也变成约定俗成。(关于 Repository 和 UnitOfWork 参看:Implementing the Repository and Unit of Work Patterns in an ASP.NET MVC Application) 。分离出 Repository, UnitOfWork 就是避免在 Controller 里直接写入 Linq2Db 的代码,这样难以实现 Mockup,好比下面的代码:
(详细参考:Walkthrough: Using TDD with ASP.NET MVC)
【没有使用 Repository Pattern 的代码】
[csharp]
public class HomeController : Controller {
ContactEntities _db = new ContactEntities();
public ActionResult Index() {
var dn = _db.Contacts;
return View(dn);
}
}
【使用 Repository Pattern 的代码】
[csharp]
namespace MvcContacts.Models {
public class EF_ContactRepository : MvcContacts.Models.IContactRepository {
private ContactEntities _db = new ContactEntities();
public Contact GetContactByID(int id) {
return _db.Contacts.FirstOrDefault(d => d.Id == id);
}
...
测试可以用个 Mock Repository,_db 数据从哪来就自由了...
[csharp]
namespace MvcContacts.Tests.Models {
class InMemoryContactRepository : MvcContacts.Models.IContactRepository {
private List<Contact> _db = new List<Contact>();
...
而 Controller 变成这样: www.2cto.com
[csharp]
public class HomeController : Controller {
IContactRepository _repository;
public HomeController() : this(new EF_ContactRepository()) { }
public HomeController(IContactRepository repository) {
_repository = repository;
}
public ViewResult Index() {
throw new NotImplementedException();
}
}
}
其实上面的都是引子,用 Unity 目的是进一步推迟 Repository 或者 UnitOfWork (很多时候演变成 Service 了) 的实例化时机,交给了 IoC 容器注入。以达到更灵活切换的目的,比如从 MS Entities 变换到 MySql Entities 或者是从 ObjectContext 变换到 DbContext (CodeFirst)。
下面介绍一下 Unity.MVC3 的实践过程:
1. EF CodeFirst Models
用 EF CodeFirst 创建一个 Models 工程,用来管理 Entities
表很简单,只有一个 DbSet<User>
[csharp]
namespace MvcWithUnityTest.Models
{
public class DbEntities : DbContext
{
public DbSet<User> Users { get; set; }
}
}
2. GenericRepository
主要针对 EF(ObjectContext) 和 EF CodeFirst(DbContext) 抽出接口 IRepository
[csharp]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
namespace GenericRepository
{
public interface IRepository<T> : IDisposable where T : class
{
IQueryable<T> AsQueryable();
IEnumerable<T> GetAll();
IEnumerable<T> Find(Expression<Func<T, bool>> where);
T Single(Expression<Func<T, bool>> where);
T First(Expression<Func<T, bool>> where);
void Delete(T entity);
void Add(T entity);
void Update(T entity);
}
}
DbContextRepository<T> 对应于 DbContext 的 IRepository<T> 实现,通过构造方法注入 Context 实例
[csharp]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;
using System.Linq.Expressions;
namespace GenericRepository
{
public class DbContextRepository<T> : IRepository<T> where T : class
{
protected DbSet<T> _objectSet;
protected DbContext _context;
public DbContextRepository(DbContext context)
{
_objectSet = context.Set<T>();
_context = context;
}
public IQueryable<T> AsQueryable()
{
return _objectSet;
}
public IEnumerable<T> GetAll()
{
return _objectSet.ToList();
}
public IEnumerable<T> Find(Expression<Func<T, bool>> where)
{
return _objectSet.Where(where);
}
public T Single(Expression<Func<T, bool>> where)
{
return _objectSet.Single(where);
}
public T First(Expression<Func<T, bool>> where)
{
return _objectSet.First(where);
}
public void Delete(T entity)
{
if (_context.Entry(entity).State == System.Data.EntityState.Detached)
_objectSet.Attach(entity);
_objectSet.Remove(entity);
}
public void Add(T entity)
{
_objectSet.Add(entity);
}
public void Update(T entity)
{
_objectSet.Attach(entity);
_context.Entry(entity).State = System.Data.EntityState.Modified;
}
public void Dispose()
{
System.Diagnostics.Trace.WriteLine("context dispose");
_context.Dispose();
}
}
}
3. MVC Web 应用
(1) 先通过 NuGet 获取 Unity.MVC3
ASP.NET MVC3 中开放了依赖注入容器的接口 IDependencyResolver,ASP.NET Controller 被调用时,会利用该接口进行依赖注入。因此可以利用这个接口,
使用任何的依赖注入容器。另外,Unity.MVC3.dll 在 UnityContainerExtensions 类里扩展了 RegisterControllers 方法,
它将为当前 Assembly 所有非 abstract Controller 完成注册(来自 IControllerFactory 的依赖 )
添加完毕,会发现在 Web 工程下多出 Bootstrapper.cs 文件
[csharp] print?public static class Bootstrapper
{
public static void Initialise()
{
var container = BuildUnityContainer();
DependencyResolver.SetResolver(new UnityDependencyResolver(container));
}
private static IUnityContainer BuildUnityContainer()
{
var container = new UnityContainer();
// register all your components with the container here
// e.g. container.RegisterType<ITestService, TestService>();
container.LoadConfiguration("default");
container.RegisterControllers();
return container;
}
}
public static class Bootstrapper
{
public static void Initialise()
{
var container = BuildUnityContainer();
DependencyResolver.SetResolver(new UnityDependencyResolver(container));
}
private static IUnityContainer BuildUnityContainer()
{
var container = new UnityContainer();
// register all your components with the container here
// e.g. container.RegisterType<ITestService, TestService>();
container.LoadConfiguration("default");
container.RegisterControllers();
return container;
}
}
这里我把依赖关系都放到配置文件里了:
[html] print?<configuration>
<configSections>
<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/>
</configSections>
...
<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
<alias alias="IRepository" type="GenericRepository.IRepository`1, GenericRepository" />
<alias alias="DbContextRepository" type="GenericRepository.DbContextRepository`1, GenericRepository" />
<alias alias="User" type="MvcWithUnityTest.Models.User, MvcWithUnityTest.Models" />
<alias alias="DbEntities" type="MvcWithUnityTest.Models.DbEntities, MvcWithUnityTest.Models" />
<container name="default">
<register type="IRepository[User]" mapTo="DbContextRepository[User]">
<lifetime type="HierarchicalLifetimeManager" />
<constructor>
<param name="context" dependencyType="DbEntities">
</param>
</constructor>
</register>
</container>
</unity>
</configuration>
<configuration>
<configSections>
<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/>
</configSections>
...
<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
<alias alias="IRepository" type="GenericRepository.IRepository`1, GenericRepository" />
<alias alias="DbContextRepository" type="GenericRepository.DbContextRepository`1, GenericRepository" />
<alias alias="User" type="MvcWithUnityTest.Models.User, MvcWithUnityTest.Models" />
<alias alias="DbEntities" type="MvcWithUnityTest.Models.DbEntities, MvcWithUnityTest.Models" />
<container name="default">
<register type="IRepository[User]" mapTo="DbContextRepository[User]">
<lifetime type="HierarchicalLifetimeManager" />
<constructor>
<param name="context" dependencyType="DbEntities">
</param>
</constructor>
</register>
</container>
</unity>
</configuration>然后在 Global.asax.cs 里调用 Bootstrapper.Initialise(); 即可。
另外,需要注意的是 <register type="IRepository[User]" mapTo="DbContextRepository[User]"> 里加上了 <lifetime type="HierarchicalLifetimeManager" />
这样在 Controller 生命周期结束时才会调用 Dispose。(待展开)
再来看看 Controller 的实现:
根据上面的配置文件:<register type="IRepository[User]" mapTo="DbContextRepository[User]">
[Dependency] 标识的 UserRepository 会在 Controller 请求时被注入实例。
[csharp] print?public class HomeController : Controller
{
[Dependency]
public IRepository<User> UserRepository { get; set; }
public ActionResult Index()
{
ViewBag.Message = "ASP.NET MVC3 With Unity.MVC3!";
var users = UserRepository.GetAll();
return View(users);
}
public ActionResult About()
{
return View();
}
}
public class HomeController : Controller
{
[Dependency]
public IRepository<User> UserRepository { get; set; }
public ActionResult Index()
{
ViewBag.Message = "ASP.NET MVC3 With Unity.MVC3!";
var users = UserRepository.GetAll();
return View(users);
}
public ActionResult About()
{
return View();
}
}
运行:
利用了 IoC 整个系统结构,好比如下图: