ASP.NET Core学习之Restful

1.Restful

  • 什么是Restful
    • Web API开发有两种风格:面向过程的(简称RPC)、面向REST的(简称REST)
  • 在RPC风格的WebAPI中,我们通过**“控制器/操作方法”**的形式来调用服务器端的方法,把服务器端的代码当成方法去调用。
    • 这种风格的接口可能会用POST请求处理所有的操作方法,无论是获取、新增、更新还是删除数据,这样的接口只是把HTTP当成一个传输数据的通道,而不关心HTTP谓词的语义
    • 在这样的接口中,我们通过QueryString(查询字符串)或者请求报文体来为服务器传递数据。只要服务器端能够正常完成客户端请求的处理,服务器就会统一返回200的HTTP状态码。对于逻辑上的错误,返回的HTTP状态码也是200,只不过在响应报文体中通过不同的错误码来表示,比如“获取的用户不存在”的错误码为1,“没有权限获取这个用户”的错误码为2。
    • 在RPC风格的接口中,当需要加载所有用户的时候,我们就向/Persons/GetAll这个路径发送GET请求;
    • 当需要加载id=8的用户的时候,我们就向/Persons/GetById?id=8这个路径发送id=8 GET请求;
    • 当需要更新id=8的用户信息的时候,我们就向/Persons/Update这个路径发送POST请求,并且把新的用户信息以JSON格式放到请求报文体中;
    • 当需要新增一个用户的时候,我们就向/Persons/AddNew这个路径发送POST 请求,并且把要新增的用户信息以JSON格式放到请求报文体中;
    • 当需要删除id=8的用户信息的时候,我们就向/Persons/DeleteByld/8这个路径发送POST请求。
    • 由此可见,在RPC风格的系统中,URL(uniform resource locator,统一资源定位符)中包令以名词形式描述的资源(比如 Persons)和以动词形式描述的动作(比如AddNew)。
  • 在REST风格的WebAPI中,接口把服务器端当成资源来处理。
    • REST风格的接口按照HTTP设计之初的语义来使用HTTP,把系统中的所有内容都抽象为资源,所有对资源的操作都是无状态的且可以通过标准的HTTP谓词来进行。
  • HTTP的设计哲学包含以下几个重点内容。
    • 在HTTP中,我们要通过URL进行资源的定位。比如要获取id=888的用户信息,我id=888们就向/user/888 这个路径发送请求,而要获取 id=888的用户的订单列表,我们就向/user/888/orders这个路径发送请求。
    • 在HTTP中,不同的请求方法(又被叫作请求谓词)有不同的含义。主要的谓词有GET、POST、PUT、DELETE、PATCH、OPTIONS等,我们这里只讨论用得比较多的GET、POST、PUT和DELETE。**不同谓词有不同的用途,获取资源用GET、新增资源用POST、整体更新(如果不存在则创建)资源用PUT、删除资源用DELETE。**我们不应该错误地使用谓词,比如删除一个资源的时候,我们不能使用GET请求,而应该使用DELETE请求。
    • 在HTTP中,DELETE、PUT、GET请求应该是幂等的,而POST则不是幂等的。
      • 所谓“幂等”指的是:**对于一个接口采用同样的参数请求一次和请求多次的结果是一致的,不会因为多次请求而产生副作用。**例如,“发表评论”功能需要是幂等的,用户填写评论并且单击【发布】按钮,评论被插入数据库,但是在返回结果的时候由于网络等问题,用户没有看到“发布成功”的消息,因此用户又单击了一次【发布】按钮,如果最终用户只发布了一条评论,那么这个操作就是幂等的,而如果用户连续单击两次【发布】按钮就发布了两条评论,这个操作就不是幂等的。
      • 由于网络环境存在不稳定性,当遇到网络故障导致请求失败时,如果接口是幂等的,系统就可以向接口重新发送请求,而不用担心重复发送请求带来副作用。
    • 在HTTP中,**GET请求的响应是可以被缓存的,而DELETE、PUT、POST请求的响应是不可以被缓存的。**客户端、网关等可以根据情况对GET请求的响应进行缓存,从而提升性能。
    • 在HTTP中,服务器端要通过状态码来反映资源获取的结果。
      • 比如,客户端要获取id=8的用户,如果要获取的用户不存在,则服务器返回的状态码为404,而如果当前客户端没有权限获取这个用户,服务器返回的状态码为403。
      • 再如,对于新增用户请求,如果新增成功,服务器返回的状态码为201。**在一个Restful风格的WebAPI系统中,每一个控制器都是对一类资源的操作的集合,每个操作方法都被不同的HTTP谓词触发。**例如,我们把系统中的“用户”抽象成Person资源,就可以开发如代码6-6所示的用户控制器。
[Route("api/[controller]")]
    [ApiController]
    public class TestController : ControllerBase
    {
        [HttpGet]
        public Person GetPerson()
        {
            return new Person("杨中科", 18);
        }

        [HttpPost]
        public string[] SaveNote(SaveNoteRequest req)
        {
            System.IO.File.WriteAllText(req.Title+".txt", req.Content);
            return new string[] { "OK",req.Title};
        }

        [HttpPost]
        public void SavePerson(Person person) { }

        [HttpDelete("{id}")]
        public void DeletePerson(long id) { }
    }

2.Restful的优缺点

Restful 更符合 HTTP设计的语义,因此我们把接口设计成Restful风格有如下的优点

  1. 所有的资源都尽量通过URL来表示,避免通过QueryString、报文体来对资源进行定位,这样URL的语义性更清晰。
  2. 对所有类型资源的新增、删除、修改、查询操作都统一为向资源发送POST、DELETE、PUT、GET请求,接口统一且具有自描述性,减少了开发人员对接口文档的依赖性。
  3. 对于GET、PUT、DELETE 等幂等的操作,网关、网络请求组件等可以对失败的请求自动重试。
  4. 网关等可以对GET请求进行缓存,能够提升系统的访问速度,而且降低服务器的压力
  5. 通过HTTP状态码反映服务器端的处理结果,能够统一错误码,避免自定义错误码带来的不统一的问题。客户端也可以根据错误码进行统一处理,比如对于403状态码,客户端统一提示用户去登录。
  6. 网关等系统可以根据状态码来分析系统的访问数据,比如可以根据HTTP状态码分析有多少成功的请求,有多少失败的请求。

Restful 风格的接口虽然有很多优点,但是也有如下的缺点:

  1. 真实系统中的资源非常复杂,很难清晰地进行资源的划分,因此Restful 风格对设计人员的IT技能和业务知识的水平要求都非常高。
  2. **真实系统中的业务很复杂,并不是所有的操作都能简单地对应到PUT、GET、DELETE,POST上。**而且对于同一个资源的同一个HTTP谓词有时候有多个业务逻辑,比如“删除id=8的用户”和“删除username=yzk的用户”这两个逻辑,如果按照Restful风格,我们就要设计“users/8”和“users/username/yzk”这两个地址,这样的地址会让开发人员迷惑,开发人员可能习惯的仍然是“users/DeleteByld/8”“users/DeleteByld?id=8” “users/DeleteByUserName/yzk"“users/DeleteByUserName?username=yzk”等能清晰地反映操作意图的地址。
  3. 真实系统是在不断进化的,**一个操作最开始的时候被设计为幂等的PUT,但是后来的版本又修改了逻辑,可能该操作就变成了不幂等的。**如果调用者继续对这个操作进行重试,可能会有副作用。
  4. 在Restful中,资源尽量通过URL来定位,要尽量避免使用QueryString 及请求报文体传递数据。比如要查询id=8的用户,我们就要使用“Users/8”这样的地址;要查询name=yzkid=8的用户,我们就要使用“users/name/yzk”这样的地址;如果要查询班级编号为8并且年龄等于18岁的学生,我们就要使用“students/class/8/age/18”这样的地址。
    1. 这样的地址格式是英文的表达方式,并不符合中文的表达习惯,因此这样写会让很多开发人员迷惑,习惯使用中文的开发人员可能更习惯通过QueryString或者通过请求报文体传递数据。
  5. HTTP状态码的个数是有限的,特别是用于表示业务相关的错误码主要在4x状态码段中,而业务系统中的错误非常复杂,仅通过HTTP状态码来反映错误有时候会无法满足要求。
  6. 有一些宽带运营商、路由器、浏览器会对非200状态码的响应报文进行篡改。比如,作者就遇到过宽带运营商把状态码为404的响应报文篡改为了广告内容。当然,目前大部分正规的系统都是通过HTTPS部署网站的,因此不太可能遇到这个问题。
  7. 有的客户端是不支持PUT、DELETE 请求的,比如旧版本的支付宝小程序、一些旧版浏览器等就不支持PUT、DELETE请求。据说,还有开发人员遇到过一些地区的小运营商的网络设备不支持PUT、DELETE请求的情况。

REST是比较学术化的概念,它只是一个参考的风格,并不是一个必须遵守的规范。尽管Restful 接口有很多优点,但是也有很多缺点。项目开发中我们需要做取舍,并不一定需要严格遵守Restful风格。

AWS、ElasticSearch等的接口比较接近于Restful风格,不仅因为这些系统的开发人员是使用英语的,更因为这些系统的业务资源比较固定、业务流程变化不大。而很多互联网系统、业务系统比这些系统复杂很多,而且面临的使用场景也更加复杂,因此即使是多互联网系统、业务系统比这些系统复杂很多,而且面临的使用场景也更加复杂,因此即使是腾讯、阿里巴巴等大公司的业务相关接口,很多也不是完全遵守Restful 风格的。

**REST 概念是用来指导我们设计接口的,而不是给开发带来麻烦的,不能因为要遵守Restful 风格而影响开发进度及系统的稳定。**如果项目的资源及业务流程像AWS、ElasticSearch等比较清晰、固定,并且开发团队中有对REST理解非常深入的开发人员,那么我们可以严格遵守Restful风格。但是对于大部分系统,业务资源和业务流程都是非常复杂的,业务需求的变动也是比较频繁的,而且大部分项目的开发人员的技术是参差不齐的,如果严格遵守Restful规范,会使得新员工的培养周期变长。因此在进行项目开发的时候,需要根据项目特点、公司人员等多方面情况,确定一个符合项目情况的定制版Restful规范。

3.Restful参数传递

在进行Restful接口设计的时候,我们需要考虑如何给服务器端传递参数。
在给服务器端传递参数的时候,有URL、QueryString、请求报文体3种主要方式。

  • 通过URL传递更符合 Restful规范,但是如果要传递的参数太多或者内容太长的话,通过URL传递的方式就不太适合。
  • 通过QueryString传递比较灵活,但是同样不适合传递太长的内容。
  • 通过请求报文体传递参数不限制内容的长度,而且通过JSON 可以传递复杂的格式,但是只有POST、PUT支持请求报文体。
  • 按照RFC 7231标准,GET、DELETE请求中的报文体是未定义的语义,有的网络设备、软件、开发包会忽略 GET、DELETE 中的报文体,因此我们可以认为GET、DELETE请求不能使用报文体。

在REST中,这3种传递参数方式的意义是不同的。

  • 通过URL传递的参数主要用于对资源进行定位,比如资源的ID、资源的分类ID等。
  • 对于额外的数据,比如分页的页码等应该通过QueryString 传递。
  • 请求报文体应该用来供PUT和POST提交主要数据,比如要更新id=8的用户的姓名为“杨中科”,我们应该向/Users/8这个路径发送PUT请求,且请求的报文体为{name:"杨中科"},这样把要更新的用户的id=8放到URL中来对资源进行定位,通过请求报id=8文体来告诉服务器具体的更新数据。

对于Web API参数的传递建议如下:

  1. 对于保存、更新类的请求一般都是使用POST、PUT请求,把全部参数都放到请求报文体中;
  2. 对于DELETE请求,要传递的参数就是一个资源的ID,因此把参数放到QueryString中即可;
  3. 对于GET请求,一般参数的内容都不会太长,因此统一通过QueryString传递参数就可以;
  4. 当然对于极少数参数内容超过URL限制的请求,由于GET、PUT请求都是幂等的,因此把请求改成通过PUT请求,然后通过报文体来传递参数

4.Restful 实现指南

我们已经了解了REST的概念,以及在项目中如何根据项目情况合理地应用REST概念

  • 对资源的操作使用RPC风格,也就是所有操作的路径为“[controller]/[action]”这样的模式,比如增加用户的路径是“Users/AddNew”、获取所有用户的路径是“Users/GetAll”、根据ID删除用户的路径是“Users/DeleteByld”。接口风格统一,更容易使用和理解。
  • 对于可以缓存的操作,使用GET请求;对于幂等的更新操作,使用PUT请求;对于幂等的删除操作,使用DELETE请求;对于其他操作,都使用POST请求。如果公司里面调用接口的开发人员对PUT、DELETE这样的请求抵触或者需要兼容不支持PUT、DELETE的客户端环境的话,也可以对可以缓存的操作使用GET请求,其他操作都用POST请求。
  • 参数的传递方式统一化。作者建议采用如下的规范:保存、更新类的请求使用POST、PUT 请求,把全部参数都放到请求报文体中;对于GETDELETE 请求,把参数放到QueryString 中。
  • 对于业务错误,服务器端返回合适的4xx段的HTTP状态码,不知道该选择哪个状态码就用400;同时,在报文体中通过code参数提供业务错误码及错误消息。
  • 如果请求的处理执行成功,服务器端返回值为200的HTTP状态码,如果有需要返回给客户端的数据,则服务器端把这些数据放到响应报文体中。

微软为Web API提供的模板代码、示例代码大部分都严格遵守Restful风格,如果把它们改造成RPC风格,需要做如下操作 :

  • 控制器上添加的[Route("[controller]")]改为[Route("[controller]/[action]")],这样[controller]就会匹配控制器的名字,而[action]就会匹配操作方法的名字。
  • 通过不同的路由配置,ASP.NET Core中的控制器可以支持多个同名的重载操作方法,但是配置不当会导致开发人员认为一个URL请求应该调用A1方法,但是却调用了A2方法。因此为了避免麻烦,我们强制要求控制器中不同的操作用不同的方法名。
  • [HttpGet]、[HttpPost]、[HttpDelete]、[HttpPut]这些 Attribute 添加到对应的操作方法上。这不仅会帮助接口开发人员明确操作方法接收的请求类型,更能帮助 Swagger+OpenAPI生成文档。

有一个需要注意的问题,在ASP.NET Core Web API中,如果控制器中存在一个没有添加[HttpGet]、[HttpPost]等的public方法,Swagger就会报错“Failed to load APIdefinition.”
对于这样的方法,请把[ApiExplorerSettings(IgnoreApi=true)]添加到方法上,从而告知Swagger忽略这个方法。

你可能感兴趣的:(C#自学,restful,asp.net,学习)