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方法:

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

觀察此結果似乎也沒什么不一樣,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方法用其詳細示意圖來表示,如下:來源【控制器執行過程】

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM