在之前的文章中,我們已經可以在一般呼叫API的情況下將回傳的JSON格式統一,且在Exception發生時也能夠將Exception已我們想要的格式回傳,但由於Asp.Net WebApi已經先幫我們做好了找不到正確的Controller和Action時的處理,導致當呼叫不存在的API時,還是無法依照想要的JSON格式回傳,這篇文章就來解決這個問題。
呼叫不存在的API分成兩種狀況,第一種是呼叫了不存在的Controller,會出現類似以下的錯誤訊息
{
"Message": "找不到與要求 URI 'http://localhost:48358/Api/Fake' 相符的 HTTP 資源。",
"MessageDetail": "找不到與名稱為 'Fake' 的控制器相符的類型。"
}
另外一種狀況是呼叫未實作的Http method,在ASP.NET WebApi中,也可以想像成呼叫了不存在的Action(例如ValuesController中只有Get這個Action,當我們用POST方法呼叫時,就會找不到)
{
"Message": "要求的資源不支援 http 方法 'POST'。"
}
針對這兩種狀況,我們可以分別繼承原來的DefaultHttpControllerSelector與ApiControllerActionSelector兩個類別,並且根據錯誤情況進行處理,將找不到Api的錯誤發生時導向自己設定的Error404Controller中來統一處理,也可以在這兩個Selector類別中處理各種不同的StatusCode。
首先我們先寫一個HttpNotFoundAwareDefaultHttpControllerSelector類別,繼承自DefaultHttpControllerSelector來處理找不到Controller時的狀況:
/// <summary>
/// HttpNotFound使用自訂Controller的ControllerSelector
/// </summary>
public class HttpNotFoundAwareDefaultHttpControllerSelector : DefaultHttpControllerSelector
{
/// <summary>
/// Initializes a new instance of the <see cref="HttpNotFoundAwareDefaultHttpControllerSelector"/> class.
/// </summary>
/// <param name="configuration">設定。</param>
public HttpNotFoundAwareDefaultHttpControllerSelector(HttpConfiguration configuration)
: base(configuration)
{
}
/// <summary>
/// 為指定的 <see cref="T:System.Net.Http.HttpRequestMessage" /> 選取 <see cref="T:System.Web.Http.Controllers.HttpControllerDescriptor" />。
/// </summary>
/// <param name="request">HTTP 要求的訊息。</param>
/// <returns>
/// 指定之 <see cref="T:System.Net.Http.HttpRequestMessage" /> 適用的 <see cref="T:System.Web.Http.Controllers.HttpControllerDescriptor" /> 執行個體。
/// </returns>
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
HttpControllerDescriptor decriptor = null;
try
{
decriptor = base.SelectController(request);
}
catch (HttpResponseException ex)
{
setErrorController(request, ex);
decriptor = base.SelectController(request);
}
return decriptor;
}
private static void setErrorController(HttpRequestMessage request, HttpResponseException ex)
{
var code = ex.Response.StatusCode;
var routeValues = request.GetRouteData().Values;
routeValues["controller"] = "Error";
if (code == HttpStatusCode.NotFound)
{
routeValues["controller"] = "Error404";
}
else
{
routeValues["controller"] = "ErrorOthers";
routeValues["id"] = code;
}
routeValues["action"] = "Get";
request.Method = HttpMethod.Get;
}
}
接著建立HttpNotFoundAwareControllerActionSelector類別,繼承自ApiControllerActionSelector來處理找不到Action(或HttpMethod錯誤)時的狀況:
/// <summary>
/// HttpNotFound使用自訂Controller的ApiActionSelector
/// </summary>
public class HttpNotFoundAwareControllerActionSelector : ApiControllerActionSelector
{
/// <summary>
/// Initializes a new instance of the <see cref="HttpNotFoundAwareControllerActionSelector"/> class.
/// </summary>
public HttpNotFoundAwareControllerActionSelector()
{
}
/// <summary>
/// 為 <see cref="T:System.Web.Http.Controllers.ApiControllerActionSelector" /> 選取動作。
/// </summary>
/// <param name="controllerContext">控制器內容。</param>
/// <returns>
/// 選取的動作。
/// </returns>
public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
{
HttpActionDescriptor decriptor = null;
try
{
decriptor = base.SelectAction(controllerContext);
}
catch (HttpResponseException ex)
{
setErrorController(controllerContext, ex);
decriptor = base.SelectAction(controllerContext);
}
return decriptor;
}
private static void setErrorController(HttpControllerContext controllerContext, HttpResponseException ex)
{
var controllerName = "Error404";
var code = ex.Response.StatusCode;
var routeValues = controllerContext.RouteData.Values;
if (code != HttpStatusCode.NotFound && code != HttpStatusCode.MethodNotAllowed)
{
controllerName = "ErrorOthers";
routeValues["id"] = code;
}
routeValues["action"] = "Get";
controllerContext.Request.Method = HttpMethod.Get;
IHttpController httpController = new Error404Controller();
controllerContext.Controller = httpController;
controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration,
controllerName, httpController.GetType());
}
}
接下來我們要設計處理錯誤的Error404Controller和ErrorOthersController。內容都很簡單
Error404Controller.cs
public class Error404Controller : ApiController
{
public object Get()
{
throw new Exception("找不到此API");
}
}
ErrorOthersController.cs
public class ErrorOthersController : ApiController
{
public object Get(int id)
{
return new HttpStatusCodeResult(id);
}
}
最後記得到WebApiConfig.cs中把處理Controller跟Action的Selector換成我們自訂的類別,就大功告成啦!
config.Services.Replace(typeof(IHttpControllerSelector), new HttpNotFoundAwareDefaultHttpControllerSelector(config));
config.Services.Replace(typeof(IHttpActionSelector), new HttpNotFoundAwareControllerActionSelector());
當呼叫的Api不存在時(Api不存在或Method有誤等等),就會回傳我們預期格式的JSON內容
{
"StatusCode": 400,
"Result": null,
"Error": {
"ErrorId": "",
"Message": "找不到此API"
}
}
整個針對WebApi回傳資料格式一致的處理方法差不多到這邊就完成了,整個專案的source code可以到我的GitHub下載。
https://github.com/wellwind/WebApiCustomResponseDemo
如果只想看程式碼片段我也整理到gist上了。
https://gist.github.com/wellwind/11a22b1a4f6fdad13f9e
之後若是有發現其他預期外的狀況,再來更新文章囉。