ASP.NET Web API 入門常用技巧

摘要:整理幾個撰寫 ASP.NET Web API 時可能會碰到的問題,包括專案目錄結構、類別的命名、處理 JSON 序列化的相關問題與解法、傳回任何格式的內容、以及如何讓 Web API 也能像傳統 web service 或 MVC 那樣自訂 action 名稱。

這篇筆記所整理的東西比較零碎,如需比較完整的入門觀念,可以參考文後附的延伸閱讀清單。
開發環境:Visual Studio 2012 + ASP.NET 4.5

Tip 1: API Controller 寫在哪裡?如何命名?

Web API Controller 通常也和一般 Controller 一樣放在 ASP.NET MVC 專案的 Controller 目錄下。可是如果已經有 UserController,又需要提供 Web API 版本的 UserController,就得避開名稱衝突的問題。

一種作法是用命名來區隔,例如 UserController 和 UserApiController。後者代表 Web API 版本的控制器類別。如此命名的話,前端存取 Web API 時的 URI 就會類似 http://HostName/AppName/api/UserApi/。
[操作提示] 新增一個 API Controller:Solution Explorer \ 應用程式專案 \ Controllers 資料夾點右鍵,Add \ Controller...,在新開啟的 Add Controller 對話窗中,輸入 Controller name,選擇 Template(可用 Empty API controller 或其他與 API controller 有關的樣板)。

另一種作法是在 Controller 目錄下建一個 Api 子目錄,把所有 API Controller 放在裡面。於是,放在 Controller\Api 目錄下的控制器類別的命名空間預設會加上一層 "Api",所以不至於有名稱衝突的情形。這辦法挺好。

還有一種選擇,是把 Web API 放在 Areas 裡面。這種方式可能會需要額外寫一些程式碼來設定 routing。

Tip 2: 讓 Web API 預設傳回 JSON 字串

ASP.NET MVC 4 Beta 版是採用 .NET 自家的 DataContractJsonSerializer 來處理 JSON 的序列化工作,不是那麼好用,而正式版已經改用  Json.NET 作為預設的序列化工具,也就不再需要額外寫 media type formatter 來取代預設的序列化工具了。

可是 Web API 會因為瀏覽器送出的 Accept 標頭而傳回不同的格式,例如 IE 會取得 JSON,並詢問要開啟檔案還是另存檔案;Chrome 則會取得 XML 格式的回應。其中原委可參考保哥的文章:如何讓 ASP.NET Web API 無論任何要求都回應 JSON 格式。文中也有提到解法,是在 Global.asax 裡面的 Application_Start 事件中動手腳。另一種類似的解法是在 App_Start\WebApiConfig.cs 中處理,例如:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault(t => t.MediaType == "application/xml");
        config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
    }
}

關鍵在後面兩行 code。

這解法是在 stackoverflow.com 上爬來的,其優點是依然保有傳回 XML 格式的能力,亦即預設傳回 JSON,但如果前端發出的 HTTP request 的 Accept 標頭是 "text/xml",就會傳回 XML。

範例程式

前面的手腳做完以後,底下的範例程式就能順利傳回 JSON 字串了。

using System.Collections.Generic;
using System.Web.Http;

namespace MvcAppDemo1.Controllers.Api
{
    public class TestJsonController : ApiController
    {
        public Dictionary<string, string> Get()
        {
            var result = new Dictionary<string, string>()
            {
                {"001", "Banana"},
                {"002", "Apple"},
                {"003", "Orange"}
            };
            return result;
        }
    }
}

你可以看到,TestJsonController 傳回的型別是 Dictionary<string, string>,但前端收到的會是已經自動幫你序列化的 JSON 字串,如下圖:


所以在寫 ASP.NET Web API 時,就不用自己寫 code 去處理 JSON 序列化的動作。如果像下面這樣畫蛇添足:
using System.Collections.Generic;
using System.Web.Http;
using NewtonSoft.Json;

namespace MvcAppDemo1.Controllers.Api
{
    public class TestJsonController : ApiController
    {
        public Dictionary<string, string> HandMadeJson()
        {
            var result = new Dictionary<string, string>()
            {
                {"001", "Banana"},
                {"002", "Apple"},
                {"003", "Orange"}
            };
            return JsonConvert.SerializeObject(result);
        }
    }
}

前端瀏覽器看到的 JSON 就會有一堆多餘的反斜線:

圖中網址列的紅色框不是框錯位置,而是跟後面的 Tip 5 要討論的議題有關。

Tip 3: 手動傳回 JSON 或任何格式的內容

有時候,Web API 的傳回結果不是 JSON,例如我們可能需要傳回一些純文字,甚至圖片或任何二進位檔案。此時就會需要跳過自動序列化的機制,使用我們自行建立的內容來傳回前端。

碰到這種情形,我們可以將 Web API 方法的回傳型別宣告為 HttpResponseMessage。參考以下範例:

using System.Collections.Generic;
using System.Net.Http.Headers;
using System.Web.Http;
using NewtonSoft.Json;

namespace MvcAppDemo1.Controllers.Api
{
    public class TestJsonController : ApiController
    {
        [HttpGet, HttpPost] // 讓此方法可同時接受 HTTP GET 和 POST 請求.
        public HttpResponseMessage HandMadeJson()
        {
            var data = new Dictionary<string, string>() { // 略 }

            string json = JsonConvert.SerializeObject(data);
            var result = new HttpResponseMessage(HttpStatusCode.OK);
            result.Content = new StringContent(json);
            result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
            return result;
        }
    }
}

由於是傳回文字內容,所以這裡是用 StringContent 來建立內容物件。

透過這種方式,即使沒有像 [Tip 2] 的額外處理,我們的 Web API 仍然可以傳回 JSON 字串,甚至任何格式的內容。例如底下的程式片段可傳回一個 jpeg 圖片:

var result = new HttpResponseMessage(HttpStatusCode.OK);
result.Content = new ByteArrayContent(imageData);
result.Content.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");
return result;

由於要傳回的圖片是二進位格式的資料,故建立內容時用的是 ByteArrayContent。它是 StringContent 的父類別。

除了 ByteArrayContent,還有 MultipartContent 和 StreamContent,這個三類別的父親都是 System.Net.Http.HttpContent

Tip 4: 使用 Request.CreateResponse() 來建立回應內容

我們當然可以在 Web API 方法中利用 Json.NET 來將物件序列化成 JSON 字串,但這種手動的方式有個缺點:我們的 API 也就只能固定傳回 JSON,而無法由用戶端發送的 HTTP 請求的 Accept 標頭來自動傳回對應之序列化格式。

如果要回傳的內容是自訂型別的物件,而且希望使用 ASP.NET Web API 內建的 content-negotitation 機制,有個方法很好用:Request.CreateResponse() 。以下是個簡單範例:

[HttpGet]
public HttpResponseMessage Get()
{
    var result = new 
    {
        UserName = "Michael",
        City = "Taipei"
    };
    return Request.CreateResponse(HttpStatusCode.OK, result);
}

這寫法要比自己手動序列化物件來得簡潔多了。

Tip 5: 使用 Action-Based Routing

在 Tip 2 的圖中,紅色框住的 URI 是要順便指出,這個畫蛇添足版的 Web API 方法名稱是 HandMadeJson,它和先前的 Get() 一樣是 TestJsonController 的成員。

可是如果沒有額外動手腳,用戶端在存取 http://HostName/api/TestJson/HandMadeJson 這樣的 URI  時,仍然會由預設的 Get 方法來服務,而不會派送給 HandMadeJson。這是 routing 設定的緣故,把 App_Start\WebApiConfig.cs 裡面的 Register 方法稍微改一下就行了:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { action = RouteParameter.Optional, id = RouteParameter.Optional }
        );

        var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault(t => t.MediaType == "application/xml");
        config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
    }
}

原本的 MapHttpRoute() 呼叫所傳入的 routeTemplate 參數是 "api/{controller}/{id}",現在只是在中間多加了一個 "action" 而已。這種 routing 方式叫做 action-based routing。

加了一層 action 的路徑,可以讓 Web API 不受限於 Get、Post、Put、Delete 等 HTTP 動作方法的命名規則,提供更大的彈性,就好像在寫傳統的 Web service 或 HTTP Handler 那樣,可以自訂動作名稱。不過,這好像就不是純正的 REST 了?這點對我來說倒不是問題。

延伸閱讀

Post Comments

技術提供:Blogger.