RESTful探索4-萬用RESTful API ashx模版類別
1 |
上集我們搞定了ASP.NET 3.5 Routing,能將api/{model}的Request正確導向指定的ashx。而在系列文一開始提過RESTful的另一個重點是依不同的HttpMethod進行不同作業,在jQuery.ajax()呼叫RESTful Web Service的文章中,其實已偷偷示範過如何用ASP.NET Web Form滿足RESTful Server端的要求,把類似的程式碼搬進ashx,就能打造出RESTful Web Service。
不過實務api中通常會包含多個Model的API程式,若只是"把類似程式碼搬進多個Model ashx再改一改"戲碼反覆上演,意味著得不斷"Copy & Paste",有違我努力奉行的DRY原則,萬萬不可! 在這個議題上,我決定採行的策略是--將每個Model API Handler都會用到的判斷HttpMethod、傳回JSON、適時回傳HttpStatus 201/404... 等程式邏輯,統一收編放在抽象類別中,而類別中宣告GetList(傳回集合), GetItem(傳回單一資料物件), CreateItem(新增資料), UpdateItem(更新資料), DeleteItem(刪除資料)五個抽象方法。如此,要開發不同Model的API Handler只需繼承這個父類別,實做GetList, GetItem, CreateItem, UpdateItem, DeleteItem五個方法,程式就寫完了。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Script.Serialization;
using System.IO;
public abstract class CompactRESTHandler<T> : IHttpHandler where T : new()
{
public CompactRESTHandler()
{
}
public bool IsReusable
{
get { return false; }
}
//REF: http://goo.gl/Uk1go
protected static bool IsNullOrEmpty(T item)
{
return EqualityComparer<T>.Default.Equals(item, default(T));
}
public void ProcessRequest(HttpContext context)
{
var args = new Dictionary<string, string>();
JavaScriptSerializer jss = new JavaScriptSerializer();
foreach (var key in context.Items.Keys)
args.Add(key.ToString(), Convert.ToString(context.Items[key]));
//傳回JSON內容的共用方法
Action<object, int> dumpDataJsonWithStatusCode = (data, statusCode) =>
{
if (statusCode > 0)
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json; charset=utf-8";
context.Response.Write(jss.Serialize(data));
};
Action<object> dumpDataJson = (data) => { dumpDataJsonWithStatusCode(data, 0); };
//傳回特定Status Code的共用方法
Action<int, string> returnHttpStatus = (statusCode, content) =>
{
context.Response.StatusCode = statusCode;
if (!string.IsNullOrEmpty(content))
context.Response.Write(content);
};
//由HttpMethod及參數決定呼叫何種方法
string httpMethod = context.Request.HttpMethod;
//除了GET以外,POST/PUT/DELETE時,Request的本文內容應為資料物件JSON
//在此將其反序列化回.NET端的物件
T item = default(T);
if (httpMethod != "GET")
{
//檢查ContentType必須為application/json,確保Client端明確知道要傳JSON
if (!context.Request.ContentType.StartsWith("application/json"))
throw new ArgumentException("ConentType is not application/json!");
try
{
//理論上還要依ContentType的charset決定Encoding,在此偷懶省略
using (StreamReader sr = new StreamReader(context.Request.InputStream))
{
string jsonString = sr.ReadToEnd();
item = (T)jss.Deserialize<T>(jsonString);
}
}
catch (Exception ex)
{
throw new ArgumentException("Failed to parse JSON string");
}
}
try
{
switch (httpMethod)
{
case "GET": //GetList及GetItem由是否提供Id來區分
if (args.ContainsKey("id"))
{
item = GetItem(args);
if (IsNullOrEmpty(item))
returnHttpStatus(404, "Not Found!");
else
dumpDataJson(item);
}
else
dumpDataJson(GetList(args));
break;
case "POST":
//傳回HTTP 201 Created
dumpDataJsonWithStatusCode(CreateItem(item), 201);
//加上Response Header - Location
context.Response.AddHeader("Location",
GetCreateLocationHeader(item));
break;
case "PUT":
UpdateItem(item);
break;
case "DELETE":
DeleteItem(item);
break;
}
}
catch (Exception ex)
{
dumpDataJsonWithStatusCode(ex.Message, 500);
}
context.Response.End();
}
//查詢取得物件集合
public abstract List<T> GetList(Dictionary<string, string> args);
//取得特定資料物件
public abstract T GetItem(Dictionary<string, string> args);
//新增資料(需回傳新增成功的資料物件)
public abstract T CreateItem(T item);
//傳回新增資料後的Location Response Header
public abstract string GetCreateLocationHeader(T item);
//更新資料
public abstract void UpdateItem(T item);
//刪除資料
public abstract void DeleteItem(T item);
}
CompactRESTHandler裡用到泛型及不少Lambda技巧,略為複雜。但在一般應用中,可將其視為黑盒子,直接當元件用,以下透過範例展示如何繼承CompactRESTHandler寫出API Handler。
我做了一個極簡單的Model—BlahEntry,其實是模仿KeyValuePair<string, string>,只有兩個屬性Key及Value,因此BlahHandler就是用來處理BlahEntry新增、修改、刪除的API程式。為求簡化起見,先不把資料庫拉進來,只用一個List<BlahEntry>當成資料儲存區,新增用List.Add()、刪除用List.Remove()、更新則用Remove()+Add()模擬;查詢部分也很簡單,清單查詢就直接吐回整個List,查詢特定Key時則用LINQ查詢搞定。
<%@ WebHandler Language="C#" Class="BlahHandler" %>
using System;
using System.Web;
using System.Collections.Generic;
using System.Linq;
public class BlahEntry
{
public string Key { get; set; }
public string Value { get; set; }
public BlahEntry() { }
public BlahEntry(string key, string value)
{
Key = key; Value = value;
}
}
public class BlahHandler : CompactRESTHandler<BlahEntry>
{
static List<BlahEntry> dataStore = new List<BlahEntry>();
static BlahHandler() //預設放入兩筆資料
{
dataStore.Add(new BlahEntry("Blog", "http://blog.darkthread.net"));
dataStore.Add(new BlahEntry("Author", "Jeffrey Lee"));
}
//清單查詢API,直接傳回結果
public override List<BlahEntry> GetList(Dictionary<string, string> args)
{
return dataStore;
}
//使用LINQ查詢特定Key值內容
private BlahEntry GetItem(string key)
{
return dataStore.SingleOrDefault(
//比對時不分大小寫
o => string.Compare(o.Key, key, true) == 0);
}
//查詢特定資料API
public override BlahEntry GetItem(Dictionary<string, string> args)
{
return GetItem(args["id"]);
}
//新增資料API
public override BlahEntry CreateItem(BlahEntry item)
{
dataStore.Add(item);
return GetItem(item.Key);
}
//更新資料API
public override void UpdateItem(BlahEntry item)
{
var toUpdate = GetItem(item.Key);
if (!IsNullOrEmpty(toUpdate))
{
//使用Remove + Add模擬Update
dataStore.Remove(toUpdate);
dataStore.Add(item);
}
else
throw new ApplicationException("Cannot find the data to update!");
}
//刪除資料API
public override void DeleteItem(BlahEntry item)
{
var toDelete= GetItem(item.Key);
if (!IsNullOrEmpty(toDelete))
dataStore.Remove(toDelete);
else
throw new ApplicationException("Cannot find the data to delete!");
}
//新增資料時,傳回剛才新增資料之URI
public override string GetCreateLocationHeader(BlahEntry item)
{
return "api/Blah/" + item.Key;
}
}
複習一下先前介紹過的$.ajax() for REST,寫成以下的測試程式:
<!DOCTYPE>
<html>
<head>
<title>Blah Test</title>
<style>
body,input { font-size: 9pt; }
#dvEditor input { width: 100px; }
</style>
<script src='http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.js'></script>
<script>
$(function () {
$.ajaxSetup({
error: function (xhr, statusText, err) {
alert("Error: " + err + "\n" + xhr.responseText);
}
});
$("#btnList").click(function () {
$.ajax({
url: "api/Blah", //用GET, 不帶{id}參數
dataType: "json", //自動將結果解成物件
cache: false, //GET時要防止Cache
success: function (list) {
alert("List: " + JSON.stringify(list));
}
});
});
//將使用者輸入內容變成{ Key:*, Value:* }物件
function getKeyValueObject() {
return {
Key: $("#txtKey").val(),
Value: $("#txtValue").val()
};
}
$("#btnCreate").click(function() {
$.ajax({
url: "api/Blah",
type: "POST", //用POST代表新增資料
dataType: "json",
contentType: "application/json",
data: JSON.stringify(getKeyValueObject()),
statusCode: {
201: function (data, status, xhr) {
//預設會直接在Response Body傳回新增完的資料物件
alert("Create-" + JSON.stringify(data));
//由Response Header可取得資料URI
alert("Location-" + xhr.getResponseHeader("Location"));
}
}
});
});
$("#btnGet").click(function() {
$.ajax({
url: "api/Blah/Remark", //指定{id},讀取特定資料
cache: false, //GET時要防止Cache
dataType: "json",
success: function(data) {
alert("Get-" + JSON.stringify(data));
}
});
});
$("#btnUpdate").click(function() {
var kv = getKeyValueObject();
$.ajax({
url: "api/Blah/" + kv.Key,
type: "PUT", //使用PUT更新資料
contentType: "application/json",
//傳入要更新的資料JSON
data: JSON.stringify(kv)
});
});
$("#btnDelete").click(function() {
var kv = getKeyValueObject();
$.ajax({
url: "api/Blah/" + kv.Key,
type: "DELETE", //使用DELETE刪除資料
contentType: "application/json",
//傳入要刪除的資料JSON
data: JSON.stringify(kv)
});
});
});
</script>
</head>
<body>
<div id="dvEditor">
Key: <input id="txtKey" value="Remark" />
Value: <input id="txtValue" value="Test Text" />
</div>
<input type="button" id="btnList" value="List" />
<input type="button" id="btnCreate" value="Create" />
<input type="button" id="btnGet" value="Get [Remark]" />
<input type="button" id="btnUpdate" value="Update" />
<input type="button" id="btnDelete" value="Delete" />
</body>
</html>
畫面如下
測試順序與結果:
- 按下【List】
List: [{"Key":"Blog","Value":"httq://blog.darkthread.net"},{"Key":"Author","Value":"Jeffrey Lee"}] - 按下【Get[Remark]】(此時還沒有資料)
Error: Not Found
Not Found! - 按下【Create】
Create-{"Key":"Remark","Value":"Test Text"}
以及
Location-api/Blah/Remark - 再按一次【List】(可以看見Remark在其中囉!)
List: [{"Key":"Blog","Value":"httq://blog.darkthread.net"},{"Key":"Author","Value":"Jeffrey Lee"},{"Key":"Remark","Value":"Test Text"}] - 在Value欄位輸入"I am awesome",按下【Update】
- 再按一次【Get[Remark]】
Get-{"Key":"Remark","Value":"I am awesome"} - 按下【Delete】
- 按下【List】(Remark資料消失囉~)
List: [{"Key":"Blog","Value":"httq://blog.darkthread.net"},{"Key":"Author","Value":"Jeffrey Lee"}]
表演完畢,下台一躹躬!
Comments
# by Tim
黑暗大請教 在Global.asax裡的 routes.Add("Item", new Route("api/{model}/{id}", new CompactRESTRouteHandler(T))); T應該為何? 感謝