目标HttpController在ASP.NET Web API中是如何被激活的:目标HttpController的选择

时间:2022-05-14 04:30:25

目标HttpController在ASP.NET Web API中是如何被激活的:目标HttpController的选择

ASP.NET Web API能够根据请求激活目标HttpController的前提是能够根据请求选择出正确的HttpController,HttpController的选择在ASP.NET Web API中通过HttpControllerSelector来实现。[本文已经同步到《How ASP.NET Web API Works?》]

目录 
HttpControllerSelector & DefaultHttpControllerSelector 
获取目标HttpController的名称 
解析HttpController名称与HttpControllerDescriptor映射 
实例演示:如何选择有效的HttpController类型 
根据请求选择HttpController

HttpControllerSelector & DefaultHttpControllerSelector

所有的HttpControllerSelector均实现了具有如下定义的接口IHttpControllerSelector。

   1: public interface IHttpControllerSelector
   2: {
   3:     IDictionary<string, HttpControllerDescriptor> GetControllerMapping();
   4:     HttpControllerDescriptor SelectController(HttpRequestMessage request);
   5: }

如下面的代码片断所示,该接口中定义了两个方法。GetControllerMapping方法返回一个描述所有HttpController类型的HttpControllerDescriptor对象与对应的HttpController名称之前的映射关系。针对请求对HttpController的选择实现在方法SelectController中,返回的是用以描述目标HttpController的HttpControllerDescriptor对象。

默认使用HttpControllerSelector依然注册到当前HttpConfiguration的ServicesContainer中,我们可以调用ServicesContainer具有如下定义的扩展方法GetHttpControllerSelector或者注册的HttpControllerSelector对象。

   1: public static class ServicesExtensions
   2: {
   3:     //其他成员
   4:     public static IHttpControllerSelector GetHttpControllerSelector(this ServicesContainer services);
   5: }

如下的代码片断所示,HttpConfiguration默认使用的DefaultServices在初始化的过程中会根据指定的HttpConfiguration对象创建一个DefaultHttpControllerSelector对象,并将其注册为默认的HttpControllerSelector。

   1: public class DefaultServices : ServicesContainer
   2: {
   3:     //其他成员
   4:     public DefaultServices(HttpConfiguration configuration)
   5:     {
   6:         //其他操作
   7:         this.SetSingle<IHttpControllerSelector>(new DefaultHttpControllerSelector(configuration));
   8:     }
   9: }

DefaultHttpControllerSelector类型定义在System.Web.Http.Dispatcher命名空间下。如下面的代码片断所示,DefaultHttpControllerSelector不仅仅实现了定义在IHttpControllerSelector接口中的两个方法,还定义另一个名为GetControllerName方法,它根据指定的请求消息得到对应的HttpController名称。

   1: public class DefaultHttpControllerSelector : IHttpControllerSelector
   2: {
   3:     public DefaultHttpControllerSelector(HttpConfiguration configuration);
   4:     public virtual IDictionary<string, HttpControllerDescriptor> GetControllerMapping();
   5:     public virtual HttpControllerDescriptor SelectController(HttpRequestMessage request);
   6:     public virtual string GetControllerName(HttpRequestMessage request);
   7: }

获取目标HttpController的名称

在“消息处理管道”中我们提到过:如果采用Web Host模式,消息管道的缔造者HttpControllerHandler在根据当前HTTP上下文创建用于表示请求的HttpRequestMessage后,会将ASP.NET路由系统解析当前请求得到的RouteData对象转换成HttpRouteData,并添加到HttpRequestMessage的属性字典中。对于Self Host模式来说,处于消息处理管道尾端的HttpRoutingDispatcher会利用ASP.NET Web API的路由系统对当前请求进行匹配并生成用以封装路由数据的HttpRouteData,这个HttpRouteData同样会被添加到表示当前请求的HttpRequestMessage中。

由于被附加到当前请求的HttpRouteData已经包含了目标HttpController的名称(对应的变量名为“controller”),所以我们可以从HttpRequestMessage中直接获取目标HttpController的名称。如下面的代码片断所示,DefaultHttpControllerSelector的GetControllerName方法也是按照这样的逻辑根据指定的HttpMessageMessage中提取目标HttpController的名称。

   1: public class DefaultHttpControllerSelector : IHttpControllerSelector
   2: {  
   3:     //其他成员
   4:     public virtual string GetControllerName(HttpRequestMessage request)
   5:     {      
   6:         IHttpRouteData routeData = request.GetRouteData();
   7:         if (routeData == null)
   8:         {
   9:             return null;
  10:         }
  11:         string str = null;
  12:         routeData.Values.TryGetValue<string>("controller", out str);
  13:         return str;
  14:     }    
  15: }

解析HttpController名称与HttpControllerDescriptor映射

DefaultHttpControllerSelector 的GetControllerMapping方法会返回类型为IDictionary<string, HttpControllerDescriptor>的字典,该字典对象包含了描述所有HttpController的HttpControllerDescriptor对象与对应HttpController名称之间的映射。

   1: public class DefaultHttpControllerSelector : IHttpControllerSelector
   2: {
   3:     //其他成员
   4:     private readonly HttpControllerTypeCache _controllerTypeCache;
   5:     public virtual IDictionary<string, HttpControllerDescriptor> GetControllerMapping();
   6: }

GetControllerMapping方法的实现逻辑简单。如上面的代码片断,DefaultHttpControllerSelector具有一个HttpControllerTypeCache类型的只读字段,通过它可以得到HttpController类型与名称之间的关系,GetControllerMapping方法只需要根据HttpController类型生成对应的HttpControllerDescriptor即可。

但是有个问题必须要考虑,由于同名的HttpController类型可能定义在不同的命名空间下,一个HttpController名称可能对应着多个HttpController类型,所以HttpControllerTypeCache缓存的数据是一个类型为Dictionary<string, ILookup<string, Type>>的字典对象的对象。但是这里的GetControllerMapping方法的返回类型为IDictionary<string, HttpControllerDescriptor>,那么同名HttpController类型应该如何取舍呢?

实际上同名的HttpController类型将不会体现在GetControllerMapping方法返回值中。举个简单的例子,我们通过如下的代码在命名空间HttpControllers1下定义了FooController和BarController,并在命名空间HttpControllers2下定义了BarController和BazController。

   1: namespace HttpControllers1
   2: {
   3:     public class FooController: ApiController{}
   4:     public class BarController : ApiController{}
   5: }
   6:  
   7: namespace HttpControllers2
   8: {
   9:     public class BarController : ApiController{}
  10:     public class BazController : ApiController{}
  11: }

假设有效的HttpController仅限于这4个,现在我们调用DefaultHttpControllerSelector的GetControllerMapping方法,返回的字典对象将只包含两个元素,对应的HttpController类型分别为FooController和BazController,BarController由于命名冲突将被丢弃。GetControllerMapping创建返回映射对象的逻辑基本上可以通过如下的代码片断来体现。

   1: public class DefaultHttpControllerSelector : IHttpControllerSelector
   2: {
   3:     //其他成员
   4:     private HttpControllerTypeCache _controllerTypeCache;
   5:     private HttpConfiguration _configuration;
   6:  
   7:     public virtual IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
   8:     {
   9:         Dictionary<string, HttpControllerDescriptor> mappings = new Dictionary<string, HttpControllerDescriptor>();
  10:         var keys = from item in _controllerTypeCache.Cache
  11:                     where item.Value.Count() == 1
  12:                     select item.Key;
  13:         foreach (string key in keys)
  14:         { 
  15:             Type controllerType = _controllerTypeCache.Cache[key].First().First();
  16:             HttpControllerDescriptor descriptor = new HttpControllerDescriptor(_configuration, key, controllerType);
  17:             mappings.Add(key, descriptor);
  18:         }
  19:         return mappings;
  20:     }
  21: }

需要强调一点的是:上面给出的代码并不是GetControllerMapping方法真正的实现。为了避免相同的操作在每次调用GetControllerMapping方法是被重复执行,DefaultHttpControllerSelector会对该方法返回的字典对象予以缓存,所以这样的操作只会执行一次。

实例演示:如何选择有效的HttpController类型

为了加深读者朋友们对此的认识,我们照例创建一个简单的演示实例。我们在一个空的ASP.NET MVC应用中创建了如下三个ApiController:FooController、BarController和BazController,简单起见,我们并没有为它们定义任何的成员。

   1: namespace MvcApp.Controllers
   2: {
   3:     public class FooController : ApiController{ }
   4:     public class BarController : ApiController{ }
   5:     public class BazController : ApiController{ }
   6: }

我们创建一个具有如下定义的HomeController。在默认的Action方法Index中,我们利用GlobalConfiguration得到注册到当前ServicesContainer上的HttpControllerSelector。我们调用其GetControllerMapping方法返回一个包含描述所有HttpController的HttpControllerDescriptor对象与HttpController名称匹配关系的字典,该字典最终通过默认的View呈现出来。

   1: public class HomeController : Controller
   2: {
   3:     public ActionResult Index()
   4:     {
   5:         IHttpControllerSelector controllerSelector = GlobalConfiguration.Configuration.Services.GetHttpControllerSelector();
   6:         IDictionary<string, HttpControllerDescriptor> mappings = controllerSelector.GetControllerMapping();
   7:         return View(mappings);
   8:     }
   9: }

如下所示的是Action方法Index对应View的定义,这是一个Model类型为IDictionary<string, HttpControllerDescriptor>的强类型View。在该View中,我们将所有HttpController的类型与它们匹配的名称以表格的形式呈现出来。

   1: @using System.Web.Http.Controllers
   2: @model IDictionary<string, HttpControllerDescriptor>
   3: <html>
   4:     <head>
   5:         <title>HttpController映射</title>
   6:     </head>
   7:     <body>
   8:         <table>
   9:            <thead>
  10:                <tr>
  11:                     <th>Controller Name</th>
  12:                     <th>Controller Type</th>
  13:                </tr>
  14:            </thead>
  15:             <tbody>
  16:                 @foreach (var item in Model)
  17:                 { 
  18:                     <tr>
  19:                         <td>@item.Key</td>
  20:                         <td>@item.Value.ControllerType.Name</td>
  21:                     </tr>
  22:                 }
  23:             </tbody>
  24:         </table>
  25:     </body>
  26: </html>

直接运行该程序后会在浏览器中呈现出如下图所示的输出结果,从中可以看出HttpControllerSelector确实能够将用于描述所有HttpController的HttpControllerDescriptor与它们对应的HttpController名称解析出来。

目标HttpController在ASP.NET Web API中是如何被激活的:目标HttpController的选择

为了演示对HttpController名称冲突(即在不同的命名空间中定义同名的HttpController)的处理,我们删除FooController、BarController和BazController这三个类型,改换成如下的定义:命名空间HttpControllers1和HttpControllers2中均定义了一个名为BarController的ApiController。

   1: namespace HttpControllers1
   2: {
   3:     public class FooController: ApiController{}
   4:     public class BarController : ApiController{}
   5: }
   6:  
   7: namespace HttpControllers2
   8: {
   9:     public class BarController : ApiController{}
  10:     public class BazController : ApiController{}
  11: }

再次运行该程序后会在浏览器中呈现出如下图所示的输出结果。我们可以清楚地看出原本定义的4个有效的HttpController,只有FooController和BazController体现在GetControllerMapping方法的返回值中,命名冲突的两个同名的BarController均被排除在外。

目标HttpController在ASP.NET Web API中是如何被激活的:目标HttpController的选择

根据请求选择HttpController

其实HttpControllerSelector的终极使命还是根据请求对目标HttpController的选择,这体现在它的SelectController方法上。对于DefaultHttpControllerSelector来说,其SelectController方法的实现逻辑非常简单:它只需要调用GetControllerName方法从给定的HttpRequestMessage提取目标HttpController的名称,然后根据此名称从GetControllerMapping方法的返回值中提取对应的HttpControllerDescriptor对象即可。

实现在SelectController方法中针对请求的HttpController选择机制虽然简单,但是针对几种特殊情况的处理我们也不应该忽视。首先,如果调用GetControllerName方法返回的HttpController名称为Null或者是一个空字符串,意味着ASP.NET路由系统(针对Web Host)或者ASP.NET Web API路由系统在对请求的解析过程中并没有得到表示目标HttpController的路由变量。在这种情况下,DefaultHttpControllerSelector会直接抛出一个响应状态为HttpStatusCode.NotFound的HttpResponseException异常,客户端自然就会接收到一个状态为“404, Not Found”的响应。其次,如果在调用GetControllerMapping方法返回的字典中并没有一个匹配的HttpControllerDescriptor,通过上面的分析我们知道如下两种情况会导致这样的问题:

  • 在通过AssembliesResolver提供的程序集中并不曾定义这么一个有效的HttpController类型。
  • 在通过AssembliesResolver提供的程序集中不同的命名空间下定义了多个同名的HttpController类型。

这两种情况下自然不能通过GetControllerMapping方法返回的字典对象来判断,但是却可以通过用于缓存HttpController类型的HttpControllerTypeCache对象来判断。对于第一种情况,依然会抛出一个响应状态为HttpStatusCode.NotFound的HttpResponseException异常。在第二种情况下,DefaultHttpControllerSelector会抛出一个InvalidOperationException异常,并提示“具有多个匹配的HttpController”。如果利用浏览器来访问的话就会得到如下图所示的输出结果。

目标HttpController在ASP.NET Web API中是如何被激活的:目标HttpController的选择

作者:Artech
出处:http://artech.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。