1 //------------------------------------------------------------
2 // Copyright (c) Microsoft Corporation. All rights reserved.
3 //------------------------------------------------------------
4 #pragma warning disable 1634, 1691
5 namespace System
.ServiceModel
.Dispatcher
8 using System
.Collections
.Generic
;
9 using System
.Collections
.ObjectModel
;
10 using System
.Diagnostics
;
11 using System
.Diagnostics
.CodeAnalysis
;
13 using System
.ServiceModel
;
14 using System
.ServiceModel
.Activation
;
15 using System
.ServiceModel
.Channels
;
16 using System
.ServiceModel
.Description
;
17 using System
.ServiceModel
.Diagnostics
;
18 using System
.ServiceModel
.Web
;
21 public class WebHttpDispatchOperationSelector
: IDispatchOperationSelector
23 public const string HttpOperationSelectorUriMatchedPropertyName
= "UriMatched";
24 internal const string HttpOperationSelectorDataPropertyName
= "HttpOperationSelectorData";
27 public const string HttpOperationNamePropertyName
= "HttpOperationName";
28 internal const string redirectOperationName
= ""; // always unhandled invoker
29 internal const string RedirectPropertyName
= "WebHttpRedirect";
31 string catchAllOperationName
= ""; // user UT=* Method=* operation, else unhandled invoker
33 Dictionary
<string, UriTemplateTable
> methodSpecificTables
; // indexed by the http method name
34 UriTemplateTable wildcardTable
; // this is one of the methodSpecificTables, special-cased for faster access
35 Dictionary
<string, UriTemplate
> templates
;
37 UriTemplateTable helpUriTable
;
39 public WebHttpDispatchOperationSelector(ServiceEndpoint endpoint
)
43 throw DiagnosticUtility
.ExceptionUtility
.ThrowHelperArgumentNull("endpoint");
45 if (endpoint
.Address
== null)
47 throw DiagnosticUtility
.ExceptionUtility
.ThrowHelperError(new InvalidOperationException(
48 SR2
.GetString(SR2
.EndpointAddressCannotBeNull
)));
50 #pragma warning disable 56506 // Microsoft, endpoint.Address.Uri is never null
51 Uri baseUri
= endpoint
.Address
.Uri
;
52 this.methodSpecificTables
= new Dictionary
<string, UriTemplateTable
>();
53 this.templates
= new Dictionary
<string, UriTemplate
>();
54 #pragma warning restore 56506
56 WebHttpBehavior webHttpBehavior
= endpoint
.Behaviors
.Find
<WebHttpBehavior
>();
57 if (webHttpBehavior
!= null && webHttpBehavior
.HelpEnabled
)
59 this.helpUriTable
= new UriTemplateTable(endpoint
.ListenUri
, HelpPage
.GetOperationTemplatePairs());
62 Dictionary
<WCFKey
, string> alreadyHaves
= new Dictionary
<WCFKey
, string>();
64 #pragma warning disable 56506 // Microsoft, endpoint.Contract is never null
65 foreach (OperationDescription od
in endpoint
.Contract
.Operations
)
66 #pragma warning restore 56506
68 // ignore callback operations
69 if (od
.Messages
[0].Direction
== MessageDirection
.Input
)
71 string method
= WebHttpBehavior
.GetWebMethod(od
);
72 string path
= UriTemplateClientFormatter
.GetUTStringOrDefault(od
);
76 if (UriTemplateHelpers
.IsWildcardPath(path
) && (method
== WebHttpBehavior
.WildcardMethod
))
78 if (this.catchAllOperationName
!= "")
80 throw DiagnosticUtility
.ExceptionUtility
.ThrowHelperError(
81 new InvalidOperationException(
82 SR2
.GetString(SR2
.MultipleOperationsInContractWithPathMethod
,
83 endpoint
.Contract
.Name
, path
, method
)));
85 this.catchAllOperationName
= od
.Name
;
87 UriTemplate ut
= new UriTemplate(path
);
88 WCFKey wcfKey
= new WCFKey(ut
, method
);
89 if (alreadyHaves
.ContainsKey(wcfKey
))
91 throw DiagnosticUtility
.ExceptionUtility
.ThrowHelperError(
92 new InvalidOperationException(
93 SR2
.GetString(SR2
.MultipleOperationsInContractWithPathMethod
,
94 endpoint
.Contract
.Name
, path
, method
)));
96 alreadyHaves
.Add(wcfKey
, od
.Name
);
98 UriTemplateTable methodSpecificTable
;
99 if (!methodSpecificTables
.TryGetValue(method
, out methodSpecificTable
))
101 methodSpecificTable
= new UriTemplateTable(baseUri
);
102 methodSpecificTables
.Add(method
, methodSpecificTable
);
105 methodSpecificTable
.KeyValuePairs
.Add(new KeyValuePair
<UriTemplate
, object>(ut
, od
.Name
));
106 this.templates
.Add(od
.Name
, ut
);
110 if (this.methodSpecificTables
.Count
== 0)
112 this.methodSpecificTables
= null;
116 // freeze all the tables because they should not be modified after this point
117 foreach (UriTemplateTable table
in this.methodSpecificTables
.Values
)
119 table
.MakeReadOnly(true /* allowDuplicateEquivalentUriTemplates */);
122 if (!methodSpecificTables
.TryGetValue(WebHttpBehavior
.WildcardMethod
, out wildcardTable
))
124 wildcardTable
= null;
129 protected WebHttpDispatchOperationSelector()
133 public virtual UriTemplate
GetUriTemplate(string operationName
)
135 if (operationName
== null)
137 throw DiagnosticUtility
.ExceptionUtility
.ThrowHelperArgumentNull("operationName");
140 if (!this.templates
.TryGetValue(operationName
, out result
))
150 [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId
= "0#", Justification
= "This method is defined by the IDispatchOperationSelector interface")]
151 public string SelectOperation(ref Message message
)
155 throw DiagnosticUtility
.ExceptionUtility
.ThrowHelperArgumentNull("message");
158 string result
= this.SelectOperation(ref message
, out uriMatched
);
159 #pragma warning disable 56506 // Microsoft, Message.Properties is never null
160 message
.Properties
.Add(HttpOperationSelectorUriMatchedPropertyName
, uriMatched
);
161 #pragma warning restore 56506
164 message
.Properties
.Add(HttpOperationNamePropertyName
, result
);
165 if (DiagnosticUtility
.ShouldTraceInformation
)
167 #pragma warning disable 56506 // Microsoft, Message.Headers is never null
168 TraceUtility
.TraceEvent(TraceEventType
.Information
, TraceCode
.WebRequestMatchesOperation
, SR2
.GetString(SR2
.TraceCodeWebRequestMatchesOperation
, message
.Headers
.To
, result
));
169 #pragma warning restore 56506
175 [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId
= "0#", Justification
= "This method is like that defined by the IDispatchOperationSelector interface")]
176 [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId
= "1#", Justification
= "This API needs to return multiple things")]
177 protected virtual string SelectOperation(ref Message message
, out bool uriMatched
)
181 throw DiagnosticUtility
.ExceptionUtility
.ThrowHelperArgumentNull("message");
184 if (this.methodSpecificTables
== null)
186 return this.catchAllOperationName
;
189 #pragma warning disable 56506 // Microsoft, message.Properties is never null
190 if (!message
.Properties
.ContainsKey(HttpRequestMessageProperty
.Name
))
192 return this.catchAllOperationName
;
194 HttpRequestMessageProperty prop
= message
.Properties
[HttpRequestMessageProperty
.Name
] as HttpRequestMessageProperty
;
197 return this.catchAllOperationName
;
199 string method
= prop
.Method
;
201 Uri to
= message
.Headers
.To
;
202 #pragma warning restore 56506
206 return this.catchAllOperationName
;
209 if (this.helpUriTable
!= null)
211 UriTemplateMatch match
= this.helpUriTable
.MatchSingle(to
);
215 AddUriTemplateMatch(match
, prop
, message
);
216 if (method
== WebHttpBehavior
.GET
)
218 return HelpOperationInvoker
.OperationName
;
220 message
.Properties
.Add(WebHttpDispatchOperationSelector
.HttpOperationSelectorDataPropertyName
,
221 new WebHttpDispatchOperationSelectorData() { AllowedMethods = new List<string>() { WebHttpBehavior.GET }
});
222 return this.catchAllOperationName
;
226 UriTemplateTable methodSpecificTable
;
227 bool methodMatchesExactly
= methodSpecificTables
.TryGetValue(method
, out methodSpecificTable
);
228 if (methodMatchesExactly
)
230 string operationName
;
231 uriMatched
= CanUriMatch(methodSpecificTable
, to
, prop
, message
, out operationName
);
234 return operationName
;
238 if (wildcardTable
!= null)
240 string operationName
;
241 uriMatched
= CanUriMatch(wildcardTable
, to
, prop
, message
, out operationName
);
244 return operationName
;
248 if (ShouldRedirectToUriWithSlashAtTheEnd(methodSpecificTable
, message
, to
))
250 return redirectOperationName
;
253 // the {method, uri} pair does not match anything the service supports.
254 // we know at this point that we'll return some kind of error code, but we
255 // should go through all methods for the uri to see if any method is supported
256 // so that that information could be returned to the user as well
258 List
<string> allowedMethods
= null;
259 foreach (KeyValuePair
<string, UriTemplateTable
> pair
in methodSpecificTables
)
261 if (pair
.Key
== method
|| pair
.Key
== WebHttpBehavior
.WildcardMethod
)
263 // the uri must not match the uri template
266 UriTemplateTable table
= pair
.Value
;
267 if (table
.MatchSingle(to
) != null)
269 if (allowedMethods
== null)
271 allowedMethods
= new List
<string>();
276 if (!allowedMethods
.Contains(pair
.Key
))
278 allowedMethods
.Add(pair
.Key
);
283 if (allowedMethods
!= null)
286 message
.Properties
.Add(WebHttpDispatchOperationSelector
.HttpOperationSelectorDataPropertyName
,
287 new WebHttpDispatchOperationSelectorData() { AllowedMethods = allowedMethods }
);
289 return catchAllOperationName
;
292 bool CanUriMatch(UriTemplateTable methodSpecificTable
, Uri to
, HttpRequestMessageProperty prop
, Message message
, out string operationName
)
294 operationName
= null;
295 UriTemplateMatch result
= methodSpecificTable
.MatchSingle(to
);
299 operationName
= result
.Data
as string;
300 Fx
.Assert(operationName
!= null, "bad result");
301 AddUriTemplateMatch(result
, prop
, message
);
307 void AddUriTemplateMatch(UriTemplateMatch match
, HttpRequestMessageProperty requestProp
, Message message
)
309 match
.SetBaseUri(match
.BaseUri
, requestProp
);
310 message
.Properties
.Add(IncomingWebRequestContext
.UriTemplateMatchResultsPropertyName
, match
);
313 bool ShouldRedirectToUriWithSlashAtTheEnd(UriTemplateTable methodSpecificTable
, Message message
, Uri to
)
315 UriBuilder ub
= new UriBuilder(to
);
316 if (ub
.Path
.EndsWith("/", StringComparison
.Ordinal
))
321 ub
.Path
= ub
.Path
+ "/";
322 Uri originalPlusSlash
= ub
.Uri
;
325 if (methodSpecificTable
!= null && methodSpecificTable
.MatchSingle(originalPlusSlash
) != null)
327 // as an optimization, we check the table that matched the request's method
328 // first, as it is more probable that a hit happens there
334 // we will redirect as long as there is any method
335 // - not necessary the one the user is looking for -
336 // that matches the uri with a slash at the end
338 foreach (KeyValuePair
<string, UriTemplateTable
> pair
in methodSpecificTables
)
340 UriTemplateTable table
= pair
.Value
;
341 if (table
!= methodSpecificTable
&& table
.MatchSingle(originalPlusSlash
) != null)
351 string hostAndPort
= GetAuthority(message
);
352 originalPlusSlash
= UriTemplate
.RewriteUri(ub
.Uri
, hostAndPort
);
353 message
.Properties
.Add(RedirectPropertyName
, originalPlusSlash
);
358 static string GetAuthority(Message message
)
360 HttpRequestMessageProperty requestProperty
;
361 string hostName
= null;
362 if (message
.Properties
.TryGetValue(HttpRequestMessageProperty
.Name
, out requestProperty
))
364 hostName
= requestProperty
.Headers
[HttpRequestHeader
.Host
];
365 if (!string.IsNullOrEmpty(hostName
))
370 IAspNetMessageProperty aspNetMessageProperty
= AspNetEnvironment
.Current
.GetHostingProperty(message
);
371 if (aspNetMessageProperty
!= null)
373 hostName
= aspNetMessageProperty
.OriginalRequestUri
.Authority
;
378 // to enforce that no two ops have same UriTemplate & Method
382 UriTemplate uriTemplate
;
383 public WCFKey(UriTemplate uriTemplate
, string method
)
385 this.uriTemplate
= uriTemplate
;
386 this.method
= method
;
388 public override bool Equals(object obj
)
390 WCFKey other
= obj
as WCFKey
;
395 return this.uriTemplate
.IsEquivalentTo(other
.uriTemplate
) && this.method
== other
.method
;
397 public override int GetHashCode()
399 return UriTemplateEquivalenceComparer
.Instance
.GetHashCode(this.uriTemplate
);