大象醫生公司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。