MVC也好,WebAPI也好,据我所知,有部分人是因为复杂的路由,而不想去学的。曾经见过一位程序猿,在他MVC程序中,一切皆路由,url中是完全拒绝"?"和“&”。对此,我也不好说什么,搞不好是个人风格。路由虽然重要,但其实也只是实现MVC的一种手段,并非你用的路由越多,你的url完全不使用参数,你的MVC就越纯正。说实话,笔者一开始对路由也感到恐惧,但是阅读了官方文档后,发现路由其实也可以很简单,关键在于我们如何使用。由于笔者也是初学者,有什么错漏的地方,欢迎大家指正。
本系列文章使用的是vs2017,WebAPI版本是2。本系列大多数内容并非原创,而是来自官网的教程(https://docs.microsoft.com/en-us/aspnet/web-api/overview/getting-started-with-aspnet-web-api/),如果你英文不好,可以将链接中的en-us改成zh-cn。中文版地址:https://docs.microsoft.com/zh-cn/aspnet/web-api/overview/getting-started-with-aspnet-web-api/。不过建议你可以的话,还是看英文版本,因为有些翻译是完全走样。
废话不多说,马上来看看如何新建一个WebAPI项目。打开vs2017,文件-新建-项目
选择空模板,勾选webapi
添加模型类,在右侧资源管理器的Models文件夹上右键-添加-类
类的代码如下:
1 namespace ProductsApp.Models 2 { 3 public class Product 4 { 5 public int Id { get; set; } 6 public string Name { get; set; } 7 public string Category { get; set; } 8 public decimal Price { get; set; } 9 } 10 }
添加空的控制器,在Controllers文件夹上右键-添加-控制器,选择Web API2 控制器 - 空
控制器名称为:ProductsController,代码如下:
using ProductsApp.Models; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Web.Http; namespace ProductsApp.Controllers { public class ProductsController : ApiController { Product[] products = new Product[] { new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } }; public IEnumerable<Product> GetAllProducts() { return products; } public IHttpActionResult GetProduct(int id) { var product = products.FirstOrDefault((p) => p.Id == id); if (product == null) { return NotFound(); } return Ok(product); } } }
这样,一个简单的WebAPI就完成了。完成后,文件结构如下:
调用的话,我们直接使用IIS新建一个网站,端口为1111
打开edge浏览器,输入地址:http://localhost:1111/api/Products,效果如下(注意,每次修改完代码后,需要重新生成一下),
一切都很简单,代码也都不复杂,不过明显有两个问题,一个是,为什么默认就调用了GetAllProducts()了,另一个是,我们明明返回一个列表的,怎么到了客户端就变成json了呢?
第一个问题,就是本文所要研究的问题。说到路由,笔者想起一桩往事。笔者自从接触asp.net以来,一直都在使用webform,即使在MVC大行其道的时候。有一次,接到一个外包项目,利用某开源社区框架做业务的扩展,由于该开源框架用的是MVC,于是就问对方的技术负责人,业务扩展项目是否也必须用MVC,对方答道,用MVC干嘛,绕来绕去不是更麻烦吗?他这句话让我深以为然,大有惺惺相惜之感。我这样说,并非要贬低MVC,更无意挑起MVC与WebForm之争,而是在遍地MVCer的情况下,还能找到WebFormer而高兴。实际上,只要能满足客户要求,谁会在意你用MVC还是WebForm呢。
废话不多说,回到我们的问题,为什么我们输入地址:http://localhost:1111/api/Products,就是在调用GetAllProducts()呢?首先,我们看看App_Start文件夹下的WebApiConfig.cs文件,这个文件是用来配置路由的,代码如下:
public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API 配置和服务 // Web API 路由 config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } }
上面的代码,实质上就是定义了一个默认的路由规则。
我们再看看Global.asax
protected void Application_Start() { GlobalConfiguration.Configure(WebApiConfig.Register); }
很明显,在程序第一次启动的时候,我们的路由规则就被配置加载了。这个默认的规则就是 api/{controller}/{id},其中{controller}匹配一个控制类,例如ProductsController,{id}是可选的,匹配的是public方法(也就是action)的参数。那么,Controller里的public方法,也就是action该如何匹配呢?
从官方的文档可以查到这么几句话:If you are familiar with ASP.NET MVC, Web API routing is very similar to MVC routing. The main difference is that Web API uses the HTTP method, not the URI path, to select the action。意思就是,如果你熟悉MVC,那么API的路由是跟MVC的路由非常相似的。两者之间的不同,是Web API使用http方法,而非URI路径去选择action。这里的action,就是我们Controller里面的public 方法。
也就是说,默认路由api/{controller}/{id},首先匹配一个Controller类,然后用http请求方法匹配Action方法名,最后,用{id},匹配Action中的参数。
http请求方法是什么东西?如果你是传统的asp开发者,或是php开发者,相信都会非常熟悉。例如我们以前写表单html,通常都会这样写:
<form action="form_action.asp" method="get"> .... </form>
里面的method就是我们所说的http请求方法,最常见的就是get和post,get的话,就是将参数放到url上去提交,post的话,参数不会显示在url中。更多的http方法,可以点击这里。
既然知道WebAPI的默认路由,是用http请求方法去匹配控制类中的action,那么就好办了,我们在地址栏输入地址:http://localhost:1111/api/Products ,其实就是相当于在使用get方法与ProductsController中的Action进行匹配了。
然而,上面代码中,两个Action方法都没明确表明是用什么http请求方法,那怎么确定调用哪一个方法呢?get跟GetAllProducts()到底有什么关系呢,以至于GetAllProducts()可以被默认调用?或许有的人已经看出来了,没错,调用的方法GetAllProducts()那么巧,也是以Get开头的。这就是我们匹配的其中一个条件。如果Controller中,public方法的名字(也就是action的名字),是以"Get", "Post", "Put", "Delete", "Head", "Options", 或 "Patch"开头,那么按照约定,该方法(action)匹配对应的http请求方法的调用。如果开头没有上述的关键字,默认表示该方法只支持Post。
例如GetAllProducts()方法,就表示使用http的get方法调用。DeleteProduct(int id)就表示用http的deletel方法调用。由于我们调用的地址是:http://localhost:1111/api/Products,翻译成匹配规则就是,匹配ProductsController中,一个使用get,同时没有参数的Action(也就是public 方法),即GetAllProducts()。如果我们有另一个Get方法,同时也是没有参数的话,就会报错。例如,我们增加一个方法:
public string GetTest() { return "GetTest is called"; }
该方法明显也是匹配Get方法,同时没有参数。重新生成下项目,然后用PostMan调用一下,会发现匹配多个的错误。(PostMan的安装就不说了,很简单,不断下一步。)
我们在原来的基础上,修改一下ProductsController的代码,增加一个方法(红色字体部分),代码如下:
using ProductsApp.Models; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Web.Http; namespace ProductsApp.Controllers { public class ProductsController : ApiController { Product[] products = new Product[] { new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } }; public IEnumerable<Product> GetAllProducts() { return products; } public IHttpActionResult GetProduct(int id) { var product = products.FirstOrDefault((p) => p.Id == id); if (product == null) { return NotFound(); } return Ok(product); } public string SayHello() { return "Hello,World"; } } }
我们在PostMan中输入地址:http://localhost:1111/Products,http请求方法,选择Post,按照我们的规则,应该会调用SayHello方法,实际效果如下:
如果我们将url改成:http://localhost:1111/api/Products/1,但方法依然是Post,那么按照上面说的,先找Post的方法,而三个方法中,只有SayHello符合,虽然后面加了id,并且值为1,由于它是可选的,所以,在post下,调用的依然是SayHello,如下图:
假如,我们将Post方法改为Get,那么就会选择调用我们的GetProduct方法,效果如下:
这个就是WebAPI默认的路由,主要使用Http请求方法来匹配Controller里的Action。而这个匹配的规则,就是使用前缀来决定哪一个最匹配,如果前缀都不是http方法,表示默认匹配Post。是不是感觉很简单呢,如果这样还觉得复杂,没关系,下面还有更简单的方法,就是属性路由。
上面的这种路由匹配规则,其实是属于约定的路由。在调用的时候,你还多多少少需要想一下,究竟url是怎样,会调用哪个方法,会不会有多个方法同时匹配等等。但是使用属性路由,你就可以完全的“精准定位”。属性路由,就是利用特性,重新定义路由。例如:
[HttpPost] [Route("aaa/bbb")] public IEnumerable<Product> GetAllProducts() { return products; }
HttpPost强制了这个方法是需要使用Post来调用,Route强制定义了这个方法的调用路径。虽然这个方法是以Get开头,但是[HttpPost]优先级大于这个约定,我们用PostMan来测试下,我们依然先输入之前的地址:http://localhost:1111/Products,方法为Get,可以看到抛出Not Found这个错误
直到我们将地址改为:http://localhost:1111/aaa/bbb,http方法改为Post的时候,调用才成功。是不是太厉害了,我们可以随便定义访问这个方法的路由,什么约定的规则完全可以置之不理,我们可以完全实现“精准定位”,路由变得不再复杂了,一切都在我们的掌握之中。
那么,我们怎样才能使用这种属性路由呢,首先,我们要打开App_Start文件夹中的WebApiConfig.cs文件,确保一下这句代码存在:
public static void Register(HttpConfiguration config) { // 确保开启了属性路由 config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); }
其次,要引入命名空间:using System.Web.Http;就是这么简单,我们就可以使用属性路由。看到WebApiConfig.cs的代码,有人担心,会不会是因为config.MapHttpAttributeRoutes()代码在前,所以优先级才大于约定的路由呢,我们可以换一下次序,代码改成这样:
public static void Register(HttpConfiguration config) { config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); //次序在后 config.MapHttpAttributeRoutes(); }
结果发现,属性路由的优先级依然大于约定的路由,如果你还担心,大可直接删除约定的路由,将上述的代码改成这样:
public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); }
这样,你就可以完全不用考虑约定的路由对属性路由的影响,一切的映射都在你的牢牢掌握中。不过坏处就是,你的每个方法,都要显式指定http方法和路由。但我觉得这个代价是值得的,因为我们再不用花时间去绕来绕去,再不用担心增加了新方法会不会造成路由冲突等问题,我们只需定好命名规则,保证我们每个方法定义的路由不重复即可。说着说着,我也觉得自己跟文章开头说的那位程序猿一样,在走向一个极端,他是在玩命的用路由,而我是在拼命的阻止路由的多匹配性,追求唯一确定,尽量不让路由造成我的负担,也许,这也是一种风格?
剩下的都是很简单,例如,路由前缀,还是用官方的例子:
public class BooksController : ApiController { [Route("api/books")] public IEnumerable<Book> GetBooks() { ... } [Route("api/books/{id:int}")] public Book GetBook(int id) { ... } [Route("api/books")] [HttpPost] public HttpResponseMessage CreateBook(Book book) { ... } }
每个方法的路由前缀都是“api/books",是不是显得很重复,我们可以将前缀抽取,为整个控制器增加公共的前缀,代码如下:
[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) { ... } // ... }
除此之外,还有路由约束,例如Route("api/books/{id:int}"),表示id是一个32位整数,如果是可选的,可以在后面加"?",例如Route("api/books/{id:int?}")
更详细的使用可以参考官网文档:https://docs.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
有了属性路由,我们甚至可以极端的抛弃约定的路由,从而实现”精准定位“,一切定位都可以牢牢的掌握在自己手中。相信这样,大家应该不会再害怕面对路由了吧。