[WCF REST] UriTemplate、UriTemplateTable与WebHttpDispatchOperationSelector

REST服务采用面向资源的架构,而资源通过URI进行标识和定位,所以URI在REST中具有重要的地位。对于WCF来说,服务调用请求的URI映射为某个具体的操作,所以服务端需要解决的是如何根据请求URI选择出对应的操作。如果采用SOAP,操作的选择是根据消息的<Action>报头来实现的,那么REST服务又采用怎样的操作选择机制呢?

目录
一、URI模板
二、UriTemplate
三、UriTemplateTable
四、WebHttpDispatchOperationSelector
五、实例演示、自定义OperationSelector实现基于URI模板的操作选择机制

一、URI模板

在定义服务契约的时候,我们可以通过应用在操作方法上的WebGetAttribute和WebInvokeAttribute特性的UriTemplate属性定义一个URI模板。如下面的代码片断所示,我们为契约接口ICalculator的Add操作定义了Uri模板"Add/{x}/{y}"),路经部分{x}和{y}对应着操作方法同名的参数。如果终结点地址为http://127.0.0.1:3721/calculatorservice,我们可以访问地址http://127.0.0.1:3721/calculatorservice/add/1/2调用Add操作并传入操作数1和2。

   1: [ServiceContract(Namespace = "http://www.artech.com/")]
   2: public interface ICalculator
   3: {
   4:     [WebGet(UriTemplate = "Add/{x}/{y}")]
   5:     double Add(double x, double y);
   6: }

关于URI模板定义的语法和规范,请参考http://msdn.microsoft.com/en-us/library/bb675245.aspx

二、UriTemplate

在Web HTTP编程模型中,URI模板通过具有如下定义的UriTemplate表示。UriTemplate具有一系列的构造函数重载,这些重载除了接受以字符串类表示的URI模板作为参数之外,还具有额外的一些参数。布尔类型的参数ignoreTrailingSlash表示是否需要忽略URI模板最右边的斜杠(“/”),而字典参数additionalDefaults用于指定默认变量值。

   1: public class UriTemplate
   2: {
   3:     //其他成员
   4:     public UriTemplate(string template);
   5:     public UriTemplate(string template, bool ignoreTrailingSlash);
   6:     public UriTemplate(string template, IDictionary<string, string> additionalDefaults);
   7:     public UriTemplate(string template, bool ignoreTrailingSlash, IDictionary<string, string> additionalDefaults); 
   8:  
   9:     public IDictionary<string, string> Defaults { get; }
  10:     public bool IgnoreTrailingSlash { get; }
  11:     public ReadOnlyCollection<string> PathSegmentVariableNames { get; }
  12:     public ReadOnlyCollection<string> QueryValueVariableNames { get; }
  13:  
  14:     public Uri BindByName(Uri baseAddress, IDictionary<string, string> parameters);
  15:     public Uri BindByName(Uri baseAddress, NameValueCollection parameters);
  16:     public Uri BindByName(Uri baseAddress, IDictionary<string, string> parameters, bool omitDefaults);
  17:     public Uri BindByName(Uri baseAddress, NameValueCollection parameters, bool omitDefaults);
  18:     public Uri BindByPosition(Uri baseAddress, params string[] values);
  19:  
  20:     public UriTemplateMatch Match(Uri baseAddress, Uri candidate);
  21: }

UriTemplate具有三个只读的属性。IgnoreTrailingSlash属性返回调用构造函数指定的同名参数,默认值为True,意味着在默认情况在模板字符串结尾指定的斜杠会被忽略。PathSegmentVariableNames和QueryValueVariableNames则返回路径表达式和查询字符串表达式中指定的变量名。

我们可以指定基地址和变量值调用BindByName方法得到一个完整的URI。变量值可以通过字典和NameValueCollection对象的形式指定,其中的Key和Value分别表示变量名和变量值。在BindByPosition方法中我们以字符串数组的形式指定变量值,URI模板中的变量会按照出现的先后顺利进行替换并最终得到一个完整的URI。

方法Match用于判断URI模板是否与指定的某个完整的URI匹配,被用于进行匹配比较的URI通过参数candidate表示,而第一个参数代表的是基地址。如果不匹配则返回Null,否则返回具有如下定义的UriTemplateMatch对象。

   1: public class UriTemplateMatch
   2: {
   3:     public Uri RequestUri { get; set; }
   4:     public Uri BaseUri { get; set; }
   5:     public UriTemplate Template { get; set; }
   6:  
   7:     public NameValueCollection BoundVariables { get; }
   8:     public NameValueCollection QueryParameters { get; }
   9:  
  10:     public Collection<string> RelativePathSegments { get; }
  11:     public Collection<string> WildcardPathSegments { get; }
  12:  
  13:     public object Data { get; set; }
  14: }

UriTemplateMatch属性Template返回的是调用Match方法的UriTemplate对象,而基地址和被用于进行匹配判断的Uri分别通过BaseUri和RequestUri属性返回。被绑定变量(变量名称和值)以及查询字符串参数(参数名称和值)分别通过NameValueCollection类型的属性BoundVariables和QueryParameters返回。属性RelativePathSegments返和WildcardPathSegments分别返回相对路径段和通配路径段。通过可读写属性Data,我们可以将任意一个对象附加在UriTemplateMatch上面。

三、UriTemplateTable

具有如下定义UriTemplateTable本质上是一个KeyValuePair<UriTemplate,

object>对象集合,我们可以使用任意类型的对象和某个UriTemplate对象关联。当我们指定某个Uri对象调用它的Match方法时,会遍历集合中的所有UriTemplate对象并调用它的Match方法,最终返回一个UriTemplateMatch集合。对于每个UriTemplateMatch对象,其Data属性直接上就是与对应UriTemplate关联的对象。

而MatchSingle方法被执行的时候会在内部调用Match方法,如果没有匹配的UriTemplate,返回Null;如果只有唯一匹配的UriTemplate,则返回对应的UriTemplateMatch对象;如果多个UriTemplate同时匹配指定的Uri,直接抛出异常。

   1: public class UriTemplateTable
   2: {   
   3:     public UriTemplateTable();
   4:     public UriTemplateTable(IEnumerable<KeyValuePair<UriTemplate, object>> keyValuePairs);
   5:     public UriTemplateTable(Uri baseAddress);
   6:     public UriTemplateTable(Uri baseAddress, IEnumerable<KeyValuePair<UriTemplate, object>> keyValuePairs);
   7:  
   8:     public void MakeReadOnly(bool allowDuplicateEquivalentUriTemplates);
   9:     public Collection<UriTemplateMatch> Match(Uri uri);
  10:     public UriTemplateMatch MatchSingle(Uri uri);
  11:  
  12:     public Uri BaseAddress { get; set; }
  13:     public Uri OriginalBaseAddress { get; }
  14:     public bool IsReadOnly { get; }
  15:     public IList<KeyValuePair<UriTemplate, object>> KeyValuePairs { get; }
  16: }

构成UriTemplateTable的KeyValuePair<UriTemplate,
object>集合通过只读属性KeyValuePairs返回,该属性在构造函数中被初始化。属性BaseAddress
表示基地址,可以在构造函数中初始化,也可以直接通过属性赋值的方式指定。只读属性OriginalBaseAddress表示在构造函数或者针对BaseAddress的属性赋值中指定的Uri,它和BaseAddress唯一不同之处在于:后者经过“标准化(Normalization)”。

   1: Uri baseAddress = new Uri("http://127.0.0.1:3721/calculatorservice");
   2: UriTemplateTable uriTemplateTable = new UriTemplateTable(baseAddress);
   3: Console.WriteLine("{0,-20}: {1}", "BaseAddress", uriTemplateTable.BaseAddress);
   4: Console.WriteLine("{0,-20}: {1}", "OriginalBaseAddress", uriTemplateTable.OriginalBaseAddress);

在如上所示的代码片断中,我们针对基地址http://127.0.0.1:3721/calculatorservice创建了一个UriTemplateTable对象,然后分别在控制台中打印出它的BaseAddress和OriginalBaseAddress属性表示的Uri。从如下所示的输出结果可以看出,OriginalBaseAddress正是我们指定的原生基地址,而经过标准化处理后的BaseAddress的路径部分全部大写,并且添加了后缀“/”。

   1: BaseAddress         : http://localhost/CALCULATORSERVICE/
   2: OriginalBaseAddress : http://127.0.0.1:3721/calculatorservice

UriTemplateTable的只读属性IsReadOnly表示是否处于只读状态,我们通过调用MakeReadOnly方法将此属性设置为True。一旦调用了该方法,我们便不允许对该UriTemplateTable作任何改变。MakeReadOnly具有一个布尔类型的参数allowDuplicateEquivalentUriTemplates表示是否允许存在多个结构等效的UriTemplate。如果该参数为False,多个结构等效UriTemplate的存在会导致异常的发生。

四、WebHttpDispatchOperationSelector

我们所说的服务调用实际上是针对寄宿服务的某个终结点的某个操作的调用,服务端运行时最终需要根据服务调用请求选择出正确的操作。对于针对SOAP的服务调用来说,我们一般通过其<Action>报头作为操作选择的依据,而对于REST服务来说,请求的地址决定了对应的操作。

WCF服务端运行时通过DispatchOperationSelector根据请求消息进行操作的选择,而Web HTTP编程模型通过自定义的DispatchOperationSelector实现了最终的操作选择,这就是我们接下来需要着重介绍的WebHttpDispatchOperationSelector类型。

WebHttpDispatchOperationSelector针对请求地址的操作选择机制是通过UriTemplateTable实现的。我们通过ServiceEndpoint对象创建WebHttpDispatchOperationSelector的时候,会遍历终结点契约的所有操作并获得通过WebGetAttribute/WebInvokeAttribute特性设置URI模板。然后根据URI模板创建UriTemplate对象并最终创建UriTemplateTable。在真正需要进行操作选择的时候,只需要调用该UriTemplateTable的MatchSingle方法并传入请求地址,如果匹配则表明UriTemplate对应的操作就是我们需要选择的操作。

五、实例演示、自定义OperationSelector实现基于URI模板的操作选择机制

为了让读者对WebHttpDispatchOperationSelector的操作选择策略有一个深刻的例子,我按照大致的原理自定义一个DispatchOperationSelector,我们将其命名为WebHttpOperationSelector。整个WebHttpOperationSelector的定义如下所示。

   1: public class WebHttpOperationSelector:IDispatchOperationSelector
   2: {
   3:     public IDictionary<string, UriTemplateTable> UriTemplateTables { get; private set; }    
   4:     public WebHttpOperationSelector(ServiceEndpoint endpoint)
   5:     {
   6:         this.UriTemplateTables = new Dictionary<string, UriTemplateTable>();
   7:         Uri baseAddress = endpoint.Address.Uri;
   8:         foreach (var operation in endpoint.Contract.Operations)
   9:         {
  10:             WebGetAttribute webGet = operation.Behaviors.Find<WebGetAttribute>();
  11:             WebInvokeAttribute webInvoke = operation.Behaviors.Find<WebInvokeAttribute>();
  12:             string method = (null != webGet) ? "GET" : webInvoke.Method;
  13:             UriTemplateTable uriTemplateTable;
  14:             if (!this.UriTemplateTables.TryGetValue(method, out uriTemplateTable))
  15:             {
  16:                 uriTemplateTable = new UriTemplateTable(baseAddress);
  17:                 this.UriTemplateTables.Add(method, uriTemplateTable);
  18:             }
  19:             string template = (null != 
  20:             webGet)?webGet.UriTemplate:webInvoke.UriTemplate;
  21:             uriTemplateTable.KeyValuePairs.Add(new KeyValuePair<UriTemplate, object>(new UriTemplate(template),operation.Name));
  22:         }
  23:     }
  24:  
  25:     public string SelectOperation(ref Message message)
  26:     {
  27:         if (!message.Properties.ContainsKey(HttpRequestMessageProperty.Name))
  28:         {
  29:             return "";
  30:         }
  31:         HttpRequestMessageProperty messageProperty = (HttpRequestMessageProperty)message.Properties[HttpRequestMessageProperty.Name];
  32:  
  33:         var address = message.Headers.To;
  34:         var method = messageProperty.Method;
  35:         UriTemplateTable uriTemplateTable = null;
  36:         if(!this.UriTemplateTables.TryGetValue(method, out uriTemplateTable))
  37:         {
  38:             return "";
  39:         }
  40:  
  41:         UriTemplateMatch match = uriTemplateTable.MatchSingle(address);
  42:         if(null == match)
  43:         {
  44:             return "";
  45:         }
  46:         return match.Data.ToString();
  47:     }
  48: }

WebHttpOperationSelector具有一个字典类型的属性UriTemplateTables,Key和Value分别代表请求消息的HTTP方法和与之对应的UriTemplateTable对象。我们基于一个ServiceEndpoint对象来创建WebHttpOperationSelector,在构造函数中我们对UriTemplateTables属性进行了初始化。从上面的代码片断我们可以看出UriTemplateTable中基于某个操作的UriTemplate对象与操作名称关联。

在真正实施操作选择的SelectOperation方法中,我们根据请求消息的HTTP方法从UriTemplateTables属性中得到对应的UriTemplateTable对象。然后以请求消息的<To>报头表示的Uri为参数调用UriTemplateTable的MatchSingle方法,如果该方法返回一个具体的UriTemplateMatch对象,其Data属性即为对应操作的名称。

为了验证WebHttpOperationSelector能够正确地根据请求消息的目标地址选择出对应的操作,我们通过一个简单的实例来验证。如下面的代码片断所示,我们为熟悉的计算服务定义了如下一个契约接口ICalculator。表示加、减、乘、除运算的四个方法应用了WebGetAttribute特性并定义相应的URI模板。

   1: [ServiceContract(Namespace = "http://www.artech.com")]
   2: public interface ICalculator
   3: {
   4:     [WebGet(UriTemplate = "Add/{x}/{y}")]
   5:     double Add(double x, double y);
   6:  
   7:     [WebGet(UriTemplate = "Substract/{x}/{y}")]
   8:     double Substract(double x, double y);
   9:  
  10:     [WebGet(UriTemplate = "Multiply/{x}/{y}")]
  11:     double Multiply(double x, double y);
  12:  
  13:     [WebGet(UriTemplate = "Divide/{x}/{y}")]
  14:     double Divide(double x, double y);
  15: }

然后我们定义如下一个静态方法GetOperationName借助于DispatchOperationSelector对象根据表示请求地址的address选择出正确的操作名称。在这个方法中,我们创建了一个空的消息并将传入的URI作为该消息的To报头,并通过添加一个HttpRequestMessageProperty类型的消息属性将HTTP方法设置为GET。最终将创建的消息作为参数调用DispatchOperationSelector的SelectOperation方法得到正确的操作名称。

   1: static string  GetOperationName(Uri address, 
   2:     IDispatchOperationSelector operationSelector)
   3: {
   4:     Message message = Message.CreateMessage(MessageVersion.None, "");
   5:     message.Headers.To = address;
   6:     HttpRequestMessageProperty messageProperty = new HttpRequestMessageProperty();
   7:     messageProperty.Method = "GET";
   8:     message.Properties.Add(HttpRequestMessageProperty.Name, messageProperty);
   9:     return operationSelector.SelectOperation(ref message);
  10: }

在如下的代码片断中,我们针对契约接口ICalculator类型创建了一个ServiceEndpoint对象,其地址为http://127.0.0.1:3721/calculatorservice,绑定类型为WebHttpBinding。然后基于该ServiceEndpoint创建我们定义WebHttpOperationSelector对象。最后我们创建了四个分别表示针对计算服务运算操作的Uri并调用GetOperationName方法测试是否能够根据我们自定义的WebHttpOperationSelector对象正确选择出相应的操作。

   1: EndpointAddress address = new EndpointAddress("http://127.0.0.1:3721/calculatorservice");
   2: Binding binding = new WebHttpBinding();
   3: ContractDescription contract = ContractDescription.GetContract(typeof(ICalculator));
   4: ServiceEndpoint endpoint = new ServiceEndpoint(contract, binding, address);
   5: WebHttpOperationSelector operationSelector = new WebHttpOperationSelector(endpoint);
   6:  
   7: Uri addAdress       =  new Uri("http://127.0.0.1:3721/calculatorservice/add/1/2");
   8: Uri substractAdress =  new Uri("http://127.0.0.1:3721/calculatorservice/substract/1/2");
   9: Uri multiplyAdress  =  new Uri("http://127.0.0.1:3721/calculatorservice/multiply/1/2");
  10: Uri divideAdress    =  new Uri("http://127.0.0.1:3721/calculatorservice/divide/1/2");
  11:  
  12: Console.WriteLine(GetOperationName(addAdress,operationSelector));
  13: Console.WriteLine(GetOperationName(substractAdress, operationSelector));
  14: Console.WriteLine(GetOperationName(multiplyAdress, operationSelector));
  15: Console.WriteLine(GetOperationName(divideAdress, operationSelector));

上面的程序执行之后在控制台上会输出如下所示的结果,它们正是与指定URI匹配的操作名称。

   1: Add
   2: Substract
   3: Multiply
   4: Divide

除了为帮助页面提供操作选择和对默认URI模板(应用在操作方法上的WebGetAttribute和WebInvokeAttribute特性并没有对UriTemplate属性进行显式设置)的支持外,WebHttpDispatchOperationSelector实现操作选择的核心逻辑与我们自定义的WebHttpOperationSelector基本类似。WebHttpDispatchOperationSelector最终通过终结点行为WebHttpBehavior(ApplyDispatchBehavior方法)应用到分发运行时上。

作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文链接

时间: 2024-02-19 11:28:49

[WCF REST] UriTemplate、UriTemplateTable与WebHttpDispatchOperationSelector的相关文章

WCF REST系列文章汇总(共9篇)

[01] 一个简单的REST服务实例 [02] WebHttpBinding与消息编码 [03] Web消息主体风格(Message Body Style) [04] 帮助页面与自动消息格式(JSON/XML)选择 [05] WebServiceHost有何特别之处? [06] UriTemplate.UriTemplateTable与WebHttpDispatchOperationSelector [07] 通过ASP.NET Output Caching实现声明式缓存 [08] 提高性能的一

十五天精通WCF——第十三天 用WCF来玩Rest

在我们玩wcf的时候,都会潜意识的觉得wcf就是通过soap协议交换消息的,并且可以在basic,tcp,msmq等等绑定中任意切换, 牛逼的一塌糊涂,但是呢,如果说哪一天wcf不再使用soap协议,而是采用json格式的字符串,是不是有一点颠覆你对wcf的认识的??? 从传统意义上说,wcf是非常重量级的,很明白的一个例子就是太多太多的配置,尤其是Behavior的配置,而且behavior对wcf来说又是重 中之重,它对wcf的扩展和性能又是最重要的,可恨的是wcf在binding,beha

《WCF全面解析》(上册)- 目录

第1章  WCF简介 (WCF Overview)    1    1.1  SOA的基本概念和设计思想    3    1.2  WCF是对现有分布式通信技术的整合    4    1.3  构建一个简单的WCF应用    6        步骤一.构建整个解决方案    7        步骤二.创建服务契约    8        步骤三  创建服务    8        步骤四  通过自我寄宿的方式寄宿服务    9        步骤五  创建客户端调用服务    12       

再说ExtJs与WCF之间的跨域访问

在前面文章ExtJs与WCF之间的跨域访问已经通过服务端代理的方式解决了 ExtJs与WCF跨域访问的问题,那个方案看起来并不怎么优雅,而当我在写过用 Restful方式调用WCF进行上传下载后,愕然发现原来WCF支持原生数据(Raw)的返 回,这就解决了ExtJs与Wcf之间进行跨域调用中的难题:返回数据必须满足 <script>格式.下面根据ExtJs与WCF之间的跨域访问中实现的项目,通过 Stream和ContentType的联合使用,返回原生数据给Extjs,从而实现跨域调用. 第一

ExtJs学习笔记(23)-ScriptTagProxy+XTemplate+WCF跨域取数据

ajax应用中跨域一直是一个非常麻烦的问题,目前也有一些解决办法,但要么比较麻烦,要么就不具备通用性,幸好ExtJs里的ScriptTagProxy提供了跨域读取数据的功能,而且在几大浏览器上都可以正常运行,但在使用过程中要注意几点: 1.服务端返回时,必须按以下格式返回: stcCallback1001({...}) 其中stcCallback1001中的1001是自动生成的,如果是分页提交的话,每再请求一次1001会变成1002,1003...类推 2.ExtJs官方的示例中虽然Script

ExtJs学习笔记(20)-利用ExtJs的Ajax与服务端WCF交互

ExtJs是一套非常不错的javascript UI库(第一次接触ExtJs的,可到官方网站http://www.extjs.com/deploy/dev/examples/samples.html看下示例.相信不少人会心动的),不仅组件丰富,效果漂亮,而且ExtJs集成的Ajax功能可以方便的与.Net的WCF进行交互. 这里我们将演示ExtJs的FormPanel从WCF加载数据,以及如何提交数据到WCF服务端 1.首先来定义一个用于传输信息的Class(实际开发中,可以是Linq to S

一个简单的WCF RESTFul服务

WCF的REST实例网上很多,这里是我这几天学习并实践通过的,算是个笔记吧 . 1.服务契约 [ServiceContract]public interface IRESTService{} 具体操作定义中,有如下几个参数要注意: 1.WebGet和WebInvoke的区别好像就是Method的定义不同,WebGet使用 "GET",WebInvoke则更灵活. 2.UriTemplate用{value}对应 参数列表. 3.WebMessageFormat包括XML和JSON,网上有

WCF 3.5对HTTP编程的增强

Justin Smith在MSDN杂志上发表了文章<使用 WCF 和 .NET Framework 3.5 进行 HTTP 编程>,畅谈了WCF 3.5对于HTTP编程的改进.以下几点值得关注: .NET Framework 3.5 中的 WCF 构建于 .NET Framework 3.0 的扩展点 之上,从而为构建符合 Web 原则的服务提供一流的支持.它包含一个易于使用 的 HTTP 编程模型.JavaScript Object Notation (JSON) 消息传递功能,以及 新的整

使用WCF的Web编程模型开发REST风格的Web Service

WCF中的Web编程模型提供了一种以REST风格来设计Web Service的功能,它不同于以往基于SOAP或者WS-*规范的Web Service,而是以URI和http协议为中心的.对于操作的每一个资源有唯一的标志符,而利用不同的http动作(例如GET,POST,PUT,DELETE)来对这些资源进行相应的操作.同时该模型中还提供URI Template,它是用来定义参数化的URI,使URI的某些部分作为参数在服务中使用.可能这样解释十分含糊不清,下面用一个小例子来说明这种Web编程模型.