Fork me on GitHub

ASP.NET Web API 2系列(二):灵活多样的路由配置

1. 导言

路由系统是请求消息进入ASP.NET Web API消息处理管道的第一道屏障,其根本目的在于利用注册的路由对请求的URL进行解析以确定目标HTTPController和Action的名称,以及与目标Action方法某个参数进行绑定的路由变量。

WebService和WCF的协议都是soap协议,数据的序列化和反序列化都是soap的格式。而WebAPI是基于Http协议,请求和返回格式结果默认是 json格式,因此,比WCF更简单、更通用,比 WebService 更节省流量、更简洁。 Web API是在.NET Framework上构建RESTful应用程序的理想平台,为了更清楚弄懂WebAPI的路由配置,我们首先要了解HTTP协议和RESTful架构风格。

2. HTTP协议

HTTP协议(HyperText Transfer Protocol,超文本传输协议)是因特网上应用最为广泛的一种网络传输协议,所有的WWW文件都必须遵守这个标准。我们在这里紧列举和本文关系密切的HTTP请求方法和HTTP状态码。

2.1 HTTP请求方法

根据HTTP标准,HTTP请求可以使用多种请求方法。

HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法,HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法,如下表所示:

序号方法描述
1 GET 请求指定的页面信息,并返回实体主体。
2 HEAD 类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头
3 POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
4 PUT 从客户端向服务器传送的数据取代指定的文档的内容。
5 DELETE 请求服务器删除指定的页面。
6 CONNECT HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
7 OPTIONS 允许客户端查看服务器的性能。
8 TRACE 回显服务器收到的请求,主要用于测试或诊断。

2.2 HTTP状态码

当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前,此网页所在的服务器会返回一个包含HTTP状态码(HTTP Status Code)的信息头(server header)用以响应浏览器的请求。

常见的HTTP状态码如下表:

状态码状态码英文名称中文描述
200 OK 请求成功。一般用于GET与POST请求
301 Moved Permanently 永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替
404 Not Found 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面
500 Internal Server Error 服务器内部错误,无法完成请求

3. RESTful介绍

在介绍RESTful之前,我们需了解什么REST,他有那些特征,以及REST成熟度模型。

3.1 REST介绍

REST是Representational State Transfer的缩写,翻译为表象化状态转变或表述性状态转变,是Roy Fielding博士在2000年他的博士论文中提出来的一种软件架构风格,它包含了一个分布式超文本系统中对于组件、连接器和数据的约束。REST 是作为互联网自身架构的抽象而出现的,其关键在于所定义的架构上的各种约束。只有满足这些约束,才能称之为符合 REST 架构风格。

3.2 REST系统的特征

RESR系统包括6个特征,如下所示:

(1)客户端-服务器结构(Client-Server)

通过一个统一的接口来分开客户端和服务器,使得两者可以独立开发和演化。客户端的实现可以简化,而服务器可以更容易的满足可伸缩性的要求;

(2)无状态(Stateless)

在不同的客户端请求之间,服务器并不保存客户端相关的上下文状态信息。任何客户端发出的每个请求都包含了服务器处理该请求所需的全部信息;

(3)可缓存(Cachable)

客户端可以缓存服务器返回的响应结果。服务器可以定义响应结果的缓存设置。

(4)分层的系统(Layered System)

在分层的系统中,可能有中间服务器来处理安全策略和缓存等相关问题,以提高系统的可伸缩性。客户端并不需要了解中间的这些层次的细节。

(5)按需代码(Code-On-Demand,可选)

服务器可以通过传输可执行代码的方式来扩展或自定义客户端的行为。这是一个可选的约束。

(6)统一接口(Uniform Interface)

该约束是 REST 服务的基础,是客户端和服务器之间的桥梁。该约束又包含下面 4 个子约束。

  • 资源标识符:每个资源都有各自的标识符。客户端在请求时需要指定该标识符。在 REST 服务中,该标识符通常是 URI。客户端所获取的是资源的表达(representation),通常使用 XML 或 JSON 格式。
  • 通过资源的表达来操纵资源:客户端根据所得到的资源的表达中包含的信息来了解如何操纵资源,比如对资源进行修改或删除。
  • 自描述的消息:每条消息都包含足够的信息来描述如何处理该消息。
  • 超媒体作为应用状态的引擎(HATEOAS):客户端通过服务器提供的超媒体内容中动态提供的动作来进行状态转换。

3.3 REST成熟度模型

Richardson 提出的 REST 成熟度模型。该模型把 REST 服务按照成熟度划分成 4 个层次,我们常用到的就是Level1和Level2,如下图所:

具体说明如下: 

Level 0:Web 服务只是使用 HTTP 作为传输方式,实际上只是远程方法调用(RPC)的一种具体形式。SOAP 和 XML-RPC 都属于此类。

Level 1:Web 服务引入了资源的概念。每个资源有对应的标识符和表达。

Level 2:Web 服务使用不同的 HTTP 方法来进行不同的操作,并且使用 HTTP 状态码来表示不同的结果。如 HTTP GET 方法来获取资源,HTTP DELETE 方法来删除资源。

Level 3:Web 服务使用 HATEOAS。在资源的表达中包含了链接信息。客户端可以根据链接来发现可以执行的动作。例如,客户端通过订单资源中包含的链接取消某一订单,GET 请求被发送去获取该订单。HATEOAS 的优点包括无需在客户端代码中写入硬链接的 URL。此外,由于资源信息中包含可允许操作的链接,客户端无需猜测在资源的当前状态下执行何种操作。

 3.4 RESTful

RESTful是一种常见的REST应用,是遵循REST风格的web服务,REST式的web服务是一种ROA(面向资源的架构)。

RESTful资源操作如下表:

http方法资源操作幂等安全
GET SELECT
POST INSERT
PUT UPDATE
DELETE DELETE

注:幂等性:对同一REST接口的多次访问,得到的资源状态是相同的;安全性:对该REST接口访问,不会使服务器端资源的状态发生改变。

RESTful URL请求格式与传统请求格式比较如下表所示:

传统URL请求格式RESTFul请求格式描述
http:/localhost/user/query/1  GET  http:/localhost/user/1  GET 根据用户id查询用户数据
http:/localhost/user/save      POST http:/localhost/user     POST 新增用户
http:/localhost/user/update   POST  http:/localhost/user     PUT 修改用户信息
http:/localhost/user/delete    GET/POST http:/localhost/user     DELETE 删除用户信息

4.Web API路由

路由的目的是用于解析请求的URL来确定Controller和Action。Web API默认路由是通过http的方法(get/post/put/delete)去匹配对应的action,也就是说webapi的默认路由并不需要指定action的名称,当然,WebApi也支持MVC里面的路由机制,但RestFul风格的服务要求请求的url里面不能包含action,所以,在WebApi里面是并不提倡使用MVC路由机制的。下边通过例子介绍Web API路由原理以及使用。

4.1新建空的Web API应用程序

上篇博客介绍了手动搭建基本框架,这次我们通过VS2017提供的模板,新建空的WebAPI应用程序MyWebAPI2,构建过程中仅勾选Web API,如下图所示。

创建完成,应用程序的目录结构如下图所示。

  4.2默认路由

App_Start文件夹下WebApiConfig.cs类用于注册Web API路由,默认路由代码如下:

public static void Register(HttpConfiguration config)
        {
           //默认路由
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }

在Models文件夹增加一个Student类,代码如下:

public class Student
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Sex { get; set; }
        public int Age { get; set; }
        public string Dept { get; set; }
    }

在Controllers文件夹中添加一个Web API控制类(v2.1),命名为StudentController,并修改相关代码,如下所示:

 public class StudentController : ApiController
    {
        private static List<Student> studentList = new List<Student>()
        {
            new Student() {Id = "001", Name = "张三", Sex = "", Age = 19, Dept = "软件学院"},
            new Student() {Id = "002", Name = "李丽", Sex = "", Age = 19, Dept = "资环学院"}
        };
        
        [HttpGet]
        public IEnumerable<Student> Get()
        {
            return studentList;
        }

        [HttpGet]
        public Student Get(string id)
        {
            List<Student> tempList = studentList.Where(p => p.Id == id).ToList();
            return tempList.Count==1?tempList.First():null ;
        }

        [HttpPost]
        public bool Post([FromBody]Student student)
        {
            if (student == null) return false;
            if (studentList.Where(p=>p.Id==student.Id).ToList().Count>0) return false;
            studentList.Add(student);
            return true;
        }

        [HttpPut]
        public bool Put(string id, [FromBody]Student student)
        {
            if (student == null) return false;
            List<Student> tempList = studentList.Where(p => p.Id == id).ToList();
            if (tempList.Count == 0) return false;
            Student originStudent = tempList[0];
            originStudent.Name = student.Name;
            originStudent.Sex = student.Sex;
            originStudent.Age = student.Age;
            originStudent.Dept = student.Dept;
            return true;
        }
[HttpDelete]
public bool Delete(string id) { List<Student> tempList = studentList.Where(p => p.Id == id).ToList(); if (tempList.Count == 0) return false; studentList.Remove(tempList[0]); return true; } }

在实际项目中,增删改查这些操作都是和数据库打交道的,这里为了演示具体实现,用一个静态数组存储数据。运行程序,我们采用Fiddler工具进行测试调用。

测试一:调用Get()方法

功能说明:通过路由解析StudentController中的Get()Action,获取所有学生列表。

URL:http://localhost:52317/api/student;HTTP方法:GET。如下图所示:

测试结果:HTTP状态码为200,获取的JSON数据如下所示:

测试二:调用Get(string id)方法

功能说明:通过路由解析StudentController中的Get(string id)Action,根据学号获取某个学生信息。

URL:http://localhost:52317/api/student/002;HTTP方法:GET

测试结果:HTTP状态码为200,获取的JSON数据如下所示:

测试三:调用Post([FromBody]Student student)方法

功能说明:通过路由解析StudentController中的Post([FromBody]Student student)Action,插入一条数据,其中参数前添加[FromBody]是指该参数不是从URL中获取,而是在RequestBody中获取。

URL:http://localhost:52317/api/student;HTTP方法:POST。在RequestBody输入Json格式的数据,如下图所示:

 测试结果:HTTP状态码为200,返回的数据TRUE。重复测试一,获取所有学生列表如下图所示。

测试四:调用Put(string id, [FromBody]Student student)方法

功能说明:通过路由解析StudentController中的Put(string id, [FromBody]Student student)Action,实现通过学号更新信息,其中学号id是在Url中获取。

URL:http://localhost:52317/api/student/002;HTTP方法:PUT

 测试结果:HTTP状态码为200,返回的数据TRUE。

测试五:调用Delete(string id)方法

功能说明:通过路由解析StudentController中的Delete(string id)Action,删除某学生信息,其中学号id是在Url中获取。

URL:http://localhost:52317/api/student/002;HTTP方法:DELETE

 测试结果:HTTP状态码为200,返回的数据TRUE。

 总结:通过上边测试,我们可以到URL中没有用到Action,默认路由就是通过参数和HTTP方法匹配Controller中的Action。

 4.3自定义路由

在实际项目中,如果http请求的类型相同,且请求参数相同(如Get),这个时候按照默认路由肯定会出问题,具体出现什么问题,我们在这做个测试:

在StudentController中添加一个Action,用于查询某个学院的所有同学,代码如下:

        [HttpGet]
        public IEnumerable<Student> GetByDept(string id)
        {
            List<Student> tempList = studentList.Where(p => p.Dept == id).ToList();
            return tempList;
        }

这时,运行程序,输入URL:http://localhost:52317/api/student/资环学院;HTTP方法:GET。测试结果发现返回的结果null,跟踪测试代码发现该URL调用的Get(string id)这个Action。

那么问题就来了,怎么才能调用这个Action呢?其中有两种方法一种是自定义路由,就像MVC那样在模板中增加Action,另一种为通过Web API路由特性实现。

在App_Start文件夹下WebApiConfig.cs类中添加ActionAPI路由模板,代码如下:

public static void Register(HttpConfiguration config)
        {
           //默认路由
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
            //自定义路由
            config.Routes.MapHttpRoute(
                name: "ActionApi",
                routeTemplate: "api/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }

运行程序,输入URL:http://localhost:52317/api/student/GetByDept/资环学院;HTTP方法:GET。运行结果如下:

注:若出现中文乱码,解决途径请参考Fiddler抓包中文乱码问题

由此可知通过自定义路由可以解决该问题,但是不符合RESTful风格,所有不提倡使用该方法。

4.4特性路由

默认路由解决不了的访问,可以通过Web API的路由特性解决。

启动路由特性:在App_Start文件夹下WebApiConfig.cs类中启用特性路由,代码如下:

public static void Register(HttpConfiguration config)
        {
            //启用Web API特性路由
            config.MapHttpAttributeRoutes();

            //默认路由
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
            //自定义路由
            config.Routes.MapHttpRoute(
                name: "ActionApi",
                routeTemplate: "api/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }

修改StudentController中GetByDept代码如下:

        [Route("student/GetByDept/{dept}")]
        [HttpGet]
        public IEnumerable<Student> GetByDept(string dept)
        {
            List<Student> tempList = studentList.Where(p => p.Dept == dept).ToList();
            return tempList;
        }

运行程序,输入URL:http://localhost:52317/student/GetByDept/软件学院;HTTP方法:GET。运行结果如下:

通过此例,可以看到Web API特性路由非常灵活方便,具体使用要结合实际项目灵活运用。

5. 总结

本文在开始部分介绍了HTTP协议及RESTful架构风格,让我们更深入的了解Web API的设计初衷,又通过实际的例子(文中的代码及结果均通过了测试),让我们掌握了路由配置的各种方法。文中若有不足之处,还望海涵,博文写作不易希望多多支持,后续会更新更多内容,感兴趣的朋友可以加关注,欢迎留言交流!

 

posted @ 2019-06-19 17:23  码探长  阅读(6377)  评论(4编辑  收藏  举报