相关的一篇文章:RESTful API URI 设计的一些总结。
问题场景:判断一个资源(Resources)是否存在,URI 该如何设计?
应用示例:判断 id 为 1 用户下,名称为 windows 10 的产品是否存在?
如果这个问题出现在 MVC 项目中,我想我们一般会这样设计:
public class PRoductService{ public async Task<bool> IsExist(int userId, string productName) { .... }}
看来没什么问题,的确也没什么问题,那如果把这部分代码搬到 asp.net WebAPI 项目中实现,会是怎样呢?我们来看一下:
public class ProductsController : ApiController{ [HttpGet] [Route("api/products/isexist/{userId}/{productName}")] public async Task<bool> IsExist(int userId, string productName) { ... }}
我想你应该发现一些问题了,这种写法完全是 MVC 的方式,但并不适用于 WebAPI,主要有三个问题:
对于上面的三个问题,我们分别来探讨下。
首先,我们知道在 REST API 中,URI 代表的是一种资源,它的设计要满足两个基本要求,第一名词而非动词,第二要能清晰表达出资源的含义,换句话说就是,从一个 URI 中,你可以很直接明了的知道访问的资源是什么,我们再来看我们设计的 URI:
api/products/isExist/{userId}/{productName}
这是什么鬼???这种设计完全违背 URI 原则,首先,我们先梳理一下,我们想要请求的资源是什么?没错,是产品(Products),但这个产品是某一个用户下的,所以用户和产品有一个上下级关系,访问产品首先得访问用户,这一点要在 URI 中进行体现,其次,我们是获取产品?还是判断产品是否存在?这个概念是不同的,产品的唯一标识和用户一样,都是 id,在 URI 的一般设计中,如果要访问某一唯一标识下的资源(比如 id 为 1 的 product),会这样进行设计:api/products/{id}
,HttpClient 请求中会用 HttpGet 方法(api/products/1),这样我们就可以获得一个 id 为 1 的 product,但现在的场景是,获取产品不通过唯一标识,而是通过产品名称,难道我们要这样设计:
api/products/{productName}
咋看之下,这样好像设计也没什么问题,但总觉得有些不对劲,比如如果再加一个产品大小,难道要改成这样:api/products/{productName}/{productSize}
,这种设计完全是不恰当的,上面说到,URI 代表的是一种资源,通过 URI 获取资源的唯一方式是通过资源的唯一标识,除此之外的获取都可以看作是对资源的查询(Query),所以,针对我们的应用场景,URI 的设计应该是这样(正确):
api/users/{userId}/products: api/users/1/products?productName=windows 10
上面的 URI 清晰明了的含义:查询 id 为 1 用户下名称为 windows 10 的产品。
对于 IsExist 的命名,如果没有很强的强迫症,其实也是可以接受的,因为 WebAPI 的 URI 并不会像 MVC 的 Route 设计那样,在访问的时候,URL 一般会默认 Action 的名字,所以,在 WebAPI Action 设计的时候,会在 Action 前面加一个 Route 属性,用来配置 URI,也就是说每一个 Action 操作会对应一个 URI 请求操作,这个请求操作也就是 HTTP 的常用方法。
如果我们想把 IsExist 改掉,那用什么命名会好些呢?先回忆一下,我们在使用 Visual Studio 创建 ASP.NET WebAPI 项目的时候,VS 会自动创建一些示例 Action,我们看看能不能从那里得到一些线索:
public class ValuesController : ApiController{ // GET api/values public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } // GET api/values/5 public string Get(int id) { return "value"; } // POST api/values public void Post([FromBody]string value) { } // PUT api/values/5 public void Put(int id, [FromBody]string value) { } // DELETE api/values/5 public void Delete(int id) { }}
上面是 Values 资源的一些 Action 实现,我们可以看到,Action 的命名和 HTTP 方法一样,比如 Get 就是 Get,而不是 GetById,Get 是动词,表示它对资源的一种操作,具体是通过什么进行操作?在参数中可以很直观的进行反应,一般会在 HelpPage 中进行注释说明。
IsExist 的含义还是判断资源是否存在,其本质上来说就是去获取一个资源,也就是 Get 操作,所以,在 WebAPI Action 中对此的命名,我们直接使用 Get 会好一下,或者使用 Exist。
bool 一般是用在项目方法中的返回值,如果用在 HTTP 请求中,就不是很恰当了,我先贴出几篇文章:
上面除去第一篇文章,其他文章都是在讨论:检查一个资源是否存在,REST API 该如何设计(HTTP status code)?客户端获取服务的响应不是通过 bool,而是通过 HTTP 状态码,主要设计三个:404、204 和 200:
204 和 404 有所不同的是,204 表示资源存在,但是为空,404 代表的是原始资源本身就不存在,并且通过唯一标识查询不到,而 204 更多的是表示,在一定条件下的资源不存在,但可以通过唯一标识查询到,所以如果资源不存在返回 204 不恰当,wikipedia 中 200 和 404 详细说明:
HTTP status code 简要说明:
在上面文章中,有一段很有意思的对话(请略过翻译):
搜刮到的一张 HTTP status code 示意图(点击查看大图):
针对一开始的应用问题,最终完善代码:
public class ProductsController : ApiController{ [HttpGet] [Route("api/users/{userId}/products")] public async Task<HttpResponseMessage> Get(int userId, string productName) { if (true) { return Request.CreateResponse(HttpStatusCode.OK); } else { return Request.CreateResponse(HttpStatusCode.NotFound); } }}public class WebApiTest{ [Fact] public async Task Exists_Product_ByProductName() { using (var client = new HttpClient()) { client.BaseAddress = new System.Uri(Base_Address); var response = await client.GetAsync("/api/users/1/products?productName=windows 10"); //var requestMessage = new HttpRequestMessage(HttpMethod.Head, "/api/users/1/products?productName=windows 10"); //var response = await client.SendAsync(requestMessage);//更好的调用方式,只请求HEAD。 Console.WriteLine(response.StatusCode); Assert.True(response.IsSuccessStatusCode); } }}
新闻热点
疑难解答