ASP.NET Web API基础(02)--- 路由和数据传递
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 特性路由
2.2.1 为什么使用特性路由
基于公约的路由的一个优势就是,这个模版被定义在一个单独的地方。这个路由规则一致的被应用于所有的控制器。不幸的是,基于公约的路由是很难支持确切的URI模式,而这个确切的URI模式在Restful APIs中是很普遍的。例如,资源经常包含子资源:客户下了订单,电影有演员,书有作者等等,它是很自然的创建这些URI来反应这些关系:
/customers/1/orders
这种类型的URI在基于公约的路由下是比较难实现的。尽管它能做到,但是如果你有许多控制器或者很多资源类型这种结果不能很好的被扩展。
对于特性路由,它是很容易的为这个URI定义一个路由。你可以简单的添加一个特性到控制器的动作上:
[Route("customers/{customerId}/orders")] public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... } |
除此之外,特性路由还可以应用到如下场景。
- API版本控制
在下面的例子中,"api/v1/products"相对于"api/v2/products"将被路由到不同的控制器。
/api/v1/products
/api/v2/products
- 重载URI片段
在下面的例子中,"1"是一个阶数,而"pending"被映射到集合。
/orders/1
/orders/pending
- 多个参数类型
在下面的例子中,"1"是一个阶数,而"2013/06/16"被指定为一个日期。
/orders/1
/orders/2013/06/16
2.2.2 启用属性路由
要启用属性路由,在配置期间需要调用MapHttpAttributeRoutes 。
public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); // 启用特性路由
config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } |
2.2.3 路由特性
下面是一个使用属性定义的路由示例:
public class OrdersController : ApiController
{
[Route("customers/{customerId}/orders")]
public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
}
这个[Route]属性定义了一个HTTP Get方法。这个字符串"customers/{customerId}/orders"是路由的URI模版。在路由模版中的"{customerId}"参数匹配了在方法中的customerId参数的名称。 例如,这个路由将匹配如下的URI:
http://example.com/customers/1/orders
这个URI模版可以有多个参数:
[Route("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }
任何没有路由属性的控制器方法将使用基于公约的路由。这种方式,你可以结合两种方式在同一个项目中。
2.2.4 路由前缀
通常情况下,在同一个控制器中的所有路由以相同的前缀开头。例如:
public class BooksController : ApiController { [Route("api/books")] public IEnumerable<Book> GetBooks() { ... }
[Route("api/books/{id:int}")] public Book GetBook(int id) { ... }
[Route("api/books")] public HttpResponseMessage CreateBook(Book book) { ... } } |
对于整个控制器你能通过一个[RoutePrefix]属性来设置一个公共的前缀:
[RoutePrefix("api/books")] public class BooksController : ApiController { // GET api/books [Route("")] public IEnumerable<Book> Get() { ... }
// GET api/books/5 [Route("{id:int}")] public Book Get(int id) { ... }
// POST api/books [Route("")] public HttpResponseMessage Post(Book book) { ... } } |
在方法特性上可以用一个波浪符号重写路由前缀:
[RoutePrefix("api/books")] public class BooksController : ApiController { // GET /api/authors/1/books [Route("~/api/authors/{authorId:int}/books")] public IEnumerable<Book> GetByAuthor(int authorId) { ... }
// ... } |
路由前缀可以包含参数:
[RoutePrefix("customers/{customerId}")] public class OrdersController : ApiController { // GET customers/1/orders [Route("orders")] public IEnumerable<Order> Get(int customerId) { ... } } |
2.2.5 可选的URI参数和默认值
你可以通过添加一个问号标记路由参数使成为一个可选的URI参数。如果一个路由参数是可选的,你必须为这个方法参数定义一个默认值。
public class BooksController : ApiController
{
[Route("api/books/locale/{lcid?}")]
public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... }
}
在这个例子中,"/api/books/locale/1033"和"/api/books/locale"将返回同样的资源。或者,你能在路由模版中定义一个默认值,如下:
public class BooksController : ApiController
{
[Route("api/books/locale/{lcid=1033}")]
public IEnumerable<Book> GetBooksByLocale(int lcid) { ... }
}
2.3 请求规则
2.3.1 Get请求规则
1、基础类型参数
// 动作方法代码 [HttpGet] public string GetAllChargingData(int id, string name) { return "ChargingData" + id; } // JS代码 $.ajax({ type: "get", url: "http://localhost:27221/api/Charging/GetAllChargingData", data: { id: 1, name: "Jim", bir: "1988-09-11"}, success: function (data, status) { if (status == "success") { $("#div_test").html(data); } } }); |
2、对象作为参数
// 模型 public class Person { public string Id { get; set; } public string Name { get; set; } public string Gender { get; set; } public int Age { get; set; } } //动作方法 [HttpGet] public string Create([FromUri]Person model) { return "提交的数据:" + model.ID; } // JS代码 $.ajax({ type: "get", url: "http://localhost:27221/api/Person/Create", contentType: "application/json", data: { Id: "1", Name: "Jim", Gender: "男",Age:20 }, success: function (data, status) { if (status == "success") { $("#div_test").html(data); } } }); |
使用Get方式提交对象,必须在动作方法的参数上添加 [FromUri] 特性,否则接受不到数据。
3. 数组作为参数
一般get请求不建议将数组作为参数,因为我们知道get请求传递参数的大小是有限制的,最大1024字节,数组里面内容较多时,将其作为参数传递可能会发生参数超限丢失的情况。
2.3.2 Post请求规则
1. 基础类型参数
post请求的基础类型的参数和get请求有点不一样,我们知道get请求的参数是通过url来传递的,而post请求则是通过http的请求体中传过来的,WebApi的post请求也需要从http的请求体里面去取参数。
- 单个值
// JS代码 $.ajax({ type: "post", url: "http://localhost:27221/api/Charging/SaveData", data: { "": "Jim" }, success: function (data, status) {} }); // 动作方法 [HttpPost] public bool SaveData([FromBody]string NAME) { return true; } |
我们一般的通过url取参数的机制是键值对,即某一个key等于某一个value,而这里的FromBody和我们一般通过url取参数的机制则不同,它的机制是=value,没有key的概念,并且如果你写了key(比如你的ajax参数写的{NAME:"Jim"}),后台反而得到的NAME等于null。
- 多个值
// JS代码 $.ajax({ type: "post", url: "http://localhost:27221/api/Charging/SaveData", contentType: 'application/json', data: JSON.stringify({ NAME: "Jim",DES:"备注" }), success: function (data, status) {} }); // 动作方法 [HttpPost] public object SaveData(dynamic obj) { var strName = Convert.ToString(obj.NAME); return strName; } |
通过dynamic动态类型能顺利得到多个参数,省掉了[FromBody]这个累赘,并且ajax参数的传递不用使用"无厘头"的{"":"value"}这种写法,有一点需要注意的是这里在ajax的请求里面需要加上参数类型为Json,即 contentType: 'application/json', 这个属性。
2.实体作为参数
使用"实体类"接收,客户端既可以用ContentType="application/x-www-form-urlencoded"提交表单,也可以用 ContentType ="application/json"提交, 模型类前面可以加[FromBody],但只能在一个参数前面加。
/动作方法 [HttpGet] public string Create([FromBody]Person model) { return "提交的数据:" + model.ID; } // JS代码 $.ajax({ type: "post", url: "http://localhost:27221/api/Person/Create", data: { Id: "1", Name: "Jim", Gender: "男",Age:20 }, success: function (data, status) { if (status == "success") { $("#div_test").html(data); } } }); |
3. 数组作为参数
// JS代码 var arr = ["1", "2", "3", "4"]; $.ajax({ type: "post", url: "http://localhost:27221/api/Charging/SaveData", contentType: 'application/json', data: JSON.stringify(arr), success: function (data, status) { } }); // 动作方法 [HttpPost] public bool SaveData(string[] ids) { return true; } |
- 实体集合作为参数
var arr = [ { ID: "1", NAME: "Jim", CREATETIME: "1988-09-11" }, { ID: "2", NAME: "Lilei", CREATETIME: "1990-12-11" }, { ID: "3", NAME: "Lucy", CREATETIME: "1986-01-10" } ]; $.ajax({ type: "post", url: "http://localhost:27221/api/Charging/SaveData", contentType: 'application/json', data: JSON.stringify(arr), success: function (data, status) {} });
HttpPost] public bool SaveData(List<Person> models) { return true; } |
总结:推荐用实体接收或用dynamic类型接收,用实体接收,无论是"application/x-www-form-urlencoded"还是"application/json"都能访问;如果用dynamic类型接收,只有"application/json"能访问, 且参数必须是序列化后的字符串
Put和Delete请求与Post请求的规则相同。
在实际开发中,记住Get请求和Post请求的标准用法以及基本的调用规则,注意的是ajax的Get请求,加上一个当前时间或者随机数的参数,避免浏览器缓存。