[Web API] 如何讓 Web API 統一回傳格式以及例外處理


當我們在開發 Web API 時,一般的情況下每個 API 回傳的資料型態或格式都不盡相同,如果你的專案從頭到尾都是由你一個人獨力完成,那也許還可以說聲「阿密陀佛」,但如果是有其他人需要和你共享你的 Api ,而回傳的資料格式又不一樣,相信是會增加使用者的困擾,也大大增加了程式的複雜度與維護上的難度。所以本篇也紀錄一下自己在實作上的經驗,一方面留個紀錄也希望幫助更多人,廢物不多說我們開始吧!

前言

當我們在開發 Web API 時,一般的情況下每個 API 回傳的資料型態或格式都不盡相同,如果你的專案從頭到尾都是由你一個人獨力完成,那也許還可以說聲「阿密陀佛」,但如果是有其他人需要和你共享你的 Api ,而回傳的資料格式又不一樣,相信是會增加使用者的困擾,也大大增加了程式的複雜度與維護上的難度。所以本篇也紀錄一下自己在實作上的經驗,一方面留個紀錄也希望幫助更多人,廢物不多說我們開始吧!

了解架構並實作

原本在找資料時找到這篇 使用Asp.Net MVC打造Web Api (16) - 統一輸入/出格式以及異常處理策略,不過發現裡面的所用到的方法似乎是 For ASP.NET MVC,而非 Web API (不知道筆者這樣認知有沒有錯誤,如果有還麻煩前輩們指教),而本篇的思考模式跟這篇是一樣的,只是把它改成 Web API 能用的方法而已。

image

按照上圖所示當使用者請求不同的 API 時,返回頁面之前會將資料重新打包後再傳回頁面給使用者,如此一來使用者所看到的資料格式就會是固定的。

1.所以首先我們需要先自定義一個 Model 來當作我們的包裝的容器,其類別的定義如下:

public class ApiResultModel
{
    public HttpStatusCode Status { get; set; }
    public object Data { get; set; }
    public string ErrorMessage { get; set; }
}

2.相信寫過 ASP.NET MVC 的朋友一定會知道,一般我們會將一些在 Action 中固定的邏輯,利用 Filter 來套用到每一個 Action 上面,例如:Authorize。如果你對 Filter 不是很熟悉可以參考一下網路上前輩所寫的文章:[VS2010] ASP.NET MVC with Action Filters。 所以這邊我們也需要使用同樣的技巧來重新打包我們回傳的資料格式,我們先新增一個 ApiResultAttribute.cs 的檔案,且繼承 System.Web.Http.Filters.ActionFilterAttribute,並且複寫 OnActionExecuted 的方法,如下:

public class ApiResultAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
       public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
       {
            base.OnActionExecuted(actionExecutedContext);     
       }
}

3.而 OnActionExecuted 會在 Action 執行之後呼叫,也表示我們將資料送進這個方法裡面,接著處理我們主要打包的程式邏輯,程式碼如下:

public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
    base.OnActionExecuted(actionExecutedContext);
 
    ApiResultModel result = new ApiResultModel();
 
    // 取得由 API 返回的狀態碼
    result.Status = actionExecutedContext.ActionContext.Response.StatusCode;
    // 取得由 API 返回的資料
    result.Data = actionExecutedContext.ActionContext.Response.Content.ReadAsAsync<object>().Result;
    // 重新封裝回傳格式
    actionExecutedContext.Response = actionExecutedContext.Request.CreateResponse(result.Status, result);
}

4.而為了要讓所有的 Web API 都能套用我們自定義的 Filter,所以我們需要到 App_Start → WebApiConfig.cs → Register 來註冊全域的 Web API Filter。(注意:若要註冊 Web API 的 Filter 需在 WebApiConfig.cs 中註冊,而非 FilterConfig.cs 中)

config.Filters.Add(new ApiResultAttribute());

5.重新建置之後我們再重新執行一次原先的 Web API 程式,就會看到回傳的格式已經變成我們自定義的格式了:

"Status": 200,
    "Data": [
        {
            "Account": "taxi",
            "Mark": "",
            "Name": "王大明",
            "Telephone": "0986540123",
            "AccountStatus": true
        },
        {
            "Account": "taxi2",
            "Mark": "",
            "Name": "方大同",
            "Telephone": "0922335111",
            "AccountStatus": true
        },
        {
            "Account": "q121234567",
            "Mark": null,
            "Name": "0000",
            "Telephone": "0972334334",
            "AccountStatus": true
        }
    ],
    "ErrorMessage": null

例外處理

前面我們已經將訊息打包成我們要的格式了,不過我們還沒確切地去處理有關例外的程式碼,一般當程式發生錯誤產生例外時,我們當然也希望接收端能知道程式發生錯誤,進而顯示該顯示的訊息,而不是活生生地看著程式 Crash 或是停頓,這樣將帶給你的客戶不好的體驗,而在 ASP.NET MVC 中也有提供專門處理例外的 ExceptionFilterAttribute,所以接著來看看該如何打包我們的例外訊息吧。

1.新增一個 ApiErrorHandleAttribute.cs 並且繼承 System.Web.Http.Filters.ExceptionFilterAttribute,接著複寫 OnException 當例外發生時執行的方法,程式碼如下:

public class ApiErrorHandleAttribute : System.Web.Http.Filters.ExceptionFilterAttribute
{
       public override void OnException(System.Web.Http.Filters.HttpActionExecutedContext actionExecutedContext)
       {
           base.OnException(actionExecutedContext);
       }
}

2.透過 OnException 的方法能讓我們捕捉當例外發生時要處理的事情,一般系統我們也會在這邊將發生錯誤的時間、登入的使用者以及錯誤的狀況記錄下來 (例如:系統事件、存入資料庫、寫入 .txt 檔 … 等),不過這邊不是我們討論的重點,我們先來看看該如何打包我們的例外訊息,程式碼如下:

public override void OnException(System.Web.Http.Filters.HttpActionExecutedContext actionExecutedContext)
{
    base.OnException(actionExecutedContext);
    
    // 取得發生例外時的錯誤訊息
    var errorMessage = actionExecutedContext.Exception.Message;
 
    var result = new ApiResultEntity()
    {
        Status = HttpStatusCode.BadRequest,
        ErrorMessage = errorMessage
    };
 
    // 重新打包回傳的訊息
    actionExecutedContext.Response = actionExecutedContext.Request
        .CreateResponse(result.Status, result);
}

 

3.而因為程式丟出例外後會先回到 OnActionExcuted 在進到例外的處理,所以我們稍微修改一下原本的 OnActionExcuted 這個方法,讓發生例外時就直接跳過不再這邊打包我們的訊息,程式碼如下:

public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
    // 若發生例外則不在這邊處理
    if (actionExecutedContext.Exception != null)
    return;
    
    base.OnActionExecuted(actionExecutedContext);
 
    ApiResultModel result = new ApiResultModel();
 
    // 取得由 API 返回的狀態碼
    result.Status = actionExecutedContext.ActionContext.Response.StatusCode;
    // 取得由 API 返回的資料
    result.Data = actionExecutedContext.ActionContext.Response.Content.ReadAsAsync<object>().Result;
    // 重新封裝回傳格式
    actionExecutedContext.Response = actionExecutedContext.Request.CreateResponse(result.Status, result);
}

3.接著我們一樣要將此自定義的 Filter 註冊到程式當中,所以一樣到 App_Start → WebApiConfig.cs → Register 來註冊我們的 Filter:

config.Filters.Add(new ApiErrorHandleAttribute());

4.重新建置後,當我們程式發生例外時也會依照我們的格式回傳給使用者:

{
    "Status": 400,
    "Data": null,
    "ErrorMessage": "嘗試以零除。"
}

總結

就這樣我們又成功解決了一個簡單的案例,不過這邊也需要提醒一下讀者,一般在處理例外這邊是不會直接將例外訊息回傳給使用者的,因為如果假設你今天丟出的例外有包含了一些比較敏感的資訊,例如:資料庫名稱或資料表名稱...等等,這樣一來你的程式就間接的有了漏洞了,所以如果真的要用此程式碼記得後面例外捕捉那邊還要在包裝一下。


參考連結

新手發文,如有錯誤煩請告知,感謝。
如果喜歡我的文章請按推薦,有任何問題歡迎下面留言~~~

 

 

簽名:

學習這條路很廣,喜歡什麼技術不重要,重要的是你肯花時間去學習