ASP.NETMVC3:正确实现UnityDependencyResolver
前日,dudu 写了篇文章 《想爱容易,相处难:当ASP.NET MVC 爱上 IoC》,介绍了在 MVC 中如何使用 Unity,不过 dudu 犯了一个错误:错误地使用了 Unity。
这要先从 Unity 使用说起:
Unity 基本使用
假定程序中有如下两个接口:
1 2 |
public interface ICustomerRepository { /*...*/ } public interface IOrderRepository { /*...*/ } |
和两个实现类:
1 2 |
public class CustomerRepository : ICustomerRepository { /*...*/ } public class OrderRepository : IOrderRepository { /*...*/ } |
可以如下使用:
1 2 3 4 5 6 7 |
var container = new UnityContainer(); //注册 container.RegisterType |
(可在配置文件中注册,请参考相关文档)
但是实际使用中,情况要复杂的多,如下面这个接口和类(如何注入?):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public interface IOrderService { /*...*/ } public class OrderService: IOrderService { private ICustomerRepository customerRepository; private IOrderRepository orderRepository; public OrderService(ICustomerRepository customerRepository, IOrderRepository orderRepository) { this.customerRepository = customerRepository; this.orderRepository = orderRepository; } /*...*/ } |
有朋友会说,可以使用构造函数注入:
1 2 3 4 |
container.RegisterType |
很好,这个这样可以解决这个问题。
但是,这种方式是存在一些缺陷的:
1、多次注册:我们已经将 CustomerRepository 注册给了 ICustomerRepository 接口,但在注册 IOrderService 接口时,还要 new CustomerRepository。多次注册会带来很多问题,假想有一天,我们需要将 CustomerRepository 统统换成 ImprovedCustomerRepository,使用这种方式不得不多处修改。
2、设计变动时,多处修改:设想 OrderService 的构造函数需要增加一个 IProductRepository 类型的参数,另外一个类 StoreService 的构造函数也要增加这么个参数。
3、更复杂的情况,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public interface IOrderController { /*...*/ } public class OrderController: IOrderController { private IOrderService orderService; private IStoreService storeService; public OrderController(IOrderService orderService, IStoreService storeService) { this.orderService = orderService; this.storeService = storeService; } } |
如何在 Unity 中注册 IOrderController? 别告诉我用构造函数注入,这可得两级构造注入。
其实 Unity 支持 Circular References,可以轻松解决这些问题:
使用 Circular References
Circular References 翻译过来是“循环引用“。
Unity 中,不需要任何设置就可以使用 Circular References,使用一个例子来说明 :
1 2 3 4 5 6 7 |
var container = new UnityContainer(); container.RegisterType |
说明(相当啰嗦,明白人可跳过):Unity 在获取 IOrderController 的实例时(第 7 行),根据第 5 行的注册得知,应该创建 OrderController 的实例。但 OrderController 有两个参数,类型分别是 IOrderService、IStoreService,势必先创建 IOrderService 的实例。根据第 4 行得知应该去创建 OrderService 的实例,OrderServer 又有 ICustomerRepository、IOrderRepository 两个参数,再根据第 2、3 行分别创建 CustomerRepository 和 OrderRepository 的实例,因为这两个类构造函数无参,直接可实例化。获取 IOrderService 实例化后,用类似的方式再获取 IStoreService 的实例。这样根据构造函数的参数,一步步向前,称为 Circular References Resolve。
有了 Circular References,不再需要过多的注册,Unity 会智能判断并处理。所以,即使一个类型没有注册在容器中,依然可以获取它的实例:
1 2 3 4 5 6 7 |
var container = new UnityContainer(); container.RegisterType |
源码下载:UnityDemo.rar (522KB)
dudu 文中存在的问题
看了前面的部分,相信你一定能指出 dudu 文中 UnityDependencyResolver 存在问题,原代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class UnityDependencyResolver : IDependencyResolver { IUnityContainer container; public UnityDependencyResolver(IUnityContainer container) { this.container = container; } public object GetService(Type serviceType) { if (!this.container.IsRegistered(serviceType)) { return null; } return container.Resolve(serviceType); } public IEnumerable |
没错,问题就在第 12~15 行,if (!this.container.IsRegistered(serviceType)) return null; 这句其实 ”屏蔽“ 了 Unity 的 Circular References,导致一系列问题,应该去除这四行代码。
另外,使用 Unity (或其它 DI 框架),不需要创建新的 ControllerFactory(如 dudu 文中的 UnityControllerFactory),除非有特殊需要。
从 ControllerBuilder 的源码中,可以看出,如果没有注册 IControllerFactory,MVC 将自动使用 DefaultControllerFactory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public class ControllerBuilder { private Func |
DefaultControllerFactory 在内部(DefaultControllerActivator 类)会尝试调用 DependencyResolver 来获取 Controller 的实例,如果不成功则使用 Activator.CreateInstance :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public class DefaultControllerFactory : IControllerFactory { private IResolver |
通过前面的说明和分析,可以写出 UnityDependencyResolver 的参考实现:
UnityDependencyResolver 参考实现及使用
参考代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public class UnityDependencyResolver : IDependencyResolver { private IUnityContainer container; public UnityDependencyResolver(IUnityContainer container) { this.container = container; } public object GetService(Type serviceType) { try { return container.Resolve(serviceType); } catch (ResolutionFailedException) { //额外操作,如日志 return null; } } public IEnumerable |
第 19~26 行 GetServices 好像还是有些问题的,有谁能指出吗? (答案在此:《ASP.NET MVC 3:放弃 Unity》)
建议实现 IViewPageActivator 接口并注册:
1 2 3 4 5 6 7 |
public class ViewPageActivator : IViewPageActivator { public object Create(ControllerContext controllerContext, Type type) { return Activator.CreateInstance(type); } } |
在 Global.asax 文件中,注册如下:
1 2 3 4 5 6 7 8 9 |
protected void Application_Start() { //... IUnityContainer container = new UnityContainer(); container.RegisterType |
不用担心 UnityDependencyResolver 异常处理带来的效率问题,因为就目前代码,启动之后 UnityDependencyResolver 中只会发生两次异常:获取 IControllerFactory 和 IControllerActivator 时各一次,损失可以忽略不计了。
修正后的源码:MvcIocDemo.rar(485KB)
后记
人非圣贤,难免犯错。
但截止到本文发布时,dudu 这篇文章却有 18 个推荐,只有 1 个反对(我的了),我们不得不反思了。
不华丽分割线
IoC在ASP.NET Web API中的应用
控制反转(Inversion of Control,IoC),简单地说,就是应用本身不负责依赖对象的创建和维护,而交给一个外部容器来负责。这样控制权就由应用转移到了外部IoC容器,控制权就实现了所谓的反转。比如在类型A中需要使用类型B的实例,而B实例的创建并不由A来负责,而是通过外部容器来创建。通过IoC的方式实现针对目标HttpController的激活具有重要的意义。[本文已经同步到《How ASP.NET Web API Works?》]
一、 基于IoC的HttpControllerActivator
将IoC应用于HttpController激活系统的目的在于让一个预定义的IoC容器来提供最终的HttpController对象。通过《ASP.NET Web API的Controller是如何被创建的?》的介绍我们知道HttpController的激活最终由HttpControllerActivator对象来完成,所以将IoC与ASP.NET Web API的HttpController激活系统进行集成最为直接的方式莫过于自定义一个HttpControllerActivator。
我们通过一个简单实例来演示如何通过自定义HttpControllerActivator的方式实现与IoC的集成,我们采用的IoC框架是Unity。我们在一个ASP.NET Web API应用中定义了这个UnityHttpControllerActivator类型。UnityHttpControllerActivator具有一个表示Unity容器的属性UnityContainer,该属性在构造函数中被初始化。在用于创建的HttpController的Create方法中,我们调用此UnityContainer对象的Resolve方法创建目标HttpController对象。
1: public class UnityHttpControllerActivator : IHttpControllerActivator
2: {
3: public IUnityContainer UnityContainer { get; private set; }
4:
5: public UnityHttpControllerActivator(IUnityContainer unityContainer)
6: {
7: this.UnityContainer = unityContainer;
8: }
9:
10: public IHttpController Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)
11: {
12: return (IHttpController)this.UnityContainer.Resolve(controllerType);
13: }
14: }
接下来我们定义了如下一个继承自ApiController的ContactsController来管理联系人信息。简单起见,我们只定义了唯一的Action方法Get用于获取联系人信息。该方法具有一个可缺省的参数id表示希望获取的联系人的ID,如果没有提供此参数则返回所有联系人列表。
1: public class ContactsController : ApiController
2: {
3: public IContactRepository Repository { get; private set; }
4: public ContactsController(IContactRepository repository)
5: {
6: this.Repository = repository;
7: }
8: public IEnumerableGet(string id = "")
9: {
10: return this.Repository.GetContacts(contact =>
11: string.IsNullOrEmpty(id) || id == contact.Id);
12: }
13: }
14:
15: public class Contact
16: {
17: public string Id { get; set; }
18: public string Name { get; set; }
19: public string PhoneNo { get; set; }
20: public string EmailAddress { get; set; }
21: public string Address { get; set; }
22: }
Action方法利用Repository属性返回的对象来实施联系人的查询工作,这个IContactRepository接口类型的属性在构造函数中初始化。我们利用IContactRepository接口来抽象对联系人数据的存储,如下面的代码片断所示,我们在此接口中仅定义了唯一的GetContacts方法根据指定的添加来筛选对应的联系人列表。
1: public interface IContactRepository
2: {
3: IEnumerableGetContacts(Predicate predicate);
4: }
我们定义了如下一个DefaultContactRepository类型作为IContactRepository接口的默认实现者,简单起见,我们采用一个静态字典来保存联系人列表。
1: public class DefaultContactRepository : IContactRepository
2: {
3: private static Listcontacts = new List
4: {
5: new Contact{ Id="001", Name = "张三", PhoneNo="123", EmailAddress = "[email protected]"},
6: new Contact{ Id="002", Name = "李四", PhoneNo="456",EmailAddress = "[email protected]"}
7: };
8:
9: public IEnumerableGetContacts(Predicate predicate)
10: {
11: return contacts.Where(contact=>predicate(contact));
12: }
13: }
我们在Global.asax中对自定义的UnityHttpControllerActivator进行了注册。如下面的代码片断所示,我们在Application_Start方法中创建了一个UnityContainer对象,并通过调用泛型方法RegisterType
1: public class WebApiApplication: System.Web.HttpApplication
2: {
3: protected void Application_Start()
4: {
5: //其他操作
6: IUnityContainer unityContainer = new UnityContainer();
7: unityContainer.RegisterType();
8: GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerActivator), new UnityHttpControllerActivator(unityContainer));
9: }
10: }
当此ASP.NET Web API应用运行之后,我们可以直接在浏览器中输入相应的地址获取所有联系人列表(“/api/contacts”)和针对某个ID为“001”(“/api/contacts/001”)的联系人信息,相应的联系人信息会以如下图所示的形式出现在浏览器上。
二、基于IoC的DependencyResolver
由于默认的DefaultHttpControllerActivator会先利用当前注册的DependencyResolver对象去激活目标HttpController,所以除了利用自定义的HttpControllerActivator将IoC引入HttpController激活系统之外,另一个有效的方案就是注册自定义的DependencyResolver。
接下来将要自定义的DependencyResolver基于另一个叫作“Ninject”的IoC框架。较之Unity,Ninject是一个更加轻量级的IoC框架。篇幅所限,我们不便对这个IoC框架作过多的介绍,有兴趣的读者可以访问其官网(“http://www.ninject.org/”)了解Ninject。
1: public class NinjectDependencyResolver : IDependencyResolver
2: {
3: private ListdisposableServices = new List ();
4: public IKernel Kernel { get; private set; }
5:
6: public NinjectDependencyResolver(NinjectDependencyResolver parent)
7: {
8: this.Kernel = parent.Kernel;
9: }
10:
11: public NinjectDependencyResolver()
12: {
13: this.Kernel = new StandardKernel();
14: }
15:
16: public void Register() where TTo : TFrom
17: {
18: this.Kernel.Bind().To ();
19: }
20:
21: public IDependencyScope BeginScope()
22: {
23: return new NinjectDependencyResolver(this);
24: }
25:
26: public object GetService(Type serviceType)
27: {
28: return this.Kernel.TryGet(serviceType);
29: }
30:
31: public IEnumerable<object> GetServices(Type serviceType)
32: {
33: foreach (var service in this.Kernel.GetAll(serviceType))
34: {
35: this.AddDisposableService(service);
36: yield return service;
37: }
38: }
39:
40: public void Dispose()
41: {
42: foreach (IDisposable disposable in disposableServices)
43: {
44: disposable.Dispose();
45: }
46: }
47:
48: private void AddDisposableService(object servie)
49: {
50: IDisposable disposable = servie as IDisposable;
51: if (null != disposable && !disposableServices.Contains(disposable))
52: {
53: disposableServices.Add(disposable);
54: }
55: }
56: }
我们创建了如上一个类型为NinjectDependencyResolver的自定义DependencyResolver。NinjectDependencyResolver的核心是类型为IKernel的只读属性Kernel,用于获取服务实例的GetService和GetServices方法分别通过调用此Kernel属性的TryGet和GetAll方法来实现。BeginScope方法返回一个新的NinjectDependencyResolver对象,它与自身拥有同一个Kernel对象。我们定义了额外的方法Register
现在我们将这个自定义的NinjectDependencyResolver应用到上一个演示实例中。我们只需要将Global.asax中针对自定义HttpControllerActivator的注册替换成针对NinjectDependencyResolver的注册即可。运行此ASP.NET Web API应用后通过浏览器试图获取联系人信息,我们依然会得到如上图所示的结果。
1: public class MvcApplication : System.Web.HttpApplication
2: {
3: protected void Application_Start()
4: {
5: //其他操作
6: NinjectDependencyResolver dependencyResolver = new NinjectDependencyResolver();
7: dependencyResolver.Register();
8: GlobalConfiguration.Configuration.DependencyResolver = dependencyResolver;
9: }
10: }
不华丽分割线
为ASP.NET MVC创建一个基于Unity的ControllerFactory
谈到IoC和ASP.NET的集成,很多人会先后想到Ninject,不过我们个人还是倾向于Unity。这篇文章简单地介绍如果创建基于Unity的ControllerFactory。如下面的代码所示,我们通过直接继承DefaultControllerFactory创建一个自定的UnityControllerFactory。构造函数指定的是配置的UnityContainer的名称,如果没有显式指定则采用默认的UnityContainer。在重写的GetControllerInstance方法中,直接调用IUnityContainer的Resolve方法根据Controller类型创建相应的对象。[源代码从这里下载]
1: public class UnityControllerFactory: DefaultControllerFactory
2: {
3: public IUnityContainer Container { get; private set; }
4: public UnityControllerFactory(string containerName = "")
5: {
6: IUnityContainer container = new UnityContainer();
7: UnityConfigurationSection configSection = ConfigurationManager.GetSection(UnityConfigurationSection.SectionName) as UnityConfigurationSection;
8: if (null == configSection && !string.IsNullOrEmpty(containerName))
9: {
10: throw new ConfigurationErrorsException("Cannot findconfiguration section" );
11: }
12:
13: if (string.IsNullOrEmpty(containerName))
14: {
15: configSection.Configure(container);
16: }
17: else
18: {
19: configSection.Configure(container, containerName);
20: }
21: this.Container = container;
22: }
23: protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
24: {
25: Guard.ArgumentNotNull(controllerType, "controllerType");
26: return (IController)this.Container.Resolve(controllerType);
27: }
28: }
为了演示DefaultControllerFactory的作用,我们来创建一个简单的例子。假设我们要创建一个维护联系人的应用,我们通过具有如下定义的Contact类型表示联系人,而IContactRepository接口定义了一个从存储中获取所有联系人的GetAllContacts方法,DefaultContactRepository是对IContactRepository接口的实现。
1: public class Contact
2: {
3: public string Name { get; set; }
4: public string Gender { get; set; }
5: public string Address { get; set; }
6: }
7:
8: public interface IContactRepository
9: {
10: IEnumerableGetAllContacts();
11: }
12:
13: public class DefaultContactRepository : IContactRepository
14: {
15: public IEnumerableGetAllContacts()
16: {
17: yield return new Contact
18: {
19: Name = "Zhang San",
20: Gender = "Male",
21: Address = "#328, XingHu Street, Su Zhou, Jiang Su Province, PRC."
22: };
23:
24: yield return new Contact
25: {
26: Name = "Li Si",
27: Gender = "Female",
28: Address = "#328, Jin Ji Hu Road, Su Zhou, Jiang Su Province, PRC."
29: };
30: }
31: }
我们在Web应用的主页显示联系人列表,为此我创建了如下一个HomeController。在这里我们演示的是构造器注入,所以我们通过构造函数指定的IContactRepository对象来初始化Repository属性。在Action方法Index中调用IContactRepository的GetAllContacts方法为对应的View指定Model。
1: public class HomeController : Controller
2: {
3: public IContactRepository Repository { get; private set; }
4: public HomeController(IContactRepository repository)
5: {
6: this.Repository = repository;
7: }
8: public ActionResult Index()
9: {
10: return View(this.Repository.GetAllContacts());
11: }
12: }
Index.cshtml代码如下所示,这是一个Model类型为IEnumerable
1: @model IEnumerable<Artech.Web.Mvc.Extensions.Contact>
2: @{
3: ViewBag.Title = "Index";
4: }
5:
6: <h2>Contact Listh2>
7:
8: <div>
9: <ul>
10: @foreach (var contact in this.Model)
11: {
12: <li>
13: <h3>@contact.Nameh3>
14: <p>Gender: @contact.Genderp>
15: <p>Address: @contact.Addressp>
16: <hr />
17: li>
18: }
19: ul>
20: div>
自定义的UnityContainerFactory的注册定义在Gloable.asax中。初次之外,额外需要做的是忽略掉针对favicon.ico的路由,否则程序运行将会失败。
1: public class MvcApplication : System.Web.HttpApplication
2: {
3: public static void RegisterGlobalFilters(GlobalFilterCollection filters)
4: {
5: filters.Add(new HandleErrorAttribute());
6: }
7:
8: public static void RegisterRoutes(RouteCollection routes)
9: {
10: routes.IgnoreRoute("favicon.ico");
11: routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
12: routes.MapRoute("Default", "{controller}/{action}/{id}",
13: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
14: );
15: }
16:
17: protected void Application_Start()
18: {
19: AreaRegistration.RegisterAllAreas();
20: RegisterGlobalFilters(GlobalFilters.Filters);
21: RegisterRoutes(RouteTable.Routes);
22:
23: ControllerBuilder.Current.SetControllerFactory(new UnityControllerFactory());
24: }
25: }
接口IContactRepository和DefualtContactRepository之间的映射关系定义在如下所示的
1: <unity>
2: <alias alias="IContactRepository" type="Artech.Web.Mvc.Extensions.IContactRepository, UnityIntegration" />
3: <alias alias="DefaultContactRepository" type="Artech.Web.Mvc.Extensions.DefaultContactRepository, UnityIntegration" />
4: <container>
5: <register type="IContactRepository" mapTo="DefaultContactRepository"/>
6: container>
7: unity>
通过浏览器访问Web应用的主页,将会得到如下所示的联系人列表。