【导读】提前预祝各位端午节快乐。有时候我们会发现方法名称都正确匹配,但就是找不到对应请求接口,所以本文我们来深入了解下何时会出现接口请求404的情况。
匹配控制器Action方法(404)
首先我们创建一个web api应用程序,我们给出如下示例控制器代码
[ApiController]
[Route("[controller]/[action]")]
public class WeatherController : ControllerBase
{[HttpGet]string Get(){return "Hello World";}
}
当我们进行如上请求时会发现接口请求不到,这是为何呢?细心的你应该可能发现了,对于请求方法是私有,而不是公共的,当我们加上public就可以请求到了接口
匹配控制器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方法名称匹配问题做了进一步的探讨,了解其本质。根据源码分析,对Acion方法名称指定会做以下3步操作:
第一:根据方法定义进行过滤筛选
第二:若方法通过AcionName特性标识则以其所给名称为准,否则以方法名称为准,最终赋值给ActionModel上的ActionName属性
第三:将ActionModel上的ActionName值赋值给路由集合中的键Action