.NET Core請求控制器Action方法正確匹配,但為何404?

Jeffcky發表於2020-06-24

前言

有些時候我們會發現方法名稱都正確匹配,但就是找不到對應請求介面,所以本文我們來深入瞭解下何時會出現介面請求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。

相關文章