傳統實現
在搭建 Web API 服務的時候,針對客戶端請求,我們一般都會自定義響應的 JSON 格式,比如:
{
"Data" : {
"Id" : 100,
"Name" : "Robin"
},
"ErrorMessage" : "錯誤消息"
}
在基於 ASP.NET Web API 的應用程序,我們一般會創建一個相應結構的 C# 類,如下:
public class ApiResult
{
public string ErrorMessage { get; set; }
public object Data { get; set; }
}
這里約定, ErrorMessage 為空或null,即表示沒有異常,這時 Data 就是需要的數據;反之如果 ErrorMessage 不為空或null, 則代表錯誤消息,這時 Data 為null。
接下來在 Action 中返回該類的一個實例, Web API 會在內部調用格式化器將對象序列化為 JSON 或 XML 等格式,如下:
public class UserController : ApiController
{
public IHttpActionResult GetUser()
{
return new ApiResult()
{
Data = new User{ Id = 100, Name = "Robin" },
ErrorMessage = string.Empty
};
}
}
public class User
{
public int Id {get; set;}
public string Name {get; set;}
}
好了,傳統做法就是這樣,也可以實現。但是再進一步考慮,如果有非常多的 Action 方法,每次都要寫 reutrn new ApiResult(){......} 是不是特別繁瑣呢?
問題
有沒有辦法在 Action 方法中只返回真正需要的數據,但是返回給客戶端時又統一成約定的 JSON 結構呢?
解決方案
當然有辦法,借助 Web API 提供的 ActionFilter 就可以實現。
首先我們新建一個 CustomActionFilter :
public class CustomActionFilter : ActionFilterAttribute
{
public override void OnActionExecuted(HttpActionExecutedContext context)
{
var content = context.Response?.Content as ObjectContent;
if (content != null)
{
content.Value = new ApiResult
{
Data = content.Value,
};
}
}
}
然后 Action 方法這樣寫:
public User GetAll()
{
return new new User{Id = 100, Name = "Robin"};
}
這樣實現的另一個好處是,由於返回值是強類型,可以據此生成 API 文檔,方法的可讀性也更好。
異常處理
前面提到的需求實現了,然后再進一步考慮,如何處理異常情況?
如果由於代碼 BUG 拋出未處理的異常,Web API仍然會調用 CustomActionFilter 中的代碼,但是這時 Response = null,也就無法給 content.Value 重新賦值。
這時 Web API 會將框架約定的 JSON 消息返回給客戶端,而不是我們業務上需要的,如下是 Web API 拋出的未處理異常消息:
{
"Message": "An error has occurred.",
"ExceptionMessage": "No MessageException parameter",
"ExceptionType": "Framework.Common.MessageException",
"StackTrace": " 在 Controllers.FooController.GetAll() 位置......
}
這時如果還希望異常消息遵循業務約定的 JSON 格式,該如何做呢?
這里要分幾種情況:
Action 內的異常
可以直接在 CustomActionFilter 的 OnActionExecuted 方法中處理,改造后的代碼如下:
public override void OnActionExecuted(HttpActionExecutedContext context)
{
var content = context.Response?.Content as ObjectContent;
if (content != null)
{
content.Value = new ApiResult { Data = content.Value };
}
// 設置發生異常時的消息
if (context.Exception != null)
{
context.Response = new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent(JsonConvert.SerializeObject(
new ApiResult
{
ErrorMessage = context.Exception.Message
}), Encoding.UTF8, "application/json")
};
}
}
同樣也可以用自定義 ExceptionFilter 來達到同樣的目的,這里為了簡化不再貼代碼。
其他異常
ActionFilterAttribute 和 ExceptionFilterAttribute 都只能處理部分異常,比如 Action 內的異常,但是譬如 以下的幾種未處理異常,過濾器就愛莫能助了:
- 來自 Controller 構造器的異常。
- 來自 Message Handlers 的異常。
- 匹配路由過程中的異常
- 在序列化響應內容期間產生的異常
為了處理全局范圍內的未處理異常,Web API 提供了 ExceptionHandler 和 ExceptionLogger。
詳情可以參考我翻譯的文檔:ASP.NET Web API 2 中的全局錯誤處理
其中只有 ExceptionHandler 可以在捕捉到未處理異常並處理后,對響應消息進行重新設置,而 ExceptionLogger 則不能。
代碼如下:
public class CollectServiceExceptionHandler : ExceptionHandler
{
public override Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken)
{
context.Result = new ApiResult { ErrorMessage = context.Exception.Message };
return base.HandleAsync(context, cancellationToken);
}
}
注意:這里 ExceptionHandlerContext 的 Result 屬性的類型是 IHttpActionResult,所以**ApiResult ** 類要實現 IHttpActionResult 接口。
ExceptionHandler 的用途就是:接收全局范圍內未處理的異常,然后返回一個自定義的錯誤消息。
總結
實現開篇的需求,有三種實現方式:
- 自定義 ActionFilterAttribute
- 自定義 ExceptionFilterAttribute
- 自定義 ExceptionHandler
補充:經 @ichengzi 指出,『web api 2.0 之前的版本不支持這種處理方法』。