说起选用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(); } }
@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... }