前言
有些時候我們會發現方法名稱都正確匹配,但就是找不到對應請求介面,所以本文我們來深入瞭解下何時會出現介面請求404的情況。
匹配控制器Action方法(404)
首先我們建立一個web api應用程式,我們給出如下示例控制器程式碼
[ApiController] [Route("[controller]/[action]")] public class WeatherController : ControllerBase { [HttpGet] string Get() { return "Hello World"; } }
當我們進行如上請求時會發現介面請求不到,這是為何呢?細心的你應該可能發現了,對於請求方法是私有,而不是公共的,當我們加上public就可以請求到了介面
[HttpGet("get")] public string Get() { return "Hello World"; }
匹配控制器Action方法本質
經過如上示例,那麼對於Action方法的到底要滿足怎樣的定義才能夠不至於請求不到呢?接下來我們看看原始碼怎麼講。我們找到DefaultApplicationModelProvider類,在此類中有一個OnProvidersExecuting方法用來構建控制器和Action方法模型,當我們構建完畢所有滿足條件的控制器模型後,緊接著勢必會遍歷控制器模型去獲取對應控制器模型下的Action方法,這裡只擷取獲取Action方法片段,原始碼如下:
foreach (var controllerType in context.ControllerTypes) { //獲取控制器模型下的Action方法 foreach (var methodInfo in controllerType.AsType().GetMethods()) { var actionModel = CreateActionModel(controllerType, methodInfo); if (actionModel == null) { continue; } actionModel.Controller = controllerModel; controllerModel.Actions.Add(actionModel); } }
上述紅色標記則是建立Action模型的重點,我們繼續往下看到底滿足哪些條件才建立Action模型呢?
protected virtual ActionModel CreateActionModel(TypeInfo typeInfo, MethodInfo methodInfo) { if (typeInfo == null) { throw new ArgumentNullException(nameof(typeInfo)); } if (methodInfo == null) { throw new ArgumentNullException(nameof(methodInfo)); } if (!IsAction(typeInfo, methodInfo)) { return null; } ...... }
到了這個方法裡面,我們找到了如何確定一個方法為Action方法的源頭,由於該方法有點長,這裡我採用文字敘述來作為判斷邏輯,如下:
protected virtual bool IsAction(TypeInfo typeInfo, MethodInfo methodInfo) { //如果有屬性訪問器(無效) //如果有NonAction特性標識無效) //如果重寫Equals(Object), GetHashCode()方法(無效) //如果實現Dispose方法(無效) //如果是靜態方法(無效) //如果是抽象方法(無效) //如果是建構函式(無效) //如果是泛型方法(無效) //必須為公共方法 return methodInfo.IsPublic; }
如上是從方法定義的角度來過濾而獲取Action方法,除此之外,我們請求方法的名稱還可以自定義,比如通過路由、ActionName特性指定,那麼這二者是否存在優先順序呢?比如如下示例:
[ApiController] [Route("[controller]/[action]")] public class WeatherController : ControllerBase { [HttpGet] [ActionName("get1")] public string get() { var routeValue = HttpContext.Request.RouteValues.FirstOrDefault(); return routeValue.Value.ToString(); } }
我們可以看到此時將以ActionName特性作為方法名稱。所以在上述過濾方法定義後開始構建方法模型,在此之後還會再做一步操作,那就是查詢該方法是否通過ActionName特性標識,若存在則以ActionName特性標識給定的名稱作為請求方法名稱,否則以方法定義名稱為準,原始碼如下:
var actionModel = new ActionModel(methodInfo, attributes); AddRange(actionModel.Filters, attributes.OfType<IFilterMetadata>()); var actionName = attributes.OfType<ActionNameAttribute>().FirstOrDefault(); if (actionName?.Name != null) { actionModel.ActionName = actionName.Name; } else { actionModel.ActionName = methodInfo.Name; }
還沒完,若是將路由特性放到Action方法上,如下,此時請求介面應該是weather/get還是weather/get1呢?
[ApiController] public class WeatherController : ControllerBase { [HttpGet] [Route("weather/get")] [ActionName("get1")] public string get() { var routeValue = HttpContext.Request.RouteValues.FirstOrDefault(); return routeValue.Value.ToString(); } }
此時若我們以weather/get1請求將出現404,還是以路由特性模板給定為準進行請求,但最終會將路由上Action方法名稱通過ActionName特性上的名稱賦值給Action模型中的ActionName進行覆蓋,原始碼如下,所以上述我們得到的action名稱為get1,,當然這麼做沒有任何實際意義。
public static void AddRouteValues(ControllerActionDescriptor actionDescriptor,ControllerModel controller,ActionModel action) { foreach (var kvp in action.RouteValues) { if (!actionDescriptor.RouteValues.ContainsKey(kvp.Key)) { actionDescriptor.RouteValues.Add(kvp.Key, kvp.Value); } } if (!actionDescriptor.RouteValues.ContainsKey("action")) { actionDescriptor.RouteValues.Add("action", action.ActionName ?? string.Empty); } if (!actionDescriptor.RouteValues.ContainsKey("controller")) { actionDescriptor.RouteValues.Add("controller", controller.ControllerName); } }
總結
本文我們只是單獨針對查詢Action方法名稱匹配問題做了進一步的探討,根據原始碼分析,對Action方法名稱指定會做3步操作:第一,根據方法定義進行過濾篩選,第二,若方法通過AcionName特性標識則以其所給名稱為準,否則以方法名稱為準,最終賦值給ActionModel上的ActionName屬性,第三,將ActionModel上的ActionName值賦值給路由集合中的鍵Action。