RESTful探索3-ASP.NET 3.5 Routing for REST
3 |
先前提到REST的一項重點是透過URI來區別資源個體,例如: /product/a123代表代號為a123的Product資料項目,與傳統Web Form透過參數指定查詢對象的做法(例如: ShowProduct.aspx?prodId=a123)明顯不同。所幸ASP.NET 3.5內建的Routing機制,可以輕易滿足REST所需。
以下示範如何自己建立一個簡單的REST風格路由導向機制,以資料CRUD(Create/Read/Update/Delete)作業為目標,仿效ASP.NET MVC在Controller目錄針對不同Model處理程式的概念,比照ASP.NET Web API用/api/{model}及/api/{model}/{id}的格式,將Request導向特定的ashx Handler。
- 建立一個ASP.NET 3.5 Web Site Project
- 為專案新增參照System.Web.Routing
- 在web.config新增設定 (參考文章: http://www.4guysfromrolla.com/articles/051309-1.aspx)
<configuration>
...
<system.web>
...
<httpModules>
...
<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
</httpModules>
</system.web>
<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<modules>
...
<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
</modules>
<handlers>
...
<add name="UrlRoutingHandler" preCondition="integratedMode" verb="*" path="UrlRouting.axd" type="System.Web.HttpForbiddenHandler, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
</handlers>
...
</system.webServer>
</configuration> - 接著,我們要寫一個IRouteHandler處理URL的重新導向。IRouteHandler的用途是掛在Global.asax的Application_Start()宣告中,例如:
<%@ Application Language="C#" %>
<%@ Import Namespace="System.Web.Routing" %>
<script runat="server">
void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}
void RegisterRoutes(RouteCollection routes)
{
//加入路徑設定
routes.Add("Item",
new Route("api/{model}/{id}", new CompactRESTRouteHandler()));
routes.Add("List",
new Route("api/{model}", new CompactRESTRouteHandler()));
}
void Application_End(object sender, EventArgs e) { }
void Application_Error(object sender, EventArgs e) { }
void Session_Start(object sender, EventArgs e) { }
void Session_End(object sender, EventArgs e) { }
</script>
- 接著來看CompactRESTRountHandler怎麼寫?
由於API用不到Server Control或HTML元素,僅以JSON格式溝通,因此用.ashx(Generic Handler)取代.aspx以來輕巧效率。另外,我偷學了ASP.NET MVC的概念,不寫死網站支援的Model,而是在接收到/api/boo時,尋找/api目錄下是否有個booHandler.ashx? 若存在就交由它處理,若無則傳回HTTP 404。
所有的RouteHandler要繼承IRouterHandler並實做public IHttpHandler GetHttpHandler(RequestContext requestContext),而requestContext.RouteData.Values是個RouteValueDictionary,在Global.asax裡設定api/{model}/{id}所比對出來的model及id參數,可透過requestContext.RouteData.Values["model"]方式取得。
另外一個課題是,在ashx無法直接取得requestContext.RouteData.Values["id"]等參數,故需由RouteHandler取出再設法接力傳給ashx,這部分可交由HttpContext.Current.Items搞定。
依據以上構想,可實現如下的簡易RouteHandler:using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Routing;
using System.IO;
using System.Web.Hosting;
using System.Web.Compilation;
using System.Web.UI;
/// <summary>
/// 簡易版的REST RouteHandler
/// </summary>
public class CompactRESTRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var routeData = requestContext.RouteData;
//取出參數
string model = Convert.ToString(routeData.Values["model"]);
string id = Convert.ToString(routeData.Values["id"]);
HttpContext.Current.Items.Add("model", model);
if (!string.IsNullOrEmpty(id))
HttpContext.Current.Items.Add("id", id);
//檢查看看有無該Model對應的ASHX?
string ashxName = model + "Handler.ashx";
//找不到對應的ASHX
if (!File.Exists(HostingEnvironment.MapPath("~/API/" + ashxName)))
return BuildManager.CreateInstanceFromVirtualPath("~/NotFound.aspx",
typeof(Page)) as Page;
//導向指定的ASHX
return BuildManager.CreateInstanceFromVirtualPath("~/API/" + ashxName,
typeof(IHttpHandler)) as IHttpHandler;
}
}
REST風格URL導向的機制建好了,我們在Web Site專案中建立一個API目錄,放入一個測試用的BooHandler.ashx:
<%@ WebHandler Language="C#" Class="BooHandler" %>
using System;
using System.Web;
public class BooHandler : IHttpHandler {
public void ProcessRequest (HttpContext context) {
context.Response.ContentType = "text/plain";
context.Response.Write(" HttpMethod=" + context.Request.HttpMethod);
context.Response.Write(" {id}=" + context.Items["id"]);
}
public bool IsReusable {
get {
return false;
}
}
}
直接用瀏覽器來個簡單測試吧!
1) FooHandler.ashx不存在時,被導向NotFound.htm
2) /api/boo時導向BooHandler.ashx,但{id}無內容
3) /api/boo/123導向BooHandler.ashx,{id} == 123
URI導向ashx的部分OK了,下一步就要進入重頭戲,在ASP.NET ashx中判別HttpMethod完成CRUD等作業囉!
Comments
# by Slash
黑暗大您好, 想要請教您一些問題... 因為想要騙瀏覽器這是一張圖片(後端仍然用 .ashx 打圖),請問有什麼辦法可以達到這樣的http要求嗎? http://localhost/AspNet35REST/foo/a.jpg 遇到的問題是 url 裡面有 "." 所以過不去 輸入 http://localhost/AspNet35REST/foo/a.jpg/ 是可以完整的解出字串,但是最後面的 "/" 反而讓網址變成是目錄了。 網路上所有的論壇都說把 web.config 設定 <httpRuntime relaxedUrlToFileSystemMapping="true"/> 就可以打開,可是我設定後根本沒有正常動作。 我的環境是 .net 4.0 + iis 7.5
# by Jeffrey
to Slash, 我寫了一個ASP.NET Routing練習(http://blog.darkthread.net/post-2012-07-13-aspnet-4-routing-download-image.aspx),你看看是否符合所提的情境。
# by chicken
to Slash, 你應該是用 IIS or IISExpress 對吧? darkthread 應該是用 dev web server .. 這兩種環境對於 .jpg 這種靜態檔案的處理方式不一樣,之前在寫類似的 httpHandler 就被整過... 這個案例主角主要是 URL routing module, IIS module 有個選項 "Invoke only for requests to ASP.NET applications or managed handlers", 預設是勾起來的, .jpg 就這樣被跳過去了 XD 解決方式是去把這個模組 [UrlRoutingModule-4.0] 的這個選項拿掉,那麼所有 request (包括 .jpg, .html 等) 都會交由 Url routing 處理,否則就只有目錄,或是 .aspx 這類 managed handlers 會接手的才有效... 要偷懶的話也可以把這段加到 web.config 內,啟用 pipeline mode 就會生效... <system.webServer> <modules> <remove name="UrlRoutingModule-4.0" /> <add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition="" /> </modules> </system.webServer>