[WCF REST] UriTemplate、UriTemplateTable與WebHttpDispatchOperationSelector
REST服務采用麵向資源的架構,而資源通過URI進行標識和定位,所以URI在REST中具有重要的地位。對於WCF來說,服務調用請求的URI映射為某個具體的操作,所以服務端需要解決的是如何根據請求URI選擇出對應的操作。如果采用SOAP,操作的選擇是根據消息的<Action>報頭來實現的,那麼REST服務又采用怎樣的操作選擇機製呢?
目錄
一、URI模板
二、UriTemplate
三、UriTemplateTable
四、WebHttpDispatchOperationSelector
五、實例演示、自定義OperationSelector實現基於URI模板的操作選擇機製
在定義服務契約的時候,我們可以通過應用在操作方法上的WebGetAttribute和WebInvokeAttribute特性的UriTemplate屬性定義一個URI模板。如下麵的代碼片斷所示,我們為契約接口ICalculator的Add操作定義了Uri模板"Add/{x}/{y}"),路經部分{x}和{y}對應著操作方法同名的參數。如果終結點地址為https://127.0.0.1:3721/calculatorservice,我們可以訪問地址https://127.0.0.1:3721/calculatorservice/add/1/2調用Add操作並傳入操作數1和2。
1: [ServiceContract(Namespace = "https://www.artech.com/")]
2: public interface ICalculator
3: {
4: [WebGet(UriTemplate = "Add/{x}/{y}")]
5: double Add(double x, double y);
6: }
關於URI模板定義的語法和規範,請參考https://msdn.microsoft.com/en-us/library/bb675245.aspx
在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本質上是一個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("https://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);
在如上所示的代碼片斷中,我們針對基地址https://127.0.0.1:3721/calculatorservice創建了一個UriTemplateTable對象,然後分別在控製台中打印出它的BaseAddress和OriginalBaseAddress屬性表示的Uri。從如下所示的輸出結果可以看出,OriginalBaseAddress正是我們指定的原生基地址,而經過標準化處理後的BaseAddress的路徑部分全部大寫,並且添加了後綴“/”。
1: BaseAddress : https://localhost/CALCULATORSERVICE/
2: OriginalBaseAddress : https://127.0.0.1:3721/calculatorservice
UriTemplateTable的隻讀屬性IsReadOnly表示是否處於隻讀狀態,我們通過調用MakeReadOnly方法將此屬性設置為True。一旦調用了該方法,我們便不允許對該UriTemplateTable作任何改變。MakeReadOnly具有一個布爾類型的參數allowDuplicateEquivalentUriTemplates表示是否允許存在多個結構等效的UriTemplate。如果該參數為False,多個結構等效UriTemplate的存在會導致異常的發生。
我們所說的服務調用實際上是針對寄宿服務的某個終結點的某個操作的調用,服務端運行時最終需要根據服務調用請求選擇出正確的操作。對於針對SOAP的服務調用來說,我們一般通過其<Action>報頭作為操作選擇的依據,而對於REST服務來說,請求的地址決定了對應的操作。
WCF服務端運行時通過DispatchOperationSelector根據請求消息進行操作的選擇,而Web HTTP編程模型通過自定義的DispatchOperationSelector實現了最終的操作選擇,這就是我們接下來需要著重介紹的WebHttpDispatchOperationSelector類型。
WebHttpDispatchOperationSelector針對請求地址的操作選擇機製是通過UriTemplateTable實現的。我們通過ServiceEndpoint對象創建WebHttpDispatchOperationSelector的時候,會遍曆終結點契約的所有操作並獲得通過WebGetAttribute/WebInvokeAttribute特性設置URI模板。然後根據URI模板創建UriTemplate對象並最終創建UriTemplateTable。在真正需要進行操作選擇的時候,隻需要調用該UriTemplateTable的MatchSingle方法並傳入請求地址,如果匹配則表明UriTemplate對應的操作就是我們需要選擇的操作。
為了讓讀者對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 = "https://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對象,其地址為https://127.0.0.1:3721/calculatorservice,綁定類型為WebHttpBinding。然後基於該ServiceEndpoint創建我們定義WebHttpOperationSelector對象。最後我們創建了四個分別表示針對計算服務運算操作的Uri並調用GetOperationName方法測試是否能夠根據我們自定義的WebHttpOperationSelector對象正確選擇出相應的操作。
1: EndpointAddress address = new EndpointAddress("https://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("https://127.0.0.1:3721/calculatorservice/add/1/2");
8: Uri substractAdress = new Uri("https://127.0.0.1:3721/calculatorservice/substract/1/2");
9: Uri multiplyAdress = new Uri("https://127.0.0.1:3721/calculatorservice/multiply/1/2");
10: Uri divideAdress = new Uri("https://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
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-26 14:04:03