Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Use data member names on REST queries.

Includes optimisation of REST route resolution.
  • Loading branch information...
commit 4599e624b36e7dceef5c9b6dc065b6c924206b96 1 parent a9403bf
Gerke Geurts authored October 09, 2012
207  src/ServiceStack.Common/ServiceClient.Web/UrlExtensions.cs
@@ -4,6 +4,7 @@
4 4
 using System.Linq;
5 5
 using System.Reflection;
6 6
 using System.Runtime.Serialization;
  7
+using System.Text;
7 8
 using ServiceStack.Net30.Collections.Concurrent;
8 9
 using ServiceStack.ServiceHost;
9 10
 using ServiceStack.Text;
@@ -27,54 +28,51 @@ public static string ToUrl(this IReturn request, string httpMethod, string forma
27 28
 
28 29
             var requestType = request.GetType();
29 30
             var requestRoutes = routesCache.GetOrAdd(requestType, GetRoutesForType);
30  
-            if (!requestRoutes.Any())
  31
+            if (requestRoutes.Count == 0)
31 32
             {
32 33
                 if (formatFallbackToPredefinedRoute == null)
33  
-                    throw new InvalidOperationException("There is no rest routes mapped for '{0}' type. ".Fmt(requestType)
  34
+                    throw new InvalidOperationException("There are no rest routes mapped for '{0}' type. ".Fmt(requestType)
34 35
                         + "(Note: The automatic route selection only works with [Route] attributes on the request DTO and" 
35 36
                         + "not with routes registered in the IAppHost!)");
36 37
 
37  
-                var predefinedRoute = "/{0}/syncreply/{1}".Fmt(formatFallbackToPredefinedRoute, request.GetType().Name);
  38
+                var predefinedRoute = "/{0}/syncreply/{1}".Fmt(formatFallbackToPredefinedRoute, requestType.Name);
38 39
                 if (httpMethod == "GET" || httpMethod == "DELETE" || httpMethod == "OPTIONS")
39 40
                 {
40  
-                    var queryString = "?{0}".Fmt(request.GetType().GetProperties().ToQueryString(request));
41  
-                    predefinedRoute += queryString;
  41
+                    var queryProperties = RestRoute.GetQueryProperties(request.GetType());
  42
+                    predefinedRoute += "?" + RestRoute.GetQueryString(request, queryProperties);
42 43
                 }
43 44
 
44 45
                 return predefinedRoute;
45 46
             }
46 47
 
47  
-            var routesApplied =
48  
-                requestRoutes.Select(route => new { Route = route, Result = route.Apply(request, httpMethod) }).ToList();
49  
-            var matchingRoutes = routesApplied.Where(x => x.Result.Matches).ToList();
50  
-            if (!matchingRoutes.Any())
  48
+            var routesApplied = requestRoutes.Select(route => route.Apply(request, httpMethod)).ToList();
  49
+            var matchingRoutes = routesApplied.Where(x => x.Matches).ToList();
  50
+            if (matchingRoutes.Count == 0)
51 51
             {
52  
-                var errors = string.Join(String.Empty, routesApplied.Select(x => "\r\n\t{0}:\t{1}".Fmt(x.Route.Path, x.Result.FailReason)).ToArray());
  52
+                var errors = string.Join(String.Empty, routesApplied.Select(x => "\r\n\t{0}:\t{1}".Fmt(x.Route.Path, x.FailReason)).ToArray());
53 53
                 var errMsg = "None of the given rest routes matches '{0}' request:{1}"
54 54
                     .Fmt(requestType.Name, errors);
55 55
 
56 56
                 throw new InvalidOperationException(errMsg);
57 57
             }
58 58
 
59  
-            var matchingRoute = matchingRoutes[0]; // hack to determine variable type.
  59
+            RouteResolutionResult matchingRoute;
60 60
             if (matchingRoutes.Count > 1)
61 61
             {
62  
-                var mostSpecificRoute = FindMostSpecificRoute(matchingRoutes.Select(x => x.Route));
63  
-                if (mostSpecificRoute == null)
  62
+                matchingRoute = FindMostSpecificRoute(matchingRoutes);
  63
+                if (matchingRoute == null)
64 64
                 {
65 65
                     var errors = String.Join(String.Empty, matchingRoutes.Select(x => "\r\n\t" + x.Route.Path).ToArray());
66 66
                     var errMsg = "Ambiguous matching routes found for '{0}' request:{1}".Fmt(requestType.Name, errors);
67 67
                     throw new InvalidOperationException(errMsg);
68 68
                 }
69  
-
70  
-                matchingRoute = matchingRoutes.Single(x => x.Route == mostSpecificRoute);
71 69
             }
72 70
             else
73 71
             {
74  
-                matchingRoute = matchingRoutes.Single();
  72
+                matchingRoute = matchingRoutes[0];
75 73
             }
76 74
             
77  
-            var url = matchingRoute.Result.Uri;
  75
+            var url = matchingRoute.Uri;
78 76
             if (httpMethod == HttpMethod.Get || httpMethod == HttpMethod.Delete)
79 77
             {
80 78
                 var queryParams = matchingRoute.Route.FormatQueryParameters(request);
@@ -97,50 +95,46 @@ private static List<RestRoute> GetRoutesForType(Type requestType)
97 95
             return restRoutes;
98 96
         }
99 97
 
100  
-        private static RestRoute FindMostSpecificRoute(IEnumerable<RestRoute> routes)
  98
+        private static RouteResolutionResult FindMostSpecificRoute(IEnumerable<RouteResolutionResult> routes)
101 99
         {
102  
-            routes = routes.ToList();
103  
-            var mostSpecificRoute = routes.OrderBy(p => p.Variables.Count).Last();
104  
-
105  
-            // We may find several different routes {code}/{id} and {code}/{name} having the same number of variables. 
106  
-            // Such case will be handled by the next check.
107  
-            var allPathesAreSubsetsOfMostSpecific = routes
108  
-                .All(route => !route.Variables.Except(mostSpecificRoute.Variables).Any());
109  
-            if (!allPathesAreSubsetsOfMostSpecific)
110  
-            {
111  
-                return null;
112  
-            }
113  
-
114  
-            // Choose
115  
-            //     /product-lines/{productId}/{lineNumber}
116  
-            // over
117  
-            //     /products/{productId}/product-lines/{lineNumber}
118  
-            // (shortest one)
119  
-            var shortestPath = routes
120  
-                .Where(p => p.Variables.Count == mostSpecificRoute.Variables.Count)
121  
-                .OrderBy(path => path.Path.Length)
122  
-                .First();
123  
-
124  
-            return shortestPath;
125  
-        }
126  
-
127  
-        public static string ToQueryString(this IEnumerable<PropertyInfo> propertyInfos, object request)
128  
-        {
129  
-            var parameters = String.Empty;
130  
-            foreach (var property in propertyInfos)
  100
+            RouteResolutionResult bestMatch = default(RouteResolutionResult);
  101
+            var otherMatches = new List<RouteResolutionResult>();
  102
+            
  103
+            foreach (var route in routes)
131 104
             {
132  
-                var value = property.GetValue(request, null);
133  
-                if (value == null)
  105
+                if (bestMatch == null)
134 106
                 {
135  
-                    continue;
  107
+                    bestMatch = route;
  108
+                }
  109
+                else if (route.VariableCount > bestMatch.VariableCount)
  110
+                {
  111
+                    otherMatches.Clear();
  112
+                    bestMatch = route;
  113
+                }
  114
+                else if (route.VariableCount == bestMatch.VariableCount)
  115
+                {
  116
+                    // Choose
  117
+                    //     /product-lines/{productId}/{lineNumber}
  118
+                    // over
  119
+                    //     /products/{productId}/product-lines/{lineNumber}
  120
+                    // (shortest one)
  121
+                    if (route.PathLength < bestMatch.PathLength)
  122
+                    {
  123
+                        otherMatches.Add(bestMatch);
  124
+                        bestMatch = route;
  125
+                    }
  126
+                    else
  127
+                    {
  128
+                        otherMatches.Add(route);
  129
+                    }
136 130
                 }
137  
-                parameters += "&{0}={1}".Fmt(property.Name.ToCamelCase(), RestRoute.FormatQueryParameterValue(value));
138  
-            }
139  
-            if (!String.IsNullOrEmpty(parameters))
140  
-            {
141  
-                parameters = parameters.Substring(1);
142 131
             }
143  
-            return parameters;
  132
+
  133
+            // We may find several different routes {code}/{id} and {code}/{name} having the same number of variables. 
  134
+            // Such case will be handled by the next check.
  135
+            return bestMatch == null || otherMatches.All(r => r.HasSameVariables(bestMatch))
  136
+                ? bestMatch
  137
+                : null;
144 138
         }
145 139
     }
146 140
 
@@ -173,8 +167,8 @@ private static string FormatValue(object value)
173 167
         private const string VariablePostfix = "}";
174 168
         private const char VariablePostfixChar = '}';
175 169
 
176  
-        private readonly Dictionary<string, PropertyInfo> queryProperties = new Dictionary<string, PropertyInfo>(StringComparer.InvariantCultureIgnoreCase);
177  
-        private readonly Dictionary<string, PropertyInfo> variablesMap = new Dictionary<string, PropertyInfo>(StringComparer.InvariantCultureIgnoreCase);
  170
+        private readonly IDictionary<string, PropertyInfo> queryProperties;
  171
+        private readonly IDictionary<string, PropertyInfo> variablesMap = new Dictionary<string, PropertyInfo>(StringComparer.InvariantCultureIgnoreCase);
178 172
 
179 173
         public RestRoute(Type type, string path, string verbs)
180 174
         {
@@ -182,11 +176,7 @@ public RestRoute(Type type, string path, string verbs)
182 176
             this.Type = type;
183 177
             this.Path = path;
184 178
 
185  
-            foreach (var propertyInfo in GetRequestProperties(type))
186  
-            {
187  
-                this.queryProperties[propertyInfo.Name] = propertyInfo;
188  
-            }
189  
-
  179
+            this.queryProperties = GetQueryProperties(type);
190 180
             foreach (var variableName in GetUrlVariables(path))
191 181
             {
192 182
                 PropertyInfo propertyInfo;
@@ -223,12 +213,12 @@ public RouteResolutionResult Apply(object request, string httpMethod)
223 213
         {
224 214
             if (!this.IsValid)
225 215
             {
226  
-                return RouteResolutionResult.Error(this.ErrorMsg);
  216
+                return RouteResolutionResult.Error(this, this.ErrorMsg);
227 217
             }
228 218
 
229 219
             if (HttpMethods != null && HttpMethods.Length != 0 && httpMethod != null && !HttpMethods.Contains(httpMethod) && !HttpMethods.Contains("ANY"))
230 220
             {
231  
-                return RouteResolutionResult.Error("Allowed HTTP methods '{0}' does not support the specified '{1}' method."
  221
+                return RouteResolutionResult.Error(this, "Allowed HTTP methods '{0}' does not support the specified '{1}' method."
232 222
                     .Fmt(HttpMethods.Join(", "), httpMethod));
233 223
             }
234 224
 
@@ -252,35 +242,67 @@ public RouteResolutionResult Apply(object request, string httpMethod)
252 242
             if (unmatchedVariables.Any())
253 243
             {
254 244
                 var errMsg = "Could not match following variables: " + string.Join(",", unmatchedVariables.ToArray());
255  
-                return RouteResolutionResult.Error(errMsg);
  245
+                return RouteResolutionResult.Error(this, errMsg);
256 246
             }
257 247
 
258  
-            return RouteResolutionResult.Success(uri);
  248
+            return RouteResolutionResult.Success(this, uri);
259 249
         }
260 250
 
261 251
         public string FormatQueryParameters(object request)
262 252
         {
263  
-            return this.queryProperties.Values.ToQueryString(request);
  253
+            return GetQueryString(request, this.queryProperties);
  254
+        }
  255
+
  256
+        internal static string GetQueryString(object request, IDictionary<string, PropertyInfo> propertyMap)
  257
+        {
  258
+            var result = new StringBuilder();
  259
+
  260
+            foreach (var queryProperty in propertyMap)
  261
+            {
  262
+                var value = queryProperty.Value.GetValue(request, null);
  263
+                if (value == null)
  264
+                {
  265
+                    continue;
  266
+                }
  267
+
  268
+                result.Append(queryProperty.Key)
  269
+                    .Append('=')
  270
+                    .Append(RestRoute.FormatQueryParameterValue(value))
  271
+                    .Append('&');
  272
+            }
  273
+
  274
+            if (result.Length > 0) result.Length -= 1;
  275
+            return result.ToString();
264 276
         }
265 277
 
266  
-        private static IEnumerable<PropertyInfo> GetRequestProperties(Type requestType)
  278
+        internal static IDictionary<string, PropertyInfo> GetQueryProperties(Type requestType)
267 279
         {
  280
+            var result = new Dictionary<string, PropertyInfo>(StringComparer.InvariantCultureIgnoreCase);
268 281
             var hasDataContract = requestType.HasAttr<DataContractAttribute>();
269 282
 
270 283
             foreach (var propertyInfo in requestType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
271 284
             {
  285
+                var propertyName = propertyInfo.Name;
  286
+
272 287
                 if (!propertyInfo.CanRead) continue;
273 288
                 if (hasDataContract)
274 289
                 {
275 290
                     if (!propertyInfo.IsDefined(typeof(DataMemberAttribute), true)) continue;
  291
+                    var dataMember = (DataMemberAttribute)propertyInfo.GetCustomAttributes(typeof(DataMemberAttribute), true)[0];
  292
+                    if (!string.IsNullOrEmpty(dataMember.Name))
  293
+                    {
  294
+                        propertyName = dataMember.Name;
  295
+                    }
276 296
                 }
277 297
                 else
278 298
                 {
279 299
                     if (propertyInfo.IsDefined(typeof(IgnoreDataMemberAttribute), true)) continue;
280 300
                 }
281 301
 
282  
-                yield return propertyInfo;
  302
+                result[propertyName.ToCamelCase()] = propertyInfo;
283 303
             }
  304
+
  305
+            return result;
284 306
         }
285 307
 
286 308
         private IEnumerable<string> GetUrlVariables(string path)
@@ -322,27 +344,42 @@ private void AppendError(string msg)
322 344
                 this.ErrorMsg += "\r\n" + msg;
323 345
             }
324 346
         }
  347
+    }
  348
+
  349
+    public class RouteResolutionResult
  350
+    {
  351
+        public string FailReason { get; private set; }
  352
+        public string Uri { get; private set; }
  353
+        public RestRoute Route { get; private set; }
325 354
 
326  
-        public class RouteResolutionResult
  355
+        public bool Matches
327 356
         {
328  
-            public string FailReason { get; private set; }
  357
+            get { return string.IsNullOrEmpty(this.FailReason); }
  358
+        }
329 359
 
330  
-            public string Uri { get; private set; }
  360
+        public static RouteResolutionResult Error(RestRoute route, string errorMsg)
  361
+        {
  362
+            return new RouteResolutionResult { Route = route, FailReason = errorMsg };
  363
+        }
331 364
 
332  
-            public bool Matches
333  
-            {
334  
-                get { return string.IsNullOrEmpty(this.FailReason); }
335  
-            }
  365
+        public static RouteResolutionResult Success(RestRoute route, string uri)
  366
+        {
  367
+            return new RouteResolutionResult { Route = route, Uri = uri };
  368
+        }
336 369
 
337  
-            public static RouteResolutionResult Error(string errorMsg)
338  
-            {
339  
-                return new RouteResolutionResult { FailReason = errorMsg };
340  
-            }
  370
+        internal int VariableCount
  371
+        {
  372
+            get { return Route.Variables.Count; }
  373
+        }
341 374
 
342  
-            public static RouteResolutionResult Success(string uri)
343  
-            {
344  
-                return new RouteResolutionResult { Uri = uri };
345  
-            }
  375
+        internal int PathLength
  376
+        {
  377
+            get { return Route.Path.Length; }
  378
+        }
  379
+
  380
+        internal bool HasSameVariables(RouteResolutionResult other)
  381
+        {
  382
+            return Route.Variables.All(v => other.Route.Variables.Contains(v));
346 383
         }
347 384
     }
348 385
 }
20  tests/ServiceStack.Common.Tests/UrlExtensionTests.cs
@@ -45,6 +45,19 @@ public class RequestWithDataMembers : IReturn
45 45
         public string Excluded { get; set; }
46 46
     }
47 47
 
  48
+    [DataContract]
  49
+    [Route("/route/{Key}")]
  50
+    public class RequestWithNamedDataMembers : IReturn
  51
+    {
  52
+        [DataMember(Name = "Key")]
  53
+        public long Id { get; set; }
  54
+
  55
+        [DataMember(Name = "Inc")]
  56
+        public string Included { get; set; }
  57
+
  58
+        public string Excluded { get; set; }
  59
+    }
  60
+
48 61
     [TestFixture]
49 62
     public class UrlExtensionTests
50 63
     {
@@ -75,5 +88,12 @@ public void Can_include_only_data_members_on_querystring()
75 88
             var url = new RequestWithDataMembers { Id = 1, Included = "Yes", Excluded = "No" }.ToUrl("GET");
76 89
             Assert.That(url, Is.EqualTo("/route/1?included=Yes"));
77 90
         }
  91
+
  92
+        [Test]
  93
+        public void Use_data_member_names_on_querystring()
  94
+        {
  95
+            var url = new RequestWithNamedDataMembers { Id = 1, Included = "Yes", Excluded = "No" }.ToUrl("GET");
  96
+            Assert.That(url, Is.EqualTo("/route/1?inc=Yes"));
  97
+        }
78 98
     }
79 99
 }

0 notes on commit 4599e62

Please sign in to comment.
Something went wrong with that request. Please try again.