dotNET Core WebAPI 統一處理(返回值、參數驗證、異常)


現在 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 項目,這些基礎處理是必不可少的,有了這些做保障才能專注於業務代碼的編寫。

本文只是拋磚引玉,同樣的思路我們還可以實現更多的功能,例如

  • 如果某些特殊接口需要直接返回值怎么辦?
  • 怎樣記錄耗時較長的接口?
  • 怎樣做接口的驗證?

點擊「閱讀原文」可訪問示例代碼。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM