前言
当我们在开发 Web API 时,一般的情况下每个 API 回传的数据型态或格式都不尽相同,如果你的项目从头到尾都是由你一个人独力完成,那也许还可以说声「阿密陀佛」,但如果是有其他人需要和你共享你的 Api ,而回传的数据格式又不一样,相信是会增加使用者的困扰,也大大增加了程序的复杂度与维护上的难度。所以本篇也纪录一下自己在实作上的经验,一方面留个纪录也希望帮助更多人,废物不多说我们开始吧!
了解架构并实作
原本在找数据时找到这篇 使用Asp.NET MVC打造Web Api (16) – 统一输入/出格式以及异常处理策略,不过发现里面的所用到的方法似乎是 For ASP.NET MVC,而非 Web API (不知道笔者这样认知有没有错误,如果有还麻烦前辈们指教),而本篇的思考模式跟这篇是一样的,只是把它改成 Web API 能用的方法而已。
[]
按照上图所示当使用者请求不同的 API 时,返回页面之前会将数据重新打包后再传回页面给使用者,如此一来用户所看到的数据格式就会是固定的。
1.所以首先我们需要先自定义一个 Model 来当作我们的包装的容器,其类别的定义如下:
{
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 的方法,如下:
{
{
base.OnActionExecuted(actionExecutedContext);
}
3.而 OnActionExecuted 会在 Action 执行之后呼叫,也表示我们将资料送进这个方法里面,接着处理我们主要打包的程序逻辑,程序代码如下:
{
base.OnActionExecuted(actionExecutedContext);
ApiResultModel result = new ApiResultModel();
result.Status = actionExecutedContext.ActionContext.Response.StatusCode;
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 程序,就会看到回传的格式已经变成我们自定义的格式了:
"Data": [
"Account": "taxi",
"Name": "王大明",
"AccountStatus": true
{
"Mark": "",
"Telephone": "0922335111",
},
"Account": "q121234567",
"Name": "0000",
"AccountStatus": true
],
例外处理
前面我们已经将讯息打包成我们要的格式了,不过我们还没确切地去处理有关例外的程序代码,一般当程序发生错误产生例外时,我们当然也希望接收端能知道程序发生错误,进而显示该显示的讯息,而不是活生生地看着程序 Crash 或是停顿,这样将带给你的客户不好的体验,而在 ASP.NET MVC 中也有提供专门处理例外的 ExceptionFilterAttribute,所以接着来看看该如何打包我们的例外讯息吧。
1.新增一个 ApiErrorHandleAttribute.cs 并且继承 System.Web.Http.Filters.ExceptionFilterAttribute,接着复写 OnException 当例外发生时执行的方法,程序代码如下:
{
{
base.OnException(actionExecutedContext);
}
2.透过 OnException 的方法能让我们捕捉当例外发生时要处理的事情,一般系统我们也会在这边将发生错误的时间、登入的用户以及错误的状况记录下来 (例如:系统事件、存入数据库、写入 .txt 档 … 等),不过这边不是我们讨论的重点,我们先来看看该如何打包我们的例外讯息,程序代码如下:
{
base.OnException(actionExecutedContext);
// 取得发生例外时的错误讯息
var errorMessage = actionExecutedContext.Exception.Message;
{
ErrorMessage = errorMessage
};
actionExecutedContext.Response = actionExecutedContext.Request
.CreateResponse(result.Status, result);
3.而因为程序丢出例外后会先回到 OnActionExcuted 在进到例外的处理,所以我们稍微修改一下原本的 OnActionExcuted 这个方法,让发生例外时就直接跳过不再这边打包我们的讯息,程序代码如下:
{
if (actionExecutedContext.Exception != null)
return;
base.OnActionExecuted(actionExecutedContext);
ApiResultModel result = new ApiResultModel();
result.Status = actionExecutedContext.ActionContext.Response.StatusCode;
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,
"ErrorMessage": "尝试以零除。"
总结
就这样我们又成功解决了一个简单的案例,不过这边也需要提醒一下读者,一般在处理例外这边是不会直接将例外讯息回传给用户的,因为如果假设你今天丢出的例外有包含了一些比较敏感的信息,例如:数据库名称或数据表名称…等等,这样一来你的程序就间接的有了漏洞了,所以如果真的要用此程序代码记得后面例外捕捉那边还要在包装一下。