現在 Web 開發比較流行前后端分離,我們的產品也是一樣,前端使用Vue,后端使用 dotNet Core WebAPI ,在寫 API 的過程中有很多地方需要統一處理:
- 文檔
- 參數驗證
- 返回值
- 異常處理
本文就說說 API 的統一處理這些事。
環境
dotNet Core:2.1
VS For Mac:8.1
文檔
Swagger 是一個 API 文檔生成框架,在非 Core 時代就一直在使用,現在前后端分離的模式下,API 文檔更是非常重要,讓前端開發人員和后端開發人員能更好的溝通和合作,前端開發人員在 Swagger 可以了解到接口的地址、入參、出參,還能模擬調用,非常方便。
安裝
在 VS For Mac 中創建 API 項目 DotNetCoreApiSample ,在依賴項中的 NuGet 上點擊右鍵,選擇添加包,如下圖:
搜索 Swashbuckle.AspNetCore
,選中搜索結果的第一條,點擊「添加包」按鈕進行添加。
配置
Startup 類的 ConfigureServices 方法中添加
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info
{
Version = "v1",
Title = "DotNet Core WebAPI文檔"
});
});
Startup 類的 Configure 方法中添加
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "DotNet Core WebAPI文檔");
});
運行效果
運行 WepAPI 項目,在瀏覽器中輸入 http://localhost:5000/swagger
,效果如下
參數驗證
此處所說的參數驗證指的是實體類型的參數驗證,通過在實體的屬性上添加特性的方式來實現。
簡單實現
創建名為 ValidationDemoController 的 API 類,代碼如下:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace DotNetCoreApiSample.Controllers
{
[Route("api/[controller]")]
public class ValidationDemoController : Controller
{
[HttpPost]
public IActionResult AddUser([FromBody]User user)
{
string errorMessage = string.Empty;
if (!ModelState.IsValid)
{
foreach (var item in ModelState.Values)
{
foreach (var error in item.Errors)
{
errorMessage += error.ErrorMessage + "|";
}
}
}
if(!string.IsNullOrEmpty(errorMessage))
{
return BadRequest(errorMessage);
}
return Ok();
}
}
public class User
{
[Required(ErrorMessage = "用戶Code不能為空")]
public string Code { get; set; }
[Required(ErrorMessage = "用戶名稱不能為空")]
public string Name { get; set; }
[Required(ErrorMessage = "用戶年齡不能為空")]
[Range(1, 100, ErrorMessage = "年齡必須介於1~100之間")]
public int Age { get; set; }
public string Address { get; set; }
}
}
- 實體類屬性使用 Required 等特性需要引用命名空間System.ComponentModel.DataAnnotations
- 除了上面的 Required 和 Range 標記,還有很多實用的標記,詳細參考:https://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations(v=vs.110).aspx
- 上面的示例代碼將錯誤信息的收集寫在了接口方法中,這是一個很不好的做法,僅僅實現了功能,下面將通過過濾器的方式來進行重構,統一處理錯誤信息
重構
添加名為 ValidateModelAttribute 的過濾器類,繼承 ActionFilterAttribute ,代碼如下
namespace DotNetCoreApiSample.Filters
{
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var result = context.ModelState.Keys
.SelectMany(key => context.ModelState[key].Errors.Select(x => new ValidationError(key, x.ErrorMessage)))
.ToList();
context.Result = new ObjectResult(result);
}
}
}
public class ValidationError
{
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Field { get; }
public string Message { get; }
public ValidationError(string field, string message)
{
Field = field != string.Empty ? field : null;
Message = message;
}
}
}
Startup 類的 ConfigureServices 方法中添加下面代碼:
services.AddMvc(options =>
{
options.Filters.Add<ValidateModelAttribute>();
});
使用 Postman 調用結果如下
返回值
返回值的統一處理需要下面幾個步驟:
- 創建統一返回結果的實體類,所有的接口方法都返回固定格式,方便前端統一處理
- 創建過濾器,過濾器用來攔截請求,包裝結果,統一輸出
- Startup 類中進行配置注冊
結果實體類
接口的返回值需要統一的格式,下面的屬性字段是我認為必須要有的
- Result:返回的結果
- Message:出現錯誤或需要提示時的提示文本內容
- Code:調用成功、失敗或出錯時的編碼
- ReturnStatus:用來判斷接口調用狀態的
創建返回結果的實體類 BaseResultModel
public class BaseResultModel
{
public BaseResultModel(int? code = null, string message = null,
object result = null, ReturnStatus returnStatus = ReturnStatus.Success)
{
this.Code = code;
this.Result = result;
this.Message = message;
this.ReturnStatus = returnStatus;
}
public int? Code { get; set; }
public string Message { get; set; }
public object Result { get; set; }
public ReturnStatus ReturnStatus { get; set; }
}
public enum ReturnStatus
{
Success = 1,
Fail = 0,
ConfirmIsContinue = 2,
Error = 3
}
過濾器類
創建名稱為 ApiResultFilterAttribute 的過濾器類,該類繼承 ActionFilterAttribute ,具體代碼如下
public class ApiResultFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
}
public override void OnResultExecuting(ResultExecutingContext context)
{
var objectResult = context.Result as ObjectResult;
context.Result = new OkObjectResult(new BaseResultModel(code:200, result: objectResult.Value));
}
}
在過濾器中將接口的返回值獲取后重新包裝到 BaseResultModel 模型類中進行返回。
Startup 配置
在 Startup 類的 ConfigureServices 方法中添加如下代碼
services.AddMvc(options =>
{
options.Filters.Add<ValidateModelAttribute>();
options.Filters.Add<ApiResultFilterAttribute>();
});
添加示例接口方法
[HttpGet]
public IActionResult GetUserCode()
{
return Ok("oec2003");
}
運行效果
使用 Postman 調用該接口方法,返回結果如下
繼續重構參數驗證
添加了返回值的過濾器類后,調用之前的參數驗證的接口,會發現返回結果如下
{
"code": 200,
"message": null,
"result": [
{
"field": "Age",
"message": "年齡必須介於1~100之間"
}
],
"returnStatus": 1
}
接口會調用兩次過濾器,先調用參數驗證的過濾器,再調用返回值的過濾器,導致驗證失敗的接口返回值狀態也是成功的,所以需要做進一步重構。
1、添加 ValidationFailedResultModel 類
public class ValidationFailedResultModel : BaseResultModel
{
public ValidationFailedResultModel(ModelStateDictionary modelState)
{
Code = 422;
Message = "參數不合法";
Result = modelState.Keys
.SelectMany(key => modelState[key].Errors.Select(x => new ValidationError(key, x.ErrorMessage)))
.ToList();
ReturnStatus = ReturnStatus.Fail;
}
}
public class ValidationError
{
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Field { get; }
public string Message { get; }
public ValidationError(string field, string message)
{
Field = field != string.Empty ? field : null;
Message = message;
}
}
將錯誤信息的收集移到了 ValidationFailedResultModel 類中,所以
ValidateModelAttribute 過濾器也需要調整。
2、修改 ValidateModelAttribute 過濾器,在修改代碼之前,先要添加名為 ValidationFailedResult 的類,該類繼承 ObjectResult ,用做參數驗證的結果收集。
public class ValidationFailedResult: ObjectResult
{
public ValidationFailedResult(ModelStateDictionary modelState)
: base(new ValidationFailedResultModel(modelState))
{
StatusCode = StatusCodes.Status422UnprocessableEntity;
}
}
修改 ValidateModelAttribute 類
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new ValidationFailedResult(context.ModelState);
}
}
3、修改 ApiResultFilterAttribute 過濾器,添加對 ValidationFailedResult 類型的判斷
public override void OnResultExecuting(ResultExecutingContext context)
{
if (context.Result is ValidationFailedResult)
{
var objectResult = context.Result as ObjectResult;
context.Result = objectResult;
}
else
{
var objectResult = context.Result as ObjectResult;
context.Result = new OkObjectResult(new BaseResultModel(code: 200, result: objectResult.Value));
}
}
4、調用參數驗證接口結果如下
異常處理
異常處理和參數驗證的方式基本相同,有以下幾個步驟
1、創建名為 CustomExceptionResultModel 的模型類
public class CustomExceptionResultModel:BaseResultModel
{
public CustomExceptionResultModel(int? code, Exception exception)
{
Code = code;
Message = exception.InnerException != null ?
exception.InnerException.Message :
exception.Message;
Result = exception.Message;
ReturnStatus = ReturnStatus.Error;
}
}
2、創建名為 CustomExceptionResult 的異常結果類
public class CustomExceptionResult:ObjectResult
{
public CustomExceptionResult(int? code, Exception exception)
: base(new CustomExceptionResultModel(code, exception))
{
StatusCode = code;
}
}
3、創建名為 CustomExceptionAttribute 的異常過濾器類,繼承自 IExceptionFilter
public class CustomExceptionAttribute : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
HttpStatusCode status = HttpStatusCode.InternalServerError;
//處理各種異常
context.ExceptionHandled = true;
context.Result = new CustomExceptionResult((int)status, context.Exception);
}
}
4、Startup 配置
在 Startup 類的 ConfigureServices 方法中添加如下代碼
services.AddMvc(options =>
{
options.Filters.Add<ValidateModelAttribute>();
options.Filters.Add<ApiResultFilterAttribute>();
options.Filters.Add<CustomExceptionAttribute>();
});
感興趣的朋友可以在 Github 上下載示例代碼進行調試。
總結
如果是從零開始搭建一個 WebAPI 項目,這些基礎處理是必不可少的,有了這些做保障才能專注於業務代碼的編寫。
本文只是拋磚引玉,同樣的思路我們還可以實現更多的功能,例如
- 如果某些特殊接口需要直接返回值怎么辦?
- 怎樣記錄耗時較長的接口?
- 怎樣做接口的驗證?
點擊「閱讀原文」可訪問示例代碼。