天天看點

Web APi之控制器選擇Action方法過程(九)

前言

前面我們叙述了關于控制器建立的詳細過程,在前面完成了對控制器的激活之後,就是根據控制器資訊來查找比對的Action方法,這就是本節要講的内容。當請求過來時首先經過宿主處理管道然後進入Web API消息處理管道,接着就是控制器的建立和執行控制器即選擇比對的Action方法最終并作出響應(在Action方法上還涉及到模型綁定、過濾器等,後續講)。從上知,這一系列大部分内容都已囊括,我們這一系列也就算快完事,在後面将根據這些管道事件以及相關的處理給出相應的練習來熟練掌握,希望此系列能幫助到想學習原理之人。

HttpControllerContext

我們回顧下此控制器上下文建立的過程:

HttpControllerContext = controllerContext = new HttpControllerContext(descriptor.Configuration,routeData,request)     {        Controller = controller;        ControllerDescrptor = descriptor      }      

從上知以及從其名稱可得知,控制器上下文就是将建立的控制器以及其描述資訊封裝到其中,友善調用而已。接下來就進入控制器上Action方法的選擇,請往下看!

ApiController

相信大家對此類并不敏感,隻要你建立了Web API控制器都是繼承于該類,下面我們來看看此類的定義:

1 public abstract class ApiController : IHttpController, IDisposable      2 {      3     // Fields      4     private HttpConfiguration _configuration;      5     private HttpControllerContext _controllerContext;      6     private bool _disposed;      7     private ModelStateDictionary _modelState;      8     private HttpRequestMessage _request;      9     private UrlHelper _urlHelper;     10      11     // Methods     12     protected ApiController();     13     public void Dispose();     14     protected virtual void Dispose(bool disposing);     15     public virtual Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken);     16 }      

此上知該類為抽象類型,并在其中定義了一個ExecuteAsync()方法,此方法是此類中最重要的一個方法,因為控制器的執行過程就是在此方法中進行,下面我們繼續來看看此方法的定義及實作。

1 public virtual Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)      2 {      3     if (this._request != null)      4     {      5         throw Error.InvalidOperation(SRResources.CannotSupportSingletonInstance, new object[] { typeof(ApiController).Name, typeof(IHttpControllerActivator).Name });      6     }      7     this.Initialize(controllerContext);      8     if (this._request != null)      9     {     10         this._request.RegisterForDispose(this);     11     }     12     HttpControllerDescriptor controllerDescriptor = controllerContext.ControllerDescriptor;     13     ServicesContainer controllerServices = controllerDescriptor.Configuration.Services;     14     HttpActionDescriptor actionDescriptor = controllerServices.GetActionSelector().SelectAction(controllerContext);     15     HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor);     16 }      

上述我隻列出此節要講的内容,有關過濾器以及其管道和授權等等都已略去,後續會講。最重要的就是上述紅色标記的四個了,下面我一一來檢視:

第一步:HttpControllerDescriptor controllerDescriptor = controllerContenxt.ControllerDescriptor

在建立并激活控制器後其控制器上下文也同時生成,我們從控制器上下文中獲得有關控制器的描述即HttpControllerDescriptor 

第二步:ServicesContainer controllerServices = controllerDescriptor.Configuration.Services;

從控制器的描述中的屬性Configuration即HttpConfiguraion來獲得其有關控制器的服務容器即Services

由于第三步涉及到其他類,此時我們将另起一段來叙述,請繼續往下看!

HttpActionDescriptor

第三步:HttpActionDescriptor actionDescriptor = controllerServices.GetActionSelector().........

這一步就是重點所在,我們在前面叙述了多次關于服務容器ServicesContainer,這裡不過是獲得有關具體的服務容器而已,我們看看GetActionSelector方法:

public static IHttpActionSelector GetActionSelector(this ServicesContainer services)     {         return services.GetServiceOrThrow<IHttpActionSelector>();     }      

到了這裡看過前面的文章相信大家不會陌生,就是從伺服器容器中去獲得我們注入的具體的實作。我們繼續看看其子類DefaultServices具體實作到底是什麼  

this.SetSingle<IHttpActionSelector>(new ApiControllerActionSelector());      

ApiControllerActionSelector

從上得知,注入的服務為ApiControllerActionSelector類,我們繼續追查此類:

1 public class ApiControllerActionSelector : IHttpActionSelector      2 {      3     // Fields      4     private readonly object _cacheKey;      5     private ActionSelectorCacheItem _fastCache;      6     private const string ActionRouteKey = "action";      7     private const string ControllerRouteKey = "controller";      8       9     // Methods     10     public ApiControllerActionSelector();     11     public virtual ILookup<string, HttpActionDescriptor> GetActionMapping(HttpControllerDescriptor controllerDescriptor);     12     private ActionSelectorCacheItem GetInternalSelector(HttpControllerDescriptor controllerDescriptor);     13     public virtual HttpActionDescriptor SelectAction(HttpControllerContext controllerContext);     14      15     // Nested Types     16     private class ActionSelectorCacheItem     17     {     18         // Fields     19         private readonly ReflectedHttpActionDescriptor[] _actionDescriptors;     20         private readonly ILookup<string, ReflectedHttpActionDescriptor> _actionNameMapping;     21         private readonly IDictionary<ReflectedHttpActionDescriptor, string[]> _actionParameterNames;     22         private readonly HttpMethod[] _cacheListVerbKinds;     23         private readonly ReflectedHttpActionDescriptor[][] _cacheListVerbs;     24         private readonly HttpControllerDescriptor _controllerDescriptor;     25      26         // Methods     27         public ActionSelectorCacheItem(HttpControllerDescriptor controllerDescriptor);     28         private static string CreateAmbiguousMatchList(IEnumerable<HttpActionDescriptor> ambiguousDescriptors);     29         private ReflectedHttpActionDescriptor[] FindActionsForVerb(HttpMethod verb);     30         private ReflectedHttpActionDescriptor[] FindActionsForVerbWorker(HttpMethod verb);     31         private IEnumerable<ReflectedHttpActionDescriptor> FindActionUsingRouteAndQueryParameters(HttpControllerContext controllerContext, IEnumerable<ReflectedHttpActionDescriptor> actionsFound, bool hasActionRouteKey);     32         public ILookup<string, HttpActionDescriptor> GetActionMapping();     33         private static bool IsSubset(string[] actionParameters, HashSet<string> routeAndQueryParameters);     34         private static bool IsValidActionMethod(MethodInfo methodInfo);     35         private static List<ReflectedHttpActionDescriptor> RunSelectionFilters(HttpControllerContext controllerContext, IEnumerable<HttpActionDescriptor> descriptorsFound);     36         public HttpActionDescriptor SelectAction(HttpControllerContext controllerContext);     37      38         // Properties     39         public HttpControllerDescriptor HttpControllerDescriptor { get; }     40     }     41      42     private class LookupAdapter : ILookup<string, HttpActionDescriptor>, IEnumerable<IGrouping<string, HttpActionDescriptor>>, IEnumerable     43     {     44         // Fields     45         public ILookup<string, ReflectedHttpActionDescriptor> Source;     46      47         // Methods     48         public LookupAdapter();     49         public bool Contains(string key);     50         public IEnumerator<IGrouping<string, HttpActionDescriptor>> GetEnumerator();     51         IEnumerator IEnumerable.GetEnumerator();     52      53         // Properties     54         public int Count { get; }     55         public IEnumerable<HttpActionDescriptor> this[string key] { get; }     56     }     57 }      

在此類中還額外包括兩個私有的類: ActionSelectorCacheItem 和 LookupAdaper 。先保留這兩個類,我們首先看看SelectAtion()方法

public virtual HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)     {         if (controllerContext == null)         {             throw Error.ArgumentNull("controllerContext");         }         return this.GetInternalSelector(controllerContext.ControllerDescriptor).SelectAction(controllerContext);     }      

我們從此方法得知,此方法的實作本質是調用了傳回值為  ActionSelectorCacheItem中的方法GetInternalSelector,我們從ActionSelectorCacheItem來猜此方法應該是生成一個控制器方法的緩存對象,是以此ActionSelectorCacheItem類型将是我們接下來介紹的重中之重。它被用來篩選Action()方法我們看看這個類:

1 private class ActionSelectorCacheItem      2 {      3     // Fields      4     private readonly ReflectedHttpActionDescriptor[] _actionDescriptors;      5     private readonly ILookup<string, ReflectedHttpActionDescriptor> _actionNameMapping;      6     private readonly IDictionary<ReflectedHttpActionDescriptor, string[]> _actionParameterNames;      7     private readonly HttpMethod[] _cacheListVerbKinds;      8     private readonly ReflectedHttpActionDescriptor[][] _cacheListVerbs;      9     private readonly HttpControllerDescriptor _controllerDescriptor;     10      11     // Methods     12     public ActionSelectorCacheItem(HttpControllerDescriptor controllerDescriptor);     13     public HttpActionDescriptor SelectAction(HttpControllerContext controllerContext);     14      15     // Properties     16     public HttpControllerDescriptor HttpControllerDescriptor { get; }     17 }      

以上四個屬性并傳回為相應的傳回值是篩選方法的五個步驟之一,還有一個當然是構造函數的初始化了,這一切都将在構造函數中進行篩選,檢視其構造函數:

public ActionSelectorCacheItem(HttpControllerDescriptor controllerDescriptor)     {         this._actionParameterNames = new Dictionary<ReflectedHttpActionDescriptor, string[]>();         this._cacheListVerbKinds = new HttpMethod[] { HttpMethod.Get, HttpMethod.Put, HttpMethod.Post };         this._controllerDescriptor = controllerDescriptor;         MethodInfo[] infoArray2 = Array.FindAll<MethodInfo>(this._controllerDescriptor.ControllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance), new Predicate<MethodInfo>(ApiControllerActionSelector.ActionSelectorCacheItem.IsValidActionMethod));         this._actionDescriptors = new ReflectedHttpActionDescriptor[infoArray2.Length];         for (int i = 0; i < infoArray2.Length; i++)         {             MethodInfo methodInfo = infoArray2[i];             ReflectedHttpActionDescriptor key = new ReflectedHttpActionDescriptor(this._controllerDescriptor, methodInfo);             this._actionDescriptors[i] = key;             HttpActionBinding actionBinding = key.ActionBinding;             this._actionParameterNames.Add(key, (from binding in actionBinding.ParameterBindings                 where (!binding.Descriptor.IsOptional && TypeHelper.IsSimpleUnderlyingType(binding.Descriptor.ParameterType)) && binding.WillReadUri()                 select binding.Descriptor.Prefix ?? binding.Descriptor.ParameterName).ToArray<string>());         }         this._actionNameMapping = this._actionDescriptors.ToLookup<ReflectedHttpActionDescriptor, string>(actionDesc => actionDesc.ActionName, StringComparer.OrdinalIgnoreCase);         int length = this._cacheListVerbKinds.Length;         this._cacheListVerbs = new ReflectedHttpActionDescriptor[length][];         for (int j = 0; j < length; j++)         {             this._cacheListVerbs[j] = this.FindActionsForVerbWorker(this._cacheListVerbKinds[j]);         }     }      

上述用五種顔色來标記五種篩選,我們由上至下一一進行簡短解釋:

  • 初始化篩選

通過傳遞進來的HttpControllerDesccriptor對象而給其該類此對象的變量屬性,然後根據反射條件 BindingFlags.Public | BindingFlags.Instance 來獲得控制器中的所有方法,最終儲存在MethodInfo[]數組中。

  • ReflectedHttpActionDescriptor[] _actiondescriptors初始化

ReflectedHttpActionDescriptor是一個抽象類,通過上述已經控制器中的方法封裝到MethodInfo[]數組中,此時将循環此數組并将每一個MehodInfo執行個體封裝成ReflectedHttpActionDescriptor對象,并将每個執行個體封裝到此對象數組中,通過該類名稱,顧名思義也知,通過反射來擷取Action方法的中繼資料資訊。

  •  IDictionary<ReflectedHttpActionDesciptor,string[]> _actionparameterNames初始化

利用獲得的ReflectedHttpActionDescriptor進行分析,并将其放到字典中,将此類作為鍵,而該方法的參數名稱。

  • ILookup<string,ReflectedHttpActionDescriptor> _actionNameMapping初始化 

還是利用獲得ReflectedHttpActionDescriptor來依據Action()方法名稱來進行分組。

  • ReflectedHttpActionDescriptor[][] _cacheListVerbs初始化

利用獲得的ReflectedHttpActionDescriptor進行依據Http方法類型來進行分類。

上面的五個隻是初始化指派,是為了下面篩選Action名稱做準備,似乎有點難以了解,請看下面我們一一進行講解!

Action名稱篩選 

既然是篩選名稱則要用到 ActionSelectorCacheItem 中的 SelectAction() 方法了,我們來仔細看看此方法的定義及實作:

public HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)     {         string str;         ReflectedHttpActionDescriptor[] descriptorArray;         Func<ReflectedHttpActionDescriptor, bool> predicate = null;         bool hasActionRouteKey = controllerContext.RouteData.Values.TryGetValue<string>("action", out str);         HttpMethod incomingMethod = controllerContext.Request.Method;         if (hasActionRouteKey)         {             ReflectedHttpActionDescriptor[] source = this._actionNameMapping[str].ToArray<ReflectedHttpActionDescriptor>();             if (source.Length == 0)             {                 throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, Error.Format(SRResources.ResourceNotFound, new object[] { controllerContext.Request.RequestUri }), Error.Format(SRResources.ApiControllerActionSelector_ActionNameNotFound, new object[] { this._controllerDescriptor.ControllerName, str })));             }             if (predicate == null)             {                 predicate = actionDescriptor => actionDescriptor.SupportedHttpMethods.Contains(incomingMethod);             }             descriptorArray = source.Where<ReflectedHttpActionDescriptor>(predicate).ToArray<ReflectedHttpActionDescriptor>();         }         else         {             descriptorArray = this.FindActionsForVerb(incomingMethod);         }         if (descriptorArray.Length == 0)         {             throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(HttpStatusCode.MethodNotAllowed, Error.Format(SRResources.ApiControllerActionSelector_HttpMethodNotSupported, new object[] { incomingMethod })));         }         IEnumerable<ReflectedHttpActionDescriptor> descriptorsFound = this.FindActionUsingRouteAndQueryParameters(controllerContext, descriptorArray, hasActionRouteKey);         List<ReflectedHttpActionDescriptor> list = RunSelectionFilters(controllerContext, descriptorsFound);         descriptorArray = null;         descriptorsFound = null;         switch (list.Count)         {             case 0:                 throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, Error.Format(SRResources.ResourceNotFound, new object[] { controllerContext.Request.RequestUri }), Error.Format(SRResources.ApiControllerActionSelector_ActionNotFound, new object[] { this._controllerDescriptor.ControllerName })));             case 1:                 return list[0];         }         string str2 = CreateAmbiguousMatchList((IEnumerable<HttpActionDescriptor>) list);         throw Error.InvalidOperation(SRResources.ApiControllerActionSelector_AmbiguousMatch, new object[] { str2 });     }      

我們通過前面的行号來講解,我們知道在Web API的配置檔案中預設是沒有Action方法,當然你也手動配置Action方法,是以此時就會出現兩種情況,由上亦知:

(6)我們從控制器上下文中擷取路由對象RouteData,并且在其Values屬性中查找action對應的值并傳回該值

  • 若注冊路由中有Action名稱  

當查找到有Action名稱時,看(10)根據上述初始化的_actionNameMapping來獲得相應的action方法的中繼資料資訊并傳回一個ReflectedHttpActionDescriptor數組也就是source。再看(17)我們從ReflectedHttpActionDescriptor的屬性集合SupportedHttpMethods即Http支援的方法中去找到根據從(7)控制器上下文中獲得請求過來的Http方法,最後以(17)為條件擷取到滿足條件的中繼資料資訊ReflectedHttpActionDescriptor數組即descriptorArray中。

關于此注冊路由有Action名稱的例子就不再叙述,與在MVC架構中請求Action方法類似。

  • 若注冊路由無Action名稱,則依據Http方法來決定Action方法名稱

此時則利用上述初始化依據Http方法類型分類的_cacheListVerbs根據控制器上下文請求過來的方法類型來進行篩選決定其Http方法 ,我們舉例來說明:

我們配置其路由為:  

config.Routes.MapHttpRoute(                     name: "DefaultApi",                     routeTemplate: "api/{controller}/{id}",                     defaults: new { id = RouteParameter.Optional }                 );      

我們的Action方法為:

public void GetProduct(int id) { }             public void PostProduct(int id) { }             public void PutProduct(int id) { }             public void DeleteProduct(int id) { }             public void HeadProduct(int id) { }      

當我們進行如下Get請求時:

$.ajax({                     type: "get",                     url: "http://localhost:7114/api/product/1",                     dataType: "json",                     contentType: "application/json; charset=utf-8",                          cache: false,                     success: function (r) {                         console.log(r);                     }                 });      

此時會比對到對應的如下Action方法:

Web APi之控制器選擇Action方法過程(九)

其他的方法請求也類似Get除了Post之外,下面我們進行Post請求:  

Web APi之控制器選擇Action方法過程(九)

觀察此結果似乎也沒什麼不一樣,No,我們在Action方法裡再添加一個方法如下:

public void Other(int id) { }      

接下來我們再來進行Post請求,你會接收到如下響應錯誤

{"Message":"An error has occurred.","ExceptionMessage":"Multiple actions were found that match the request: \r\nVoid PostProduct(Int32) on type MvcWebApi.Controllers.ProductController\r\nVoid Other(Int32) on type MvcWebApi.Controllers.ProductController".......}

出錯原因是有多個Action方法被比對到,那麼為什麼會出現這樣的錯誤呢?這就要說到約定了:

定義在控制器上的Action方法預設是僅支援一種Http方法,并且這個支援的Http方法決定于Action方法的名稱,明确來講,具有以上Action方法的字首與對應的Http方法将預設支援,比如以上的GetProduct,那麼預設隻支援Get。但是如果Action名稱沒有這樣的字首,如以上的Other方法,那麼預設支援Post。

基于以上預設約定,是以此時PostProduct和Other都會被比對到,但是Action預設又僅支援一種Http方法,是以此時會出現如上錯誤。

這種有Action方法決定它所支援的Http方法的政策與RESET架構風格完全吻合,因為後者推薦直接采用Http方法來表示針對目标資源的操作方式,題外話,但是很多情況下,我們需要一個Action方法既支援Get又支援Post方法這個時候就需要用到ActionHttpMethodProvider特性解決。  

依據請求參數名稱及個數篩選 

  • 有參數

在這種情況下,會先把路由資料對象的Values屬性中的Keys值存放在一個集合A中,然後再擷取目前請求的查詢字元串集合,并且把集合中的所有Key值添加到集合A中,這就使的所有請求的參數名稱都在一個集合中,然後就會從上述初始化的第二個的結果中根據目前的ReflectedHttpActionDescriptor類型執行個體(這裡是接着上述初始化第三個的流程,是以這裡是ReflectedHttpActionDescriptor類型的數組周遊執行)從_actionParameterNames擷取對應的參數名稱數組,然後是集合A會和擷取的參數名稱數組做一一的比較,看看集合A中是否包含參數名稱,如果都有了則是滿足條件。

這裡傳回的依然可能是ReflectedHttpActionDescriptor類型的數組,因為在一個方法有重載時,比如說Get(stringa)和Get(string a,string b)兩個方法時,請求中如果有a和b兩個參數的話,Get(string a)也是滿足條件的。

  • 無參數

分别是利用 IActionMethodSelector、IActionMethodSelector以及IActionMethodSelector 來實作,就不再叙述,有興趣的朋友可以檢視其源碼。

總結

關于控制器的執行選擇Action方法用其詳細示意圖來表示,如下:來源【控制器執行過程】

Web APi之控制器選擇Action方法過程(九)

未完待續:接下來将叙述過濾器,敬請期待。。。。。。。

所有的選擇不過是為了下一次選擇做準備