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