WebApi有两种路由规则,默认路由和特性路由,都位于WebApiConfig类中的Register方法里进行配置,
(1). config.MapHttpAttributeRoutes(); 代表特性路由
(2). config.Routes.MapHttpRoute(); 代表统一的默认路由
特别注意:特性路由的优先级 大于 默认路由的优先级
2.1 默认路由
2.1.1 默认路由规则
Visual Studio的Web API项目模板就创建了一个默认的路由表:
routes.MapHttpRoute(
name: "API Default",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
这个路由被定义在App_Start目录下的WepApiConfig.cs文件中。
路由表中的每条记录都包含了一个路由模板。Web API的默认路由模板是"api/{controller}/{id}"。在这个模板中,"api"是一个字面路径字段,而{controller}和{id}都是占位符变量。
当Web API框架收到了HTTP请求时,它将会尽力匹配URI到路由表中的路由模板的其中一个。如果没有路由被匹配到,客户端就会收到404错误。例如,以下URI会匹配到默认路由:
1. /api/contacts
2. /api/contacts/1
3. /api/products/gizmo1
然而,以下URI不会匹配到,因为它缺乏"api"字段。
/contacts/1
备注:在路由中使用"api"的原因是为了避免和ASP.NET MVC的路由冲突。也就是说,你可以使用"/contacts"匹配到MVC的路由,使用"api/contacts"匹配到Web API的路由。当然了,如果你不喜欢这种约定,你也可以修改默认路由表。
一旦某个路由匹配到了,Web API就会选择相应的控制器及动作:
- 为了找到控制器,Web API将"Controller"添加到{controller}变量上。
- 为了找到动作,Web API会遍历HTTP方法,然后查找一个其名字以HTTP方法的名字开头的动作。例如,有一个GET请求,Web API会查找以"Get…."开头的动作,比如"GetContact"或"GetAllContacts"。这种方式仅仅适用于GET、POST、PUT和DELETE方法。
-
路由模板的其他占位符变量,比如{id},会被映射到动作的参数。
让我们来看一个示例。假定你定义了如下的控制器:
public class ProductsController : ApiController { public void GetAllProducts() { } public IEnumerable<Product> GetProductById(int id) { } public HttpResponseMessage DeleteProduct(int id){ } } |
这里是一些可能的HTTP请求,以及相应的得到执行的动作:
HTTP Method | URI Path | Action | Parameter |
GET | api/products | GetAllProducts | (none) |
GET | api/products/4 | GetProductById | 4 |
DELETE | api/products/4 | DeleteProduct | 4 |
POST | api/products | (no match) |
注意URI的{id}字段,如果存在,它会被映射到动作的id参数中。在本例,控制器定义了两个GET方法,其中一个包含id参数,而另一个不包含id参数。
同样的,注意到POST请求会失败,因为控制器中并没有定义"POST…"方法。
2.1.2 路由偏差(Routing Variations)
除了使用这些HTTP方法的命名约定,你也可以通过用HttpGet、HttpPut、HttpPost或HttpDelete属性来赋予这些动作来具体地为每个动作设定HTTP方法。
在下面这个例子中,FindProduct方法被映射到GET请求:
public class ProductsController : ApiController { [HttpGet] public Product FindProduct(id) {} } |
为了让一个动作支持多个HTTP方法,或支持除GET、PUT、POST和DELETE之外的HTTP方法,你可以使用AcceptVerbs属性,它以一个HTTP方法列表为参数。
public class ProductsController : ApiController { [AcceptVerbs("GET", "HEAD")] public Product FindProduct(id) { } [AcceptVerbs("Link")] public void MakeCollection() { } } |
上述两个方法可以通过设置请求方式为"GET"、"Head"、"Link"即可请求相应的方法
2.1.3 定义多个路由
默认路由可以同时声明多个,只要里面的name值不一样即可,满足任何一个路由规则都可以访问。
2.2 特性路由
可以通过[Route]和[RoutePrefix]这两个标记来自定义路由规则,[Route]作用于action,[RoutePrefix]作用于Controller, 一旦设置了其中任何一个,默认路由routeTemplate就不起任何作用了。
当[RoutePrefix]和[Route]都存在的话,规则为二者的拼接结合,先[RoutePrefix] 后[Route]
//[RoutePrefix("Api/Third")] public class ThirdController : ApiController { /// <summary> /// 演示多个默认路由的情况 /// 需要把控制器的特性注释掉!需要把WebApiConfig中的 DefaultApi2和DefaultApi3两个路由打开,其他的都注释掉 /// </summary> /// <param name="userName"></param> /// <returns></returns> [HttpGet] public string GetUserName(string userName) { return $"userName的值为{userName}"; } /// <summary> /// 演示淡出的[Route]特性 /// </summary> /// <param name="userName"></param> /// <returns></returns> [Route("myApi/Third/GetM1")] [HttpGet] public string GetM1(string userName) { return $"GetM1您的返回值为:{userName}"; } /// <summary> /// 演示[RoutePrefix]和[Route] /// </summary> /// <param name="userName"></param> /// <returns></returns> [Route("myApi/Third/GetM2")] [HttpGet] public string GetM2(string userName) { return $"GetM2您的返回值为:{userName}"; } /// <summary> /// 演示自定义[Route]的特殊写法 /// </summary> /// <param name="firstName"></param> /// <param name="lastName"></param> /// <returns></returns> [Route("myApi/Third/GetFullName/{firstName}/{lastName}")] [HttpGet] public string GetFullName(string firstName,string lastName) { return $"firstName值为{firstName},lastName值为{lastName}"; } } |
特性路由的作用
(1). 标记特殊的访问路径,该功能有点鸡肋,用的比较少。
(2). 多版本控制的时候会使用. (后面介绍)
2.3 请求规则
2.3.1 Get请求规则
发起请求的方式都为 api/Second/CheckLogin?userName=admin&pwd=123456 这种类型,不管几个参数(1个或多个),接收的时候,如果是分参数接收,没有任何问题, 可以正常接收,但如果是通过实体类来接收,必须在前面加特性[FromUri],否则接收不到。
备注:当然也可以什么参数都不写,通过传统的Request或者Request.QueryString来接受。
例如:
实体类:
public class LoginModel { public string userName { get; set; } public string pwd { get; set; } } |
服务器端代码
/// <summary> /// Get api/Second/CheckLogin1?userName=admin&pwd=123456 /// </summary> /// <param name="userName"></param> /// <param name="pwd"></param> /// <returns></returns> [HttpGet] public string CheckLogin1(string userName, string pwd) { if (userName == "admin" && pwd == "123456") { return "ok"; } else { return "error"; } } #endregion #region 02-用实体接收,加[FromUri]特性 /// <summary> /// Get api/Second/CheckLogin2?userName=admin&pwd=123456 /// </summary> /// <param name="model"></param> /// <returns></returns> [HttpGet] public string CheckLogin2([FromUri]LoginModel model) { if (model.userName == "admin" && model.pwd == "123456") { return "ok"; } else { return "error"; } } #endregion #region 03-用实体接收,不加[FromUri]特性 /// <summary> /// Get api/Second/CheckLogin3?userName=admin&pwd=123456 /// </summary> /// <param name="model"></param> /// <returns></returns> [HttpGet] public string CheckLogin3(LoginModel model) { if (model.userName == "admin" && model.pwd == "123456") { return "ok"; } else { return "error"; } } #endregion #region 04-用dynamic接收,加[FromUri]特性 /// <summary> /// Get api/Second/CheckLogin4?userName=admin&pwd=123456 /// </summary> /// <param name="model"></param> /// <returns></returns> [HttpGet] public string CheckLogin4([FromUri]dynamic model) { if (model.userName == "admin" && model.pwd == "123456") { return "ok"; } else { return "error"; } } #endregion #region 05-没有任何参数,直接用Request相关方法接收 /// <summary> /// Get api/Second/CheckLogin5?userName=admin&pwd=123456 /// </summary> /// <param name="model"></param> /// <returns></returns> [HttpGet] public string CheckLogin5() { var userName = HttpContext.Current.Request["userName"]; var pwd = HttpContext.Current.Request.QueryString["pwd"]; if (userName == "admin" && pwd == "123456") { return "ok"; } else { return "error"; } } |
前端JS代码
//一.下面是Get请求的测试 //1. 分参数接收,可以正常访问 $("#getBtn1").click(function () { $.ajax({ url: "/api/Second/CheckLogin1", type: "get", data: { userName: "admin", pwd: "123456" }, success: function (data) { alert(data); } }); }); //2. 用实体类接收,前面加[FromUrl],可以正常访问 $("#getBtn2").click(function () { $.ajax({ url: "/api/Second/CheckLogin2", type: "get", data: { userName: "admin", pwd: "123456" }, success: function (data) { alert(data); } }); }); //3. 用实体类接收,前面什么不加,报错 // "Message":"出现错误","ExceptionMessage":"未将对象引用设置到对象的实例" $("#getBtn3").click(function () { $.ajax({ url: "/api/Second/CheckLogin3", type: "get", data: { userName: "admin", pwd: "123456" }, success: function (data) { alert(data); } }); }); //4. 用dynamic接收,前面什么不加,报错 // "Message":"出现错误","ExceptionMessage":"未将对象引用设置到对象的实例" $("#getBtn4").click(function () { $.ajax({ url: "/api/Second/CheckLogin4", type: "get", data: { userName: "admin", pwd: "123456" }, success: function (data) { alert(data); } }); }); //5. 后台直接用Request或者Request.QueryString,能正常接收 $("#getBtn5").click(function () { $.ajax({ url: "/api/Second/CheckLogin5", type: "get", data: { userName: "admin", pwd: "123456" }, success: function (data) { alert(data); } }); }); |
示例说明:
(1). 分别调用CheckLogin1、CheckLogin2、CheckLogin3方法, 发现前两个方法都能正常调用,CheckLogin3这个方法报错, 报:"Message":"出现错误","ExceptionMessage":"未将对象引用设置到对象的实例"。
(2). 调用CheckLogin4方法,报与上面相同的错误,证明dynamic类型也不能这么用。
(3). 调用CheckLogin5方法,能正常接收,表明可以通过传统的Request或者Request.QueryString来接受。
2.3.2 Post请求规则
参数一定要用"实体类"接收(即使一个参数也要封装实体类),客户端既可以用ContentType="application/x-www-form-urlencoded"提交表单,也可以用 ContentType ="application/json"提交, 模型类前面可以加[FromBody],但只能在一个参数前面加.
#region 01-单个参数,加[FromBody]特性 [HttpPost] public string Register0([FromBody]string userName) { if (userName == "admin") { return "ok"; } else { return "error"; } } #endregion #region 02-多个参数,分参数接收 [HttpPost] public string Register1(string userName, string pwd) { if (userName == "admin" && pwd == "123456") { return "ok"; } else { return "error"; } } #endregion #region 03-用实体接收,加[FromBody]特性 [HttpPost] public string Register2([FromBody]LoginModel model) { if (model.userName == "admin" && model.pwd == "123456") { return "ok"; } else { return "error"; } } #endregion #region 04-用dynamic接收,加[FromBody]特性 [HttpPost] public string Register3([FromBody]dynamic model) { if (model.userName == "admin" && model.pwd == "123456") { return "ok"; } else { return "error"; } } #endregion #region 05-没有任何参数,直接用Request相关方法接收 [HttpPost] public string Register4() { var userName = HttpContext.Current.Request["userName"]; var pwd = HttpContext.Current.Request.Form["pwd"]; if (userName == "admin" && pwd == "123456") { return "ok"; } else { return "error"; } } #endregion } 服务器端代码 |
//二.下面是Post请求的测试(默认情况下为:ContentType="application/x-www-form-urlencoded"提交表单的形式) //PS: { userName: "admin", pwd: "123456" } 这就是一个JSON对象,也可以叫实体 //1. 一个参数的情况,后台分参数接收,且必须加[FromBody]特性 $("#postBtn0").click(function () { //1.1 正常拼接,可以访问通,但是拿不到userName的值 $.ajax({ url: "/api/Second/Register0", type: "Post", data: { userName: "admin" }, success: function (data) { alert(data); } }); //1.2 没有键,只有值,可以正常访问,能拿到userName的值 //$.ajax({ url: "/api/Second/Register0", type: "Post", data: { "": "admin" }, success: function (data) { alert(data); } }); }); //2. 多个参数的情况,后台分参数接收,正常的键值对提交,无法访问,找不到匹配的资源 $("#postBtn1").click(function () { //访问不通 $.ajax({ url: "/api/Second/Register1", type: "Post", data: { userName: "admin", pwd: "123456" }, success: function (data) { alert(data); } }); }); //3. 一个或多个参数的情况,后台用实体接收,且加[FromBody]特性,可以正常访问,并获取到请求值 $("#postBtn2").click(function () { //情况①,默认的post请求,即ContentType="application/x-www-form-urlencoded"的形式,可以正常请求 //$.ajax({ url: "/api/Second/Register2", type: "Post", data: { userName: "admin", pwd: "123456" }, success: function (data) { alert(data); } }); //情况②,将post请求指定为contentType: 'application/json',且传递的参数格式化成Json字符串,则可以正常访问 $.ajax({ url: "/api/Second/Register2", type: "Post", contentType: 'application/json', data: JSON.stringify({ userName: "admin", pwd: "123456" }), success: function (data) { alert(data); } }); }); //4. 一个或多个参数的情况,后台用dynamic接收,且加[FromBody]特性,需要分情况讨论 $("#postBtn3").click(function () { //情况①,默认的post请求,即ContentType="application/x-www-form-urlencoded"的形式,服务器报500错误 //$.ajax({ url: "/api/Second/Register3", type: "Post", data: { userName: "admin", pwd: "123456" }, success: function (data) { alert(data); } }); //情况②,将post请求指定为contentType: 'application/json',且传递的参数格式化成Json字符串,则可以正常访问 $.ajax({ url: "/api/Second/Register3", type: "Post", contentType: 'application/json', data: JSON.stringify({ userName: "admin", pwd: "123456" }), success: function (data) { alert(data); } }); }); //5. 一个或多个参数的情况,没有参数,通过Request或者Request.Form相关方法可以获取请求值 $("#postBtn4").click(function () { //访问不通 $.ajax({ url: "/api/Second/Register4", type: "Post", data: { userName: "admin", pwd: "123456" }, success: function (data) { alert(data); } }); }); 前端JS代码 |
(1). 当只有一个参数的情况,且加[FromBody]特性,不封装实体,如Register0,正常的键值对 { userName: "admin" }能访问通,但后台拿不到值,只有省掉键名{ "": "admin" },才能正常访问,且后台能拿到值。
(2). 多个参数的情况,后台分参数接收,如Register1,正常的键值对提交,无法访问,提示找不到匹配的资源。
(3). 一个或多个参数的情况,后台用实体接收,如Register2,且加[FromBody]特性,可以正常访问,并获取到请求值。
①:默认的post请求,即ContentType="application/x-www-form-urlencoded"的形式,可以正常请求。
②:将post请求指定为contentType: 'application/json',且传递的实体格式化成Json字符串,则可以正常请求。
(4). 一个或多个参数的情况,后台用dynamic接收,且加[FromBody]特性,需要分情况讨论。
①:默认的post请求,即ContentType="application/x-www-form-urlencoded"的形式,服务器报500错误。
②:将post请求指定为contentType: 'application/json',且传递的实体格式化成Json字符串,则可以正常请求。
阶段总结:后台用实体接收和用dynamic类型接收,用实体接收,无论是"application/x-www-form-urlencoded"还是"application/json"都能访问;如果用dynamic类型接收,只有"application/json"能访问, 且参数必须是序列化后的字符串
(5). 一个或多个参数的情况,没有参数,如Register4,通过Request或者Request.Form相关方法可以获取请求值。
Put和Delete请求与Post请求的规则相同。
在实际开发中,记住Get请求和Post请求的标准用法以及基本的调用规则,注意的是ajax的Get请求,加上一个当前时间或者随机数的参数,使用HttpClient 等需要禁用缓存。