說起選用ASP.NET MVC的原因,有兩個:一.以Web編程的方式來編寫Web程式,這句聽起來有點拗口,但對比WebForm就會有明顯的感覺了,WebPage為了類比出WinForm,犧牲了太多的Web特性,而這些特性恰恰是在HTTP上很關鍵的東西;二.ASP.NET MVC上構建Web Service真是方便到了極點,為什麼呢?基本上ASP.NET MVC的每個Action都像是一個HttpHandler一樣,可以處理各種類型的請求,而且Controller和Fitler的運用,更使程式碼群組織和功能擴充有了較強的提升;可以這麼說,M$的架構,個人感覺MVC是比較合“口味”的,相比學完Silverlight,學MVC覺得是沒有選錯。
使用WCF也能構建RESTful服務,但是連WCF都不敢聲稱自已是RESTful架構,還談的上什麼編程體驗,而構建RPC式的Web Service,一直以來也不是我想要做的事情,昨天整理資料時找到了以前做的一份文檔中的一段話,對RPC和ROA兩種構架服務的方式有一點小總結:
- RPC(Remote Procedure Call)式架構採用SOAP協議,使用各種契約,高度依賴開發工具。
- ROA(Resource Oriented Architecture)面向資源的架構直接使用HTTP協議,使用統一介面,採用幾近於原生態的Web開發方式。
- RPC式架構封裝層次多,互通性、延展性可維護性低。
- ROA式架構訪問方式是透明,所有服務均採用同一種訪問方式。
- RPC式架構暴露內部演算法。
- ROA式架構暴露內部資料。
- RPC式架構其本都面向過程。
- ROA式架構可以物件導向
關於RESTful服務的話題不是今天討論的重點,如果大家感興趣,可以查看本人部落格裡面關於RESTful方面的博文,或是去閱讀《RESTful Web Service》那本書,那本書不錯,只不過適合有閑暇工夫時看,如果項目急著用就不必了。
準備使用ASP.NET MVC編程了,就必須對ASP.NET MVC的方方面面有一個瞭解,否則項目做了一半突然發現有繞不過去的彎,那就麻煩大了,這兩天試著用MVC做了點小玩意,今天再試試看到底如何使用MVC架構架構RESTful服務。
我的方向一直是公司資訊管理系統,所以一般提到的技術都是在構架和實施公司資訊管理系統時的解決方案,如果您是做Web應用的,那麼可能在思路上會有所出入。
在我的設想中,對於公司資訊管理系統而言,一個基礎服務(比如帳號管理),簡單點的方式是使用頁面來管理資料,然後使用服務的方式來發布資料,說簡單點,就是建一個項目,其中有兩部分:一部分是增刪改查頁面,一部分是供其他系統調用的資料服務,這種設計可以降低在系統構架初期的複雜度,即:先設計唯讀式的服務,等到應用擴充到一定程度後,再擴充為讀寫式的服務。在MVC中有一種"Area"方式,我感覺正好可以把網站和服務兩種方式從代碼的角度給"隔離"開來,當然建兩個應用程式也是可行的,只不過如果應用規模還沒有大到一定程度,那麼結構簡化也不是什麼壞事情。
建立MVC項目後第一步當然是考慮存取權限問題,頁面部分採用SSO來控管許可權肯定是沒有錯的,服務部分值的思考下,如果僅是自已公司的系統調用,那麼使用SSO認證是可行的,只要在以後的Request中加入FormsAuthentication的Ticket即可,如果服務需要做成開放應用,那麼使用oauth是個不錯的選擇,但是這個方案似乎有點太重量級了,次點的方法使用Http Basick認證,當然視情況也可以使用SSO的授權。
因為我們是測試服務構建,所以訪問授權問題可以不加進來,但是如果是真正做項目,只有一句忠告:訪問授權問題越早考慮越好。
建立一個ASP.MVC項目作為服務提供者,在這個項目中我們建立一個Area,名稱為"Svc",在Controller檔案夾中建立一個控制器,名稱為UserController,其內部代碼如下:
public class UserController : Controller { private const string OUTERNS = "http://sample.cleversoft.com/user/1.0"; public ActionResult GetAll() { ActionResult result = null; var users = this.GetUsers(); if (this.ControllerContext.HttpContext.Request.ContentType.Contains("application/atom+xml")) { var feed = new SyndicationFeed(); var items = new List<SyndicationItem>(); foreach (var user in users) { var item = new SyndicationItem(); item.Id = user.ID.ToString(); item.Title = new TextSyndicationContent(user.Name); item.ElementExtensions.Add("Name", OUTERNS, user.Name); item.ElementExtensions.Add("Mail", OUTERNS, user.Mail); items.Add(item); } feed.Items = items; result = new SyndicationFeedResult(feed); } else { var json = new JsonResult(); json.JsonRequestBehavior = JsonRequestBehavior.AllowGet; json.Data = (from user in users select new { ID = user.ID.ToString(), Name = user.Name, Mail = user.Mail }).ToArray<object>(); result = json; } return result; } private IList<User> GetUsers() { var users = new List<User>(); users.AddRange(new User[] { new User() {ID = Guid.NewGuid(),Name = "a",Password ="a",Mail="a@cleversoft.com"}, new User() {ID = Guid.NewGuid(),Name = "b",Password ="b",Mail="b@cleversoft.com"}, new User() {ID = Guid.NewGuid(),Name = "c",Password ="c",Mail="c@cleversoft.com"}, new User() {ID = Guid.NewGuid(),Name = "d",Password ="d",Mail="d@cleversoft.com"}, new User() {ID = Guid.NewGuid(),Name = "e",Password ="e",Mail="e@cleversoft.com"} }); return users; } } public class User { public Guid ID { get; set; } public string Name { get; set; } public string Password { get; set; } public string Mail { get; set; } }
因為並不是每個使用者都是通過Client調用的,所以我為一個服務提供了兩種資料響應格式,如果Request的Content-Type為application/atom+xml的話,使用Atom格式返回資料,否則返回json格式。SyndicationFeedResult類型在這裡有定義。
建立另一個項目做為測試服務的用戶端,在項目中添加如下代碼做為服務的用戶端:
public class Client { public SyndicationFeed Query(Uri queryUri) { var request = WebRequest.CreateDefault(queryUri); request.Method = "GET"; request.ContentType = "application/atom+xml, charset=\"utf-8\""; //authorization var response = request.GetResponse() as HttpWebResponse; var stream = response.GetResponseStream(); var feed = SyndicationFeed.Load<GenericFeed<User>>(XmlReader.Create(stream)); stream.Close(); response.Close(); return feed; } } public class GenericFeed<T> : SyndicationFeed where T : SyndicationItem, new() { protected override SyndicationItem CreateItem() { return new T(); } } public class User : SyndicationItem { private const string OUTERNS = "http://sample.cleversoft.com/user/1.0"; public string Name { get { return GetValue<string>("Name"); } set { SetValue("Name", value); } } public string Mail { get { return GetValue<string>("Mail"); } set { SetValue("Mail", value); } } private T GetValue<T>(string outerName) { return base.ElementExtensions.ReadElementExtensions<T>(outerName, OUTERNS)[0]; } private void SetValue(string outerName, object value) { base.ElementExtensions.Add(outerName, OUTERNS, value); } } public class UserManager { public IList<User> GetAll() { //Get service url Uri queryUri = new Uri("http://localhost:20074/Svc/User/GetAll/"); var client = new Client(); var feed = client.Query(queryUri); var result = (from item in feed.Items select item as User).ToList<User>(); return result; } }
然後在調用該Client
public class HomeController : Controller { public ActionResult Index() { ViewBag.Message = "Welcome to ASP.NET MVC!"; UserManager userManager = new UserManager(); var users = userManager.GetAll(); return View(users); } public ActionResult About() { return View(); } }
對應的View如下:
@model IList<RESTfulClient.Code.User>@{ ViewBag.Title = "Home Page";}<h2>@ViewBag.Message</h2><p> To learn more about ASP.NET MVC visit <a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.</p><table> <tr> <td style="width: 200px"> ID </td> <td style="width: 200px"> Name </td> <td style="width: 200px"> Mail </td> </tr> @foreach (var user in Model) { <tr> <td>@user.Id.ToString() </td> <td>@user.Name </td> <td>@user.Mail </td> </tr> }</table>
F5運行結果如下:
以上是個人對於MVC構建RESTful服務最初級的構想和實踐,真正應用肯定還有很多細節要去考慮,歡迎有共同想法兄弟提供意見建議。
樣本程式
3月25日更新內容:POST SyndicationFeed樣本-----------------------------------
服務端:
[HttpPost] public ActionResult Create() { if (Request.ContentType.ToUpper().StartsWith("application/atom+xml".ToUpper())) { SyndicationFeed feed = SyndicationFeed.Load(XmlReader.Create(Request.InputStream)); } return new EmptyResult(); }
用戶端:
public void Create(Uri postUrl, GenericFeed<User> feed) { var request = WebRequest.CreateDefault(postUrl); request.Method = "POST"; MemoryStream ms = new MemoryStream(); using (var writer = XmlWriter.Create(ms)) { feed.SaveAsAtom10(writer); } request.ContentType = "application/atom+xml"; request.ContentLength = ms.Length; using (var sw = request.GetRequestStream()) { sw.Write(ms.ToArray(), 0, (int)ms.Length); } var response = request.GetResponse(); //Todo... }