大象医生公司HTTP Service开发指南
1 WebAPI接口实现规范
1.1 GET方法的几种实现情况
1.1.1 基本实现
// GET: api/v1/Products/1455ebe0-832b-46c5-8772-b9483d947a63
/// <summary>
/// 获取特定产品
/// </summary>
/// <param name="id">产品标识</param>
/// <returns></returns>
[Route("~/api/v1/Products/{id}")]
[ResponseType(typeof(ProductOutput))]
public IHttpActionResult GetProduct(Guid id)
{
var result = productService.GetProductById(id);
return Ok(result);
}
规范:
- 首行:以一个具体的URL示例作为首行,URL以HTTP方法名开始,HTTP方法名大写,后跟冒号+空格+URL。要求在运行时使用该URL进行调试能够进入方法体;
- 注释行:建议以获取XX资源或符合XX条件的XX资源作为描述,例如:获取正在促销的产品;
- 路由:在符合默认路由规则时,该部分是可选的。参数部分,作为本资源的标识时,不需要添加资源名称作为前缀。例如,这里不使用productId,而直接使用id。仅当URL中出现其它资源标识时,该其它资源标识需要添加用于限定的资源名称作为前缀。Route特性总是出现在接口方法或ResponseType特性之上。
- 响应类型:当接口方法返回的是IHttpActionResult类型时,Swagger无法推导具体的响应类型,必须使用ResponseType加以声明,以便生成正确的Swagger元数据。ResponseType特性总是出现在接口方法(或SwaggerResponse特性)之上。
- 接口方法声明:方法命名总是使用完整的语义,以便清晰描述本方法的具体功能。Swagger根据方法名生成元数据中的operationId,且operationId不允许重复。所以必须确保语义表述准确以避免重复。然而在本例中,使用GetProduct或GetProducts来返回特定id的产品或全部产品是允许的命名惯例,并不需要特意命名为GetProductById或GetAllProducts。
- 返回结果中间变量:建议定义中间变量var result获取返回结果值,以便于调试,并使最终结果返回代码return Ok()中的参数显得更简洁。
1.1.2 带有多个状态码返回的实现
// GET: api/v1/Products/1455ebe0-832b-46c5-8772-b9483d947a63
/// <summary>
/// 获取特定产品
/// </summary>
/// <param name="id">产品标识</param>
/// <returns></returns>
[Route("~/api/v1/Products/{id}")]
[ResponseType(typeof(ProductOuput))]
[SwaggerResponse(HttpStatusCode.NotFound)]
public IHttpActionResult GetProduct(Guid id)
{
var result = productService.GetProductById(id);
if (result == null)
{
return NotFound();
}
return Ok(result);
}
规范:
- SwaggerResponse:SwaggerResponse特性用于描述接口返回除200以外的其它状态码,这将在元数据中生成对应的描述。建议SwaggerResponse总是出现接口方法之上。入参包括StatusCode, Description和Type。当无法利用StatusCode推断返回意义时,必须使用Description注明描述;当应答体有返回内容时,必须使用Type标注返回类型。
-
使用XML Documentation的response配置节也可以达到与SwaggerResponse类似的效果,如下所示。需要注意的是,两者出现冲突时,SwaggerResponse的优先级更高。
/// <response code="404">Not Found</response>
1.1.3 返回集合结果的实现
// GET: api/v1/Products
/// <summary>
/// 获取所有产品
/// </summary>
/// <returns>产品集合</returns>
[Route("~/api/v1/Products")]
public IEnumerable<ProductOutput> GetProducts()
{
var result = productService.GetProducts();
return result;
}
规范:
- 当返回集合作为结果时,如果结果集为空,不需要返回404,只需返回空结果集。因此,在不需要有其它非200返回的情况下,方法的返回类型可以是集合类型,而非IHttpActionResult。
1.1.4 返回分页集合结果的实现
// GET: api/v1/Products?Pager.PageIndex=1&Pager.PageSize=10
/// <summary>
/// 获取所有产品的分页列表
/// </summary>
/// <returns>产品分页列表</returns>
[Route("~/api/v1/Products")]
public IPagedList<ProductOutput> GetPagedProducts([FromUri] Pager pager)
{
var result = productService.GetPagedProducts(pager);
return result;
}
规范:
- 分页器、筛选器、排序器等,应该通过Query String传递。本例中,Query String中的参数PageIndex和PageSize必须使用Pager前缀,才能绑定至方法参数pager中。
- Pager, IPagedList, SortBy等类型的实现,由Elephant.Core核心库提供。关于如何引用企业核心库可参考《大象医生公司核心库开发与发布说明》。
1.2 POST方法的几种实现情况
1.2.1 创建资源的基本实现
// POST: api/v1/Products
/// <summary>
/// 创建产品
/// </summary>
/// <param name="product">产品</param>
/// <returns></returns>
[Route("~/api/v1/Products")]
[SwaggerResponseRemoveDefaults]
[SwaggerResponse(HttpStatusCode.BadRequest)]
[SwaggerResponse(HttpStatusCode.Created, Type = typeof(ProductOutput))]
public IHttpActionResult PostProduct([FromBody]ProductInput product)
{
if (ModelState.IsValid)
{
return BadRequest(ModelState);
}
var result = productService.CreateProduct(product);
return CreatedAtRoute("DefaultApi", new { id = product.Id }, result);
}
规范:
- 创建资源成功时,应当返回201 Created而不是200 OK。同时,需要在HTTP response headers的location节中返回所创建的新资源对应的URL,同时返回该资源的内容。
- SwaggerResponseRemoveDefaults:可以在生成的元数据中取消默认返回状态码200。此例中,201将成为新的默认返回状态码,显示在Swagger文档中。
- 当有DTO作为输入时,需要对DTO进行模型合法性校验。使用ModelState.IsValid进行校验,并在校验失败时,返回400,同时返回ModelState。ModelState包含了模型校验失败的具体原因。
-
使用CreatedAtRoute可以应用一个现成的路由。本例中使用了名称为DefaultApi的默认路由,此默认路由通常定义在WebApiConfig.cs文件中。但更多的情况是,使用一个已存在的自定义路由,此时需要将这一路由声明为一个具名路由。例如,前例中GET方法的路由,可以改写为如下方式,使之成为一个具名路由,并使用GetProductById这一名称(替换本例中的DefaultApi),此路由即用于创建新资源的URL。本例中new { id = product.Id }中的参数id,将会替换该路由中的参数id,而result则作为Content中的内容返回。
[Route("~/api/v1/Products/{id}", Name = "GetProductById")]
-
本例中,方法的入参product应当为一个作为Input的DTO,而result变量应当为一个作为Output的DTO。相关的DTO的命名以Input或Output作为后缀。后缀不并仅限于使用Input和Output,也可以使用Create或Update等,进一步区分用途。
1.2.2 其它非幂等性操作的实现
// POST: api/v1/Products/Last/Remove
/// <summary>
/// 删除最后一个产品
/// </summary>
/// <returns></returns>
[Route("~/api/v1/Products/Last/Remove")]
[SwaggerResponseRemoveDefaults]
[SwaggerResponse(HttpStatusCode.NotFound)]
[SwaggerResponse(HttpStatusCode.NoContent)]
public IHttpActionResult RemoveLastProduct()
{
var result = productService.RemoveLastProduct();
if (!result)
{
return NotFound();
}
return StatusCode(HttpStatusCode.NoContent);
}
规范:
- 本例中,删除最后一个产品的操作满足非幂等性,即多次操作可能删除多个不同的产品,使得产生不同的系统状态。对于本来在语义上有可能使用PUT或DELETE的操作,如果其满足非幂等性,都应当使用POST方法。
- 当方法代表的操作不再用于创建资源时,使用操作名词本身替代Post用于方法的命名,因此本例中的方法不是PostRemoveLastProduct。
- 当使用非Post前缀的方法名称时,按照命名惯例,Web API将默认该方法为Post方法。因此本例中不需要加HttpPost特性(其它HTTP方法需要显式声明)。
- 当操作不包含返回值时,应返回204 NoContent。
1.3 PUT方法的几种实现情况
1.3.1 更新资源的基本实现
// PUT: api/v1/Products/1455ebe0-832b-46c5-8772-b9483d947a63
/// <summary>
/// 更新产品
/// </summary>
/// <param name="id">产品标识</param>
/// <param name="product">产品</param>
/// <returns></returns>
[Route("~/api/v1/Products/{id}")]
[SwaggerResponseRemoveDefaults]
[SwaggerResponse(HttpStatusCode.NotFound)]
[SwaggerResponse(HttpStatusCode.NoContent)]
public IHttpActionResult PutProduct(Guid id, [FromBody]ProductInput product)
{
if (ModelState.IsValid)
{
return BadRequest(ModelState);
}
var result = productService.GetProductById(id);
if (result == null)
{
return NotFound();
}
productService.UpdateProduct(id, product);
return StatusCode(HttpStatusCode.NoContent);
}
规范:
- 根据官方的定义,使用PUT方法,意味着或者不存在该资源,则创建一个新的资源;或者存在该资源,使用新的资源完整替换原有资源。无论哪一种,多次操作后得到的系统状态的结果是完全一致的,因而符合幂等性。在本例中,并没有因为不存在该资源而创建新的资源,而是返回了404 NotFound,但仍然没有违反幂等性。具体采用哪一种策略,应该根据实际应用场景需要决定。如果创建新资源,则应当返回201 Created。
- 资源的标识在方法中,通过参数id独立传递,ProductInput并不包含id属性。在更新前,需要判断id对应的资源是否存在,然后进行进一步的操作。
1.3.2 其它幂等性操作的实现
// PUT: api/v1/Products/1455ebe0-832b-46c5-8772-b9483d947a63/Disable
/// <summary>
/// 下架产品
/// </summary>
/// <param name="id">产品标识</param>
/// <returns></returns>
[HttpPut]
[Route("~/api/v1/Products/{id}/Disable")]
[SwaggerResponseRemoveDefaults]
[SwaggerResponse(HttpStatusCode.NotFound)]
[SwaggerResponse(HttpStatusCode.NoContent)]
public IHttpActionResult DisableProduct(Guid id)
{
var result = productService.GetProductById(id);
if (result == null)
{
return NotFound();
}
productService.DisableProduct(id);
return StatusCode(HttpStatusCode.NoContent);
}
规范:
- 当方法代表的操作不再用于更新资源时,使用操作名词本身替代Put用于方法的命名。由于命名惯例决定了默认HTTP方法是POST,因此这里需要显式标识HttpPut特性。
- 由于多次下架同一产品的结果是一致的,所以本例的操作符合幂等性。
1.4 DELETE方法的几种实现情况
1.4.1 删除资源的基本实现
// DELETE: api/v1/Products/1455ebe0-832b-46c5-8772-b9483d947a63
/// <summary>
/// 删除产品
/// </summary>
/// <param name="id">产品标识</param>
/// <returns></returns>
[Route("~/api/v1/Products/{id}")]
[SwaggerResponseRemoveDefaults]
[SwaggerResponse(HttpStatusCode.NotFound)]
[SwaggerResponse(HttpStatusCode.NoContent)]
public IHttpActionResult DeleteProduct(Guid id)
{
var result = productService.GetProductById(id);
if (result == null)
{
return NotFound();
}
productService.DeleteProduct(id);
return StatusCode(HttpStatusCode.NoContent);
}
规范:
- DELETE方法应当满足幂等性。本例中,多次删除操作都将使系统状态改变为产品已删除的状态,因此虽然状态码的返回存在多个可能,但系统状态始终是一致的。
1.5 幂等性的界定
幂等性应关注发送多个重复的操作,系统状态的结果是否始终一致,而不是关注接口返回是否一致。系统状态在这里主要指的是业务状态,而不包括那些业务之外额外生成的状态变更,例如日志、统计数据等。符合幂等性的写操作,可以使用POST、DELETE方法;否则,即使语义上属于更新或删除操作,也应当使用POST方法,以符合HTTP协议规定,确保基于协议之上的一些外部行为的结果是符合预期的。
1.5 状态返回码
- 上述示例中,操作成功后没有返回内容的,200 OK和204 NoContent在大多数情况下都是通用的。但为了编程一致上的考虑,统一使用204 NoContent返回。
- 当违反业务规则约束使得操作失败时,应当返回409 Conflict。