WCF REST服务
一个基于REST的WEB服务操作请求只需要体现两点 一是资源的唯一标识 二是操作类型 资源的唯一标识通过URI来完成 而操作类型通过HTTP方法(GET/HEAD POST PUT DELETE)来表示 而如果服务采用基于SOAP 那么操作是通过<Action>来表示的 REST从资源的角度来观察整个网络 整个网络的资源由URI确定 而客户端的应用只需要通过URI来获取资源的表征 获得这些表征致使这些应用程序转变了其状态 随着不断获取资源的表征 客户端应用不断地在转变着其状态 这就是所谓的表征状态转移(Representational State Transfer)其实这里面的概念说直白些 就是通过URI的渠道 我们能得到我们想要的资源 资源的方式可以是XML、JSON或者是HTML 取决于读者是机器还是人 是消费web服务的客户软件还是web浏览器 当然也可以是任何其他的方式 REST通俗点来讲究是一种使用针对资源进行增删改查操作、使用HTTP协议来通讯的服务端-客户端交互的通信风格 如果两个通信的程序都是相同的架构(比如.Net Application) 则使用WCF SOAP会非常方便 但如果客户有使用浏览器调用服务、js调用服务 那么服务最好还是设计成Rest风格的 也就是说现阶段其实Rest的开放性、通用性都要优于SOAP
REST服务的体现
一个REST服务需要体现以下几点
URI
用于标识某一互联网资源名称的字符串 如一个提供获取所有员工列表的资源 可以定义为这样的URI来标识
http://www.cnblogs.com/CRMService/Employees
消息格式
请求消息或回复的消息格式可以是JSON XML YAML HTML 等
请求方法
资源所接受的HTTP请求方式 有PUT(增)DELETE(删)POST(改)GET(查)
一个简单的REST服务
下面来一个实例演示 实现一个基本的REST服务 创建一个Service.Interface的项目 添加对System.ServiceModel、System.ServiceModel.Web、System.Runtime.Serialization的引用 添加一个Employee类 如
1 using System.ServiceModel; 2 using System.Runtime.Serialization; 3 4 namespace Service.Interface 5 { 6 [DataContract(Namespace = "http://www.cnblogs.com/")] 7 public class Employee 8 { 9 [DataMember] 10 public string ID { get; set; } 11 [DataMember] 12 public string Name { get; set; } 13 [DataMember] 14 public string Department { get; set; } 15 [DataMember] 16 public string Grade { get; set; } 17 18 public override string ToString() 19 { 20 return string.Format("ID:{0.-5}姓名:{1,-5}级别:{2,-4}部门:{3}",ID,Name,Grade,Department); 21 } 22 } 23 }
再添加一个服务契约接口
1 using System.ServiceModel; 2 using System.ServiceModel.Web; 3 4 namespace Service.Interface 5 { 6 [ServiceContract(Namespace = "http://www.cnblogs.com/")] 7 public interface IEmployees 8 { 9 [WebGet(UriTemplate="all")] 10 IEnumerable<Employee> GetEmployees(); 11 12 [WebGet(UriTemplate="{id}")] 13 Employee GetByID(string id); 14 15 [WebInvoke(UriTemplate="/",Method="POST")] 16 void AddEmployee(Employee employee); 17 18 [WebInvoke(UriTemplate="{id}",Method="DELETE")] 19 void DeleteEmployee(string id); 20 21 [WebInvoke(UriTemplate="/",Method="PUT")] 22 void UpdateEmployee(Employee employee); 23 } 24 }
创建Service控制台项目 添加对Service.Interface和System.ServiceModel.Web的引用 添加一个EmployeeService类实现IEmployees契约接口
1 using Service.Interface; 2 using System.ServiceModel.Web; 3 4 namespace Service 5 { 6 public class EmployeeService:IEmployees 7 { 8 private static List<Employee> employees = new List<Employee> 9 { 10 new Employee{ ID="001", Name="sam" , Grade="G6", Department="开发部"}, 11 new Employee{ ID="002", Name="korn" , Grade="G7", Department="人事部"} 12 }; 13 14 //获取所有员工 15 public IEnumerable<Employee> GetEmployees() 16 { 17 return employees; 18 } 19 20 //根据id查询某位员工 21 public Employee GetByID(string id) 22 { 23 var employee=employees.Where(n => n.ID == id).SingleOrDefault(); 24 if (employee == null) 25 { 26 WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.NotFound; 27 } 28 return employee; 29 } 30 31 //添加新员工 32 public void AddEmployee(Employee employee) 33 { 34 var TestEmployee = employees.Where(n => n.ID == employee.ID).SingleOrDefault(); 35 if (TestEmployee == null) 36 { 37 employees.Add(employee); 38 } 39 else 40 { 41 WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.Conflict; 42 } 43 } 44 45 //删除员工 46 public void DeleteEmployee(string id) 47 { 48 var employee=this.GetByID(id); 49 if (employee != null) 50 { 51 employees.Remove(employee); 52 } 53 } 54 55 //更新员工 56 public void UpdateEmployee(Employee employee) 57 { 58 this.DeleteEmployee(employee.ID); 59 employees.Add(employee); 60 } 61 } 62 }
现在将EmployeeService服务寄宿在控制台进程中 先配置终结点 终结点绑定使用的是webHttpBinding 如下
1 <?xml version="1.0" encoding="utf-8" ?> 2 <configuration> 3 <system.serviceModel> 4 <services> 5 <service name="Service.EmployeeService"> 6 <endpoint address="http://127.0.0.1:8888/employees" binding="webHttpBinding" contract="Service.Interface.IEmployees"/> 7 </service> 8 </services> 9 </system.serviceModel> 10 </configuration>
寄宿使用WebServiceHost
1 using System.ServiceModel.Web; 2 3 namespace Service 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 using (WebServiceHost host = new WebServiceHost(typeof(EmployeeService))) 10 { 11 host.Open(); 12 Console.Read(); 13 } 14 } 15 } 16 }
因为服务时基于web的 服务中有两个服务操作的请求方式被设置为GET 它们是GetEmployees方法和GetByID方法 所以我们可以通过浏览器键入服务的终结点地址来调用服务 打开浏览器 输入终结点地址Get的终结点别名
1 http://127.0.0.1:8888/employees/all
我们调用了GetEmployees服务操作 服务端以xml返回了结果
接下来测试GetByID操作 结果如下
1 http://127.0.0.1:8888/employees/001
WebGet特性和WebInvoke特性
ns:System.ServiceModel.Web
WebGet特性应用在契约操作上 表示此操作接受的请求类型为HTTP GET请求 而WebInvoke特性应用在契约操作上 表示此操作接受的请求类型为HTTP的其它三种请求(除GET)请求 两个特性实现了IOperationBehavior接口 REST服务是基于WEB的 所以契约操作可以不应用OperationContract特性 只需根据需要选择使用WebGet或者WebInvoke特性 除了WebInvoke的Method属性 两个特性都具有如下属性
RequestFormat属性
设置请求消息的格式 值为WebMessageFormat枚举 可能的值如Json、Xml 默认值Xml
ResponseFormat属性
设置回复消息的格式 值为WebMessageFormat枚举 可能的值如Json、Xml 默认值Xml
UriTemplate属性
此属性用于设置当前操作与服务终结点需要组成的操作地址 UriTemplate是一个URI模板 它有两种类型的Segment参数 一种是静态参数 另一种是动态参数 看例子
1 [WebGet(UriTemplate="{id}")] 2 Employee GetByID(string id);
使用中括号括起来的参数 我们成为动态Segment参数 此例子中就是设了一个动态的Segment参数 假设服务的终结点地址为http://127.0.0.1:8888/employees 那么为契约操作应用WebGet或WebInvoke的UriTemplate为{id} 则客户端调用该操作时地址的格式类似这样 http://127.0.0.1:8888/employees/001 不使用中括号括起来的参数 称为静态Segment参数 静态的参数在请求的URI中必须提供一样的值 如果把例子改为
1 [WebGet(UriTemplate="sam")] 2 [Description("根据id查询某位员工")] 3 Employee GetByID(string id);
则客户端调用该操作时地址的格式必须是这样 http://127.0.0.1:8888/employees/sam 静态参数和动态参数可以组合使用 所以可以如下定义UriTemplate
1 [WebGet(UriTemplate = "employee'sID-{id}")] 2 [Description("根据id查询某位员工")] 3 Employee GetByID(string id);
对应的操作地址为:http://127.0.0.1:8888/employees/employee'sID-001 还可以为动态参数提供默认值 只需这样做{id=001}
BodyStyle属性
设置请求消息或回复消息的消息主体的风格 值为WebMessageBodyStyle枚举 可能的值如下
Bare
消息主体部分仅仅包含序列化后的内容 它BodyStyle的默认值
Wrapped
消息主体部分不但包含序列化后的内容 还会在内容外部设置一个封套 封套名称就是当前操作的名称+result
WrappedRequest
封套请求消息 但不封套回复消息
WrappedResponse
封套回复消息 但不封套请求消息
继续使用前面的简单的REST服务的例子 现在做一些测试来看看设置了WebGet或WebInvoke的BodyStyle属性后具有怎样的效果
1 [ServiceContract(Namespace = "http://www.cnblogs.com/")] 2 public interface IEmployees 3 { 4 [WebGet(UriTemplate="all",ResponseFormat=WebMessageFormat.Json,BodyStyle=WebMessageBodyStyle.Bare)] 5 IEnumerable<Employee> GetEmployees(); 6 7 //其它操作略…… 8 }
在浏览器键入终结点地址来调用该操作
1 http://127.0.0.1:8888/employees/all
服务返回的结果如下 我们将回复消息的格式设为了JSON 并指定了回复消息主体的风格为Bare 即只返回序列化后的内容 无封套
1 [ 2 {"Department":"开发部","Grade":"G6","ID":"001","Name":"sam"}, 3 {"Department":"人事部","Grade":"G7","ID":"002","Name":"korn"} 4 ]
再看将消息主体风格设为Wrapped后的效果
1 [WebGet(UriTemplate="all",ResponseFormat=WebMessageFormat.Json,BodyStyle=WebMessageBodyStyle.Wrapped)] 2 IEnumerable<Employee> GetEmployees();
结果为
1 { 2 "GetEmployeesResult": 3 [ 4 {"Department":"开发部","Grade":"G6","ID":"001","Name":"sam"}, 5 {"Department":"人事部","Grade":"G7","ID":"002","Name":"korn"} 6 ] 7 }
Description特性
ns:System.ComponentModel
此特性表示契约操作的描述信息 可以在契约操作上应用此特性 应用后通过开启REST服务帮助页面 那么调用服务的客户端就可以打开服务的帮助页面查看关于服务的所有操作的描述信息 如
1 using System.ComponentModel; 2 public interface IEmployees 3 { 4 [WebGet(UriTemplate="all",ResponseFormat=WebMessageFormat.Json,BodyStyle=WebMessageBodyStyle.Wrapped)] 5 [Description("获取所有员工")] 6 IEnumerable<Employee> GetEmployees(); 7 }
WebHttpBehavior
此类表示REST服务的终结点行为之一 它的属性如下
helpEnabled属性
WCF4.0为REST服务提供了帮助页面的功能 此功能默认是关闭的 可以通过设置服务端的终结点行为WebHttp的helpEnabled为true来开启此功能
1 <?xml version="1.0" encoding="utf-8" ?> 2 <configuration> 3 <system.serviceModel> 4 <behaviors> 5 <endpointBehaviors> 6 <behavior> 7 <webHttp helpEnabled="true"/> 8 </behavior> 9 </endpointBehaviors> 10 </behaviors> 11 <services> 12 <service name="Service.EmployeeService"> 13 <endpoint address="http://127.0.0.1:8888/employees" binding="webHttpBinding" contract="Service.Interface.IEmployees"/> 14 </service> 15 </services> 16 </system.serviceModel> 17 </configuration>
我们将帮助页面开启并结合应用在契约操作上的Description特性 开启服务 在浏览器地址栏输入http://127.0.0.1:8888/employees/Help 打开的帮助页面如下所示
defaultBodyStyle属性
表示如果契约操作没有通过应用WebGet或WebInvoke的BodyStyle属性的值来指定某个操作在消息主体中的显示风格 那么就会使用此属性设置的默认风格 值为Bare、Wrapped、WrappedRequest、WrappedResponse
automaticFormatSelectionEnabled属性
布尔值 契约操作是否使用默认的消息格式
如果契约操作通过应用WebGet或WebInvoke的ResponseFormat属性指定了某个操作的回复消息的消息格式 则使用该格式
如果HTTP消息报头具有Accept报头 则自动根据该报头来决定回复消息的格式
如果HTTP消息报头具有Content-Type报头 则自动根据该报头来决定回复消息的格式
如果设置了WebHttp的defaultOutgoingResponseFormat属性 则决定回复消息使用该属性指定的格式
以上都没有显示设置 则使用默认的Xml消息格式
AspNetCacheProfile特性
ns:System.ServiceModel.Web
此特性表示输出缓存 ASP.NET输出缓存机制允许我们针对整个WEB页面或页面的某个部分最终呈现的HTML进行缓存 对于后续针对相同资源的请求 只需要直接将缓存的HTML呈现给客户端而无需再次使用服务端程序对请求进行处理 以达到提高服务端计算性能 节约带宽的目的 下面来演示如何在WCF REST服务上使用缓存机制
要建立的几个项目结构如图
Client控制台项目
Service类库项目
Service.Interface类库项目
WebService网站项目 (右击解决方案- 新建网站 - WCF服务 - 选择网站存储路径 并命名为WebService)
在Service.Interface中创建一个用于获取时间的服务契约
1 using System.ServiceModel; 2 using System.ServiceModel.Web; 3 4 namespace Service.Interface 5 { 6 [ServiceContract(Namespace = "http://www.cnblogs.com/")] 7 public interface ITime 8 { 9 [WebGet(UriTemplate="/Time")] 10 [AspNetCacheProfile("default")] 11 DateTime GetTime(); 12 } 13 }
契约操作GetTime方法应用了AspNetCacheProfile特性 表示寄宿在ASP.NET网站中的服务操作将使用在Web.config的outputCacheProfiles配置节中指定的缓存策略 接着在Service项目中实现契约接口ITime
1 using Service.Interface; 2 using System.ServiceModel.Activation; 3 4 namespace Service 5 { 6 [AspNetCompatibilityRequirements(RequirementsMode=AspNetCompatibilityRequirementsMode.Allowed)]//兼容模式 7 public class TimeService:ITime 8 { 9 public DateTime GetTime() 10 { 11 return DateTime.Now; 12 } 13 } 14 }
AspNetCompatibilityRequirements特性ASP.NET兼容模式 属性RequirementsMode的值为Allowed 表示此服务操作将以ASP.NET兼容模式来运行此服务 现在我们来将WCF服务寄宿在IIS上 首先在WebService中删除App_Code中的文件 接着将svc文件命名为Service.svc 打开它 输入以下指令
1 <%@ ServiceHost Language="C#" Debug="true" Service="Service.TimeService" Factory="System.ServiceModel.Activation.WebServiceHostFactory" %>
然后打开Web.config 作如下配置
1 <system.web> 2 <caching> 3 <outputCacheSettings> 4 <outputCacheProfiles> 5 <add name="default" duration="60" varyByParam="none"/> 6 </outputCacheProfiles> 7 </outputCacheSettings> 8 </caching> 9 </system.web> 10 11 12 <system.serviceModel> 13 <services> 14 <service name="Service.TimeService"> 15 <endpoint binding="webHttpBinding" contract="Service.Interface.ITime"/> 16 </service> 17 </services> 18 <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/> 19 </system.serviceModel>
caching/outputCacheSettings/outputCacheProfiles配置节添加了一个缓存策略 名为default 对应了契约操作GetTime所应用的特性[AspNetCacheProfile("default")] 该策略对操作的结果缓存了60秒 varyByParam属性表示忽略请求的URI中的查询字符
完成后打开IIS信息服务管理器 添加一个新网站 命名为myWeb 物理路径选择WebService 项目根目录 点击连接为 路径凭据选择特定用户 点击设置 输入登录计算机的用户名和密码后点击确定 IP设为127.0.0.1 端口默认80即可
最后启动该网站即可 这样WCF就寄宿在了IIS中 接下来在浏览器中输入http://127.0.0.1/Service.svc/Time 结果如下
1 <dateTime xmlns="http://schemas.microsoft.com/2003/10/Serialization/">2013-09-09T16:44:35.9183382+08:00</dateTime>
刷新该地址几次 得到的将是同样的结果 时间并无改变 正是因为该操作使用了缓存的缘故 我们也可以通过Client项目在控制台程序中调用该服务操作来测试缓存 控制台需要配置终结点 配置如下所示
1 <system.serviceModel> 2 <behaviors> 3 <endpointBehaviors> 4 <behavior name="webBehavior"> 5 <webHttp/> 6 </behavior> 7 </endpointBehaviors> 8 </behaviors> 9 <client> 10 <endpoint binding="webHttpBinding" address="http://127.0.0.1/Service.svc" name="timeservice" contract="Service.Interface.ITime" behaviorConfiguration="webBehavior"/> 11 </client> 12 </system.serviceModel>
因为创建的服务是REST的 所以终结点使用了webHttp行为
1 using System.ServiceModel; 2 using Service.Interface; 3 using System.Threading; 4 5 using (ChannelFactory<ITime> factory = new ChannelFactory<ITime>("timeservice")) 6 { 7 var proxy = factory.CreateChannel(); 8 //每隔2秒调用一次服务 可以看到10秒内重复调用服务操作的结果是一样的 9 for (var i = 0; i < 5; i++) 10 { 11 Console.WriteLine(proxy.GetTime()); 12 Thread.Sleep(1000); 13 } 14 Console.Read(); 15 }
结果如下
缓存的条件获取
缓存存在这样的问题 客户端每次请求得到的都是缓存 那么当服务端数据做了修改 客户端却不能得到最新的数据 实际上HTTP对条件获取提供了原生的支持 内部的具体实现有两种 第一种是当服务端第一次接收到某个请求后 除了将回复消息返回给客户端 同时还会为HTTP回复消息添加一个ETag报头 它会将客户端请求的数据的哈希值添加到ETag报头中与回复消息一并发往客户端 客户端接受到回复消息 当再次请求时 客户端会为HTTP请求添加一个If-None-Match报头 此报头的值就是ETag报头的值 服务端再次接收到该请求并取出If-None-Match报头的值 将其与服务端数据的哈希值进行匹配 如果完全相等 则不再处理这个请求 而是将回复消息的值设为304(未做改变)然后返回 这样客户端请求的数据就会从缓存中读取 如果服务端取出的If-None-Match报头的值与数据的哈希值不等 则会将新数据的哈希值放进ETag报头同时将结果存进回复消息主体一并返回给客户端 第二种是基于Last Modified Time(最近修改时间)来实现的 服务端会记录下最近一次对某个操作的修改时间 将此时间作为ETag报头添加到HTTP回复消息中 客户端再次请求相同的操作时 它会将上次请求后服务端回复的ETag的时间添加到If-Modifiled-Since报头中 服务端接收到该请求 取出If-Modifiled-Since中的时间与服务端针对某个操作的最近一次修改时间进行对比 如果相等 则将回复消息的值设为304(未做改变)然后返回 如果不相等 则会将新的时间添加到ETag报头中同时将新的数据返回给客户端 可以将前面的例子做一个修改 来测试条件获取 先打开浏览器访问这个地址 http://127.0.0.1/Service.svc/Time 结果如下
1 <dateTime xmlns="http://schemas.microsoft.com/2003/10/Serialization/">2013-09-09T16:44:35.9183382+08:00</dateTime>
接着修改契约操作 将GetTime操作的返回值改为string类型
1 [ServiceContract(Namespace = "http://www.cnblogs.com/")] 2 public interface ITime 3 { 4 [WebGet(UriTemplate="/Time")] 5 [AspNetCacheProfile("default")] 6 string GetTime(); 7 }
修改服务操作TimeService 将返回的时间格式化为中文日期
1 [AspNetCompatibilityRequirements(RequirementsMode=AspNetCompatibilityRequirementsMode.Allowed)]//兼容模式 2 public class TimeService:ITime 3 { 4 public string GetTime() 5 { 6 return DateTime.Now.ToString("yyyy年MM月dd HH时mm分ss秒"); 7 } 8 }
生成解决方案一次 再次刷新浏览器 可以看到 当服务端数据改变后 缓存是最新的数据了
1 <string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">2013年09月09 18时47分11秒</string>
WebOperationContext
此类与基于SOAP消息的OperationContext有类似的作用 表示操作的上下文 使用该对象可以设置出栈消息的相关信息和接收入栈消息的信息 设置后 信息会自动附加到消息上 它通过Current静态属性返回一个WebOperationContext实例对象 实例属性如下
IncomingRequest属性
返回一个表示入栈请求消息的上下文对象
IncomingResponse属性
返回一个表示入栈回复消息的上下文对象
OutgoingRequest属性
返回一个表示出栈请求消息的上下文对象
OutgoingResponse属性
返回一个表示出栈回复消息的上下文对象