在一个空ASP.NET Web项目上创建一个ASP.NET Web API 2.0应用
由于ASP.NET Web API具有与ASP.NET MVC类似的编程方式,再加上目前市面上专门介绍ASP.NET Web API 的书籍少之又少(我们看到的相关内容往往是某本介绍ASP.NET MVC的书籍“额外奉送”的),以至于很多人会觉得ASP.NET Web API仅仅是ASP.NET MVC的一个小小的扩展而已,自身并没有太多“大书特书”的地方。而真实的情况下是:ASP.NET Web API不仅仅具有一个完全独立的消息处理管道,而且这个管道比为ASP.NET MVC设计的管道更为复杂,功能也更为强大。虽然被命名为“ASP.NET Web API”,但是这个消息处理管道却是独立于ASP.NET平台的,这也是为什么ASP.NET Web API支持多种寄宿方式的根源所在。[本文已经同步到《How ASP.NET Web API Works?》] 为了让读者朋友们先对ASP.NET Web API具有一个感性认识,接下来我们以实例演示的形式创建一个简单的ASP.NET Web API应用。这是一个用于实现“联系人管理”的单页Web应用,我们以Ajax的形式调用Web API实现针对联系人的CRUD操作。[源代码从这里下载]
一、构建解决方案Visual Studio为我们提供了专门用于创建ASP.NET Web API应用的项目模板,借助于此项目模板提供的向导,我们可以“一键式”创建一个完整的ASP.NET Web API项目。在项目创建过程中,Visual Studio会自动为我们添加必要的程序集引用和配置,甚至会为我们自动生成相关的代码,总之一句话:这种通过向导生成的项目在被创建之后其本身就是一个可执行的应用。 对于IDE提供的这种旨在提高生产效率的自动化机制,我个人自然是推崇的,但是我更推荐读者朋友们去了解一下这些自动化机制具体为我们做了什么?做这些的目的何在?哪些是必需的,哪些又是不必要的?正是基于这样的目的,在接下来演示的实例中,我们将摒弃Visual Studio为我们提供的向导,完全在创建的空项目中编写我们的程序。这些空项目体现在如右图所示的解决方案结构中。 如右图所示,整个解决方案一共包含6个项目,上面介绍的作为“联系人管理器”的单页Web应用对应着项目WebApp,下面的列表给出了包括它在内的所有项目的类型和扮演的角色。
二、定义Web API在正式定义Web API之前,我们需要在项目Common中定义代表联系人的数据类型Contact。简单起见,我们仅仅为Contact定义了如下几个简单的属性,它们分别代表联系人的ID、姓名、联系电话、电子邮箱和联系地址。 1: public class Contact 2: {
3: string Id { get; set; } 4: string Name { get; set; } 5: string PhoneNo { get; set; } 6: string EmailAddress { get; set; } 7: string Address { get; set; } 8: }
表现为HttpController的Web API定义在WebApi项目之中,我们一般将ApiController作为继承的基类。ApiController定义在“System.Web.Http.dll”程序集中,我们可以在目录“%ProgramFiles%Microsoft ASP.NETASP.NET Web Stack 5Packages”中找到这个程序集。具体来说,该程序集存在于子目录“Microsoft.AspNet.WebApi.Core.5.0.0libnet45”中。 Web API体现在如下所示的ContactsController类型中。在该类型中,我们定义了Get、Post、Put和Delete这4个Action方法,它们分别实现了针对联系人的查询、添加、修改和删除操作。Action方法Get具有一个表示联系人ID的可缺省参数,如果该参数存在则返回对应的联系人,否则返回整个联系人列表。由于ASP.NET Web API默认实现了Action方法与HTTP方法的映射,所以方法名也体现了它们各自所能处理请求必须采用的HTTP方法。 class Global : System.Web.HttpApplication
protected void Application_Start(object sender,EventArgs e) 4: {
5: GlobalConfiguration.Configuration.Routes.MapHttpRoute(
6: Name : "DefaultApi", 7: routeTemplate : "api/{controller}/{id}", 8: defaults : new { id = RouteParameter.Optional }); 9: }
10: }
如上面的代码片断所示,路由注册是通过调用代表全局路由表的HttpRouteCollection对象的扩展方法MapHttpRoute来完成的。GlobalConfiguration的静态属性Configuration返回一个代表当前配置的HttpConfiguration对象,全局路由表就注册在它的Routes属性上。 如果你了解ASP.NET MVC的路由注册,可能觉得奇怪:注册路由的模板中并没有表示目标Action的路由参数,ASP .NET Web API如何根据请求确定哪个Action方法应该被调用呢?答案其实很简单:它能根据请求采用HTTP方法来确定目标Action方法。当然,在注册路由模板中提供代表Action名称的路由参数({action})也是支持的。 在默认情况下,通过Visual Studio(VS 2012或者VS 2013,本书采用的是后者)创建的Web应用总是使用IIS Express作为服务器,它会自动为我们指定一个可用的端口号。为了更好地模拟真实发布环境,同时避免“跨域资源共享”带来的困扰,我们采用本地IIS作为服务器。如下图所示,WebHost项目在IIS中映射的Web应用采用的URL为“http://localhost/webhost”。 实际上到此为止,Web API的Web Host寄宿工作就已经完成,我们可以利用浏览器来调用寄宿的Web API来判断寄宿工作是否成功。由于浏览器在默认情况下访问我们在地址栏中输入的地址总是采用HTTP-GET请求,所以我们只能利用它来调用支持HTTP-GET的Action方法,即定义在ContactsController中的Get方法。 根据我们注册的路由,如果我们访问目标地址“http://localhost/webhost/api/contacts”可以获得所有联系人列表;如果目标地址为“http://localhost/webhost/api/contacts/001”,则可以得到ID为“001”的联系人信息,右图证实了这一点。 从右图可以看到,我们采用的浏览器为Chrome,获取的联系人列表总是表示为XML,这是为什么呢?在前面介绍REST的时候,我们曾经提及一种旨在识别客户端期望的资源表示形式并被称为“内容协商”的机制,它可以根据请求携带的相关信息来判断客户端所期望的响应资源表现形式。 对于ASP.NET Web API来说,它会优先利用请求报头“Accept”携带的媒体类型来确定响应内容采用的表现形式。如下所示的是Chrome访问“http://localhost/webhost/api/contacts/001”发送请求的内容,它之所以会得到以XML表示的响应是因为“Accept”报头指定的媒体类型列表中只有“application/xml”被ASP.NET Web API支持。如果我们采用IE,请求的“Accept”报头将携带不同的媒体类型列表,我们实际上会得到以JSON格式表示的响应结果。 <configuration>
2: ...
3: system.webServer> modules runAllManagedModulesForAllRequests="true"> 5: remove name="WebDAVModule" /> </modules> 7: > 8: > ? 四、 以Self Host方式寄宿Web API与WCF类似,寄宿Web API不一定需要IIS的支持,我们可以采用Self Host的方式使用任意类型的应用程序(控制台、Windows Forms应用、WPF应用甚至是Windows Service)作为宿主。对于我们演示的实例来说,项目SelfHost代表的控制台程序就是一个采用Self Host寄宿模式的宿主。 对于SelfHost这么一个空的控制台应用来说,除了需要添加针对WebApi的项目引用之外,还需要添加如下4个程序集引用。除了程序集“System.Net.Http.dll”(它属于.NET Framework 原生的程序集)之外,其余3个均可以在目录“%ProgramFiles%Microsoft ASP.NETASP.NET Web Stack 5Packages”中找到。
通过上面的介绍我们可以看到以Web Host的方式寄宿Web API需要做的唯一一件事情是路由注册。但是对于Self Host来说,除了必需的路由注册外,我们还需要完成额外的一件事情,即手工加载定义了HttpController类型的程序集。整个寄宿工作通过如下几行简单的代码就可以实现。 5: Process();
6: Console.Read();
7: }
8:?
9: private async void Process() 10: {
11: //获取当前联系人列表 12: HttpClient httpClient = new HttpClient(); 13: HttpResponseMessage response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts"); 14: IEnumerable<Contact> contacts = await response.Content.ReadAsAsync<IEnumerable<Contact>>();
15: Console.WriteLine("当前联系人列表:"); 16: ListContacts(contacts);
17:?
18: //添加新的联系人 19: Contact contact = new Contact { Name = "王五",PhoneNo = "0512-34567890",1)">"wangwu@gmail.com" }; 20: await httpClient.PostAsJsonAsync<Contact>("http://localhost/selfhost/api/contacts",contact); 21: Console.WriteLine("添加新联系人“王五”:"); 22: response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts"); 23: contacts = await response.Content.ReadAsAsync<IEnumerable<Contact>>();
24: ListContacts(contacts);
25:?
26: //修改现有的某个联系人 27: response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts/001"); 28: contact = (await response.Content.ReadAsAsync<IEnumerable<Contact>>()).First();
29: contact.Name = "赵六"; 30: contact.EmailAddress = "zhaoliu@gmail.com"; 31: await httpClient.PutAsJsonAsync<Contact>("http://localhost/selfhost/api/contacts/001",contact); 32: Console.WriteLine("修改联系人“001”信息:"); 33: response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts"); 34: contacts = await response.Content.ReadAsAsync<IEnumerable<Contact>>();
35: ListContacts(contacts);
37: //删除现有的某个联系人 38: await httpClient.DeleteAsync("http://localhost/selfhost/api/contacts/002"); 39: Console.WriteLine("删除联系人“002”:"); 40: response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts"); 41: contacts = await response.Content.ReadAsAsync<IEnumerable<Contact>>();
42: ListContacts(contacts);
43: }
44:?
45: private void ListContacts(IEnumerable<Contact> contacts) 46: {
47: foreach (Contact contact in contacts) 48: {
49: Console.WriteLine("{0,-6}{1,-6}{2,-20}{3,-10}",contact.Id,contact.Name,contact.EmailAddress,contact.PhoneNo); 50: }
51: Console.WriteLine();
52: }
53: }
如上面的代码片段所示,我们创建了一个HttpClient对象并调用其GetAsync方法向目标地址“http://localhost/selfhost/api/contacts”发送了一个GET请求,返回的对象HttpResponseMessage表示接收到的响应。该HttpResponseMessage对象的Content属性返回一个表示响应主体内容的HttpContent对象,我们调用其ReadAsAsync<T>方法读取响应主体内容并将其反序列化成一个Contact集合。我们将表示当前联系人列表的Contact集合输出在控制台上。 我们接下来调用HttpClient的PostAsJsonAsync<T>方法向目标地址“http://localhost/selfhost/api/contacts”发送一个POST请求以添加一个新的联系人。正如方法名称所体现的,作为参数的Contact对象将以JSON格式被写入请求的主体部分。请求被正常发送并接收到响应之后,我们会打印出当前联系人列表。 在此之后,我们向目标地址“http://localhost/selfhost/api/contacts/001”发送一个GET请求以获取ID为“001”的联系人。在修改了联系人的姓名(“赵六”)和电子邮箱(“zhaoliu@gmail.com”)之后,我们将其作为参数调用HttpClient的PutAsJsonAsync<T>方法,以此向目标地址“http://localhost/selfhost/api/contacts/001”发送一个PUT请求以更新对应联系人的相关信息。联系人信息是否正常更新同样通过输出当前所有联系人列表来证实。 我们最后调用HttpClient的DeleteAsync方法向地址“http://localhost/selfhost/api/contacts/002”发送一个DELETE请求以删除ID为“002”的联系人并通过输出当前所有联系人列表来证实删除参数是否成功完成。 我们在运行宿主程序SelfHost之后启动此ConsoleApp程序,会在控制台上得到下所示的输出结果,由此可以看出通过调用HttpClient的GetAsync、PostAsJsonAsync、PutAsJsonAsync和DeleteAsync方法帮助我们成功完成了针对联系人的获取、添加、修改和删除。 <!DOCTYPE html 2: html xmlns="http://www.w3.org/1999/xhtml">
3: headtitle>联系人管理器link href="css/bootstrap.min.css" rel="stylesheet"> 6: 7: body 8: ... script src="Scripts/jquery-1.10.2.min.js"></script> 1:?
2: <script src="Scripts/bootstrap.min.js"></script> "Scripts/knockout-3.0.0.js"></script>
"Scripts/viewmodel.js"></script>
10: > 11: > jQuery,这个“地球人都知道”的JavaScript框架,我们无须对它作任何介绍了。Bootstrap 是集 HTML、CSS 和 JavaScript 于一体,是由微博的先驱 Twitter 在2011年8月开源的整套前端解决方案,Web 开发人员利用它能够轻松搭建出具有清爽风格的界面以及实现良好的交互效果的Web应用。Bootstrap是ASP.NET MVC 5默认支持的框架,当我们利用Visual Stduio创建一个ASP.NET MVC项目时,项目目录下就包含了Bootstrap相关的CSS和JavaScript文件。 在本例中,我们主要利用jQuery来实现以Ajax方式调用Web API,同时它也是其他两个框架(Bootstrap和KnockOut)的基础框架。至于Bootstrap,我们则主要使用它的页面布局功能和它提供的CSS。除此之外,“编辑联系人”对话框就是利用Bootstrap提供的JavaScript组件实现的。 MVVM与Knockout考虑到可能有人对Knockout(以下简称KO)这个JavaScript框架不太熟悉,在这里我们对它作一下概括性的介绍。KO是微软将应用于WPF/Silverlight的MVVM模式在Web上的尝试,这是一个非常有用的JavaScript框架。对于面向数据的Web应用来说,MVVM模式是一项不错的选择,它借助框架提供的“绑定”机制使我们无需过多关注UI(HTML)的细节,只需要操作绑定的数据源。MVVM最早被微软应用于WPF/SL的开发,所以针对Web的MVVM框架来说,Knockout(以下简称KO)无疑是“根正苗红”。 MVVM可以看成是MVC模式的一个变体,Controller被View Model取代,但两者具有不同的职能,三元素之间的交互也不相同。以通过KO实现的MVVM为例,其核心是“绑定”,我个人又将其分为“数据的绑定”和“行为的绑定”。所谓数据的绑定,就是将View Model定义的数据绑定到View中的UI元素(HTML元素)上,KO同时支持单向和双向绑定。行为绑定体现为事件注册,即View中UI元素的事件(比如某个<button>元素的click事件)与View Model定义的方法(function)进行绑定。 如右图所示,用户行为(比如某个用户点击了页面上的某个按钮)首先触发View的某个事件,与之绑定的定义在View Model中的EventHandler(View Model的某个方法成员)被自动执行。它可以执行Model,并修改自身维护的数据,如果View和View Model的数据绑定是双向的,用户在界面上输入的数据可以被View Model捕获,View Model对数据的更新可以自动反映在View上。这样的好处显而易见:我们在通过JavaScript定义UI处理逻辑的时候,无需关注View的细节(View上的HTML),只需要对自身的数据进行操作即可。 我们通过一个简单的例子来说明两种绑定在KO中的实现。假设我们需要设计如左图所示的“地址编辑器页面”,在页面加载的时候它会将默认的地址信息绑定到表示省、市、区和街道的文本框和显示完整地址信息的<span>元素上,当用户在文本框中输入新的值并点击“确认”按钮后,显示的完整地址会相应的变化。 我们可以利用KO按照如下的方式来实现地址信息的绑定和处理用户提交的编辑确认请求。我们首先需要通过一个函数来创建表示View Model的“类”,需要绑定的数据和函数将作为该类的成员,组成View的HTML元素则通过内联的“data-bind”属性实现数据绑定和事件注册。我们最终需要创建View Model对象,并将其作为参数调用ko.applyBindings方法将绑定应用到当前页面。 function ViewModel() {
2: self = this; 3: self.contacts = ko.observableArray(); //当前联系人列表 4: self.contact = ko.observable(); //当前编辑联系人 //获取当前联系人列表
7: self.load = function () { 8: $.ajax({
9: url : "http://localhost/webhost/api/contacts",1)"> 10: type : "GET",1)"> 11: success : function (result) { 12: self.contacts(result);
13: }
14: });
15: };
//弹出编辑联系人对话框
18: self.showDialog = function (data) { //通过Id判断"添加/修改"操作
20: if (!data.Id) { 21: data = { ID: "",Name: 22: Address: "" } 23: }
24: self.contact(data);
25: $(".modal").modal('show'); 26: };
27:?
28: //调用Web API添加/修改联系人信息 29: self.save = function () { 30: $('hide'); 31: if (self.contact().Id) { 32: $.ajax({
33: url : "http://localhost/webhost/api/contacts/" + self.contact.Id, 34: type : "PUT", 35: data : self.contact(),
36: success : function () {self.load();} 37: });
38: }
39: else { 40: $.ajax({
41: url?? : 42: type : "POST", 43: data : self.contact(),
44: success : function () {self.load();} 45: });
46: }
47: };
48:?
49: //删除现有联系人 50: self.delete = function (data) { 51: $.ajax({
52: url : "http://localhost/webhost/api/contacts/" + data.Id, 53: type : "DELETE", 54: success : function () {self.load();} 55: });
56: };
57:?
58: self.load();
59: }
60:?
61: $(function () { 62: ko.applyBindings(new ViewModel()); 63: });
对于上面定义的作为整个页面View Model的“类型”(ViewModel)来说,它具有两个“数据”成员(其实是函数)contacts和contact,前者表示当前联系人列表,后者则表示当前修改或者添加的联系人。contacts和contact分别通过调用方法observableArray和observable创建,所以它们均支持双向绑定。这两个数据成员分别被绑定到呈现当前联系人的表格和用于编辑联系人信息的对话框中。除了这两个数据成员之外,我们还定义了4个方法成员。
HTML如下所示的是页面主体部分包含的HTML,ViewModel的相关成员会绑定到相应HTML元素上。整个内容大体包含两个部分,第一部分用于呈现当前联系人列表,第二部分在用于定义弹出的对话框。 |