Permalink
Browse files

Use data member names on REST queries.

Includes optimisation of REST route resolution.
  • Loading branch information...
1 parent a9403bf commit 4599e624b36e7dceef5c9b6dc065b6c924206b96 @ggeurts ggeurts committed Oct 9, 2012
@@ -4,6 +4,7 @@
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
+using System.Text;
using ServiceStack.Net30.Collections.Concurrent;
using ServiceStack.ServiceHost;
using ServiceStack.Text;
@@ -27,54 +28,51 @@ public static string ToUrl(this IReturn request, string httpMethod, string forma
var requestType = request.GetType();
var requestRoutes = routesCache.GetOrAdd(requestType, GetRoutesForType);
- if (!requestRoutes.Any())
+ if (requestRoutes.Count == 0)
{
if (formatFallbackToPredefinedRoute == null)
- throw new InvalidOperationException("There is no rest routes mapped for '{0}' type. ".Fmt(requestType)
+ throw new InvalidOperationException("There are no rest routes mapped for '{0}' type. ".Fmt(requestType)
+ "(Note: The automatic route selection only works with [Route] attributes on the request DTO and"
+ "not with routes registered in the IAppHost!)");
- var predefinedRoute = "/{0}/syncreply/{1}".Fmt(formatFallbackToPredefinedRoute, request.GetType().Name);
+ var predefinedRoute = "/{0}/syncreply/{1}".Fmt(formatFallbackToPredefinedRoute, requestType.Name);
if (httpMethod == "GET" || httpMethod == "DELETE" || httpMethod == "OPTIONS")
{
- var queryString = "?{0}".Fmt(request.GetType().GetProperties().ToQueryString(request));
- predefinedRoute += queryString;
+ var queryProperties = RestRoute.GetQueryProperties(request.GetType());
+ predefinedRoute += "?" + RestRoute.GetQueryString(request, queryProperties);
}
return predefinedRoute;
}
- var routesApplied =
- requestRoutes.Select(route => new { Route = route, Result = route.Apply(request, httpMethod) }).ToList();
- var matchingRoutes = routesApplied.Where(x => x.Result.Matches).ToList();
- if (!matchingRoutes.Any())
+ var routesApplied = requestRoutes.Select(route => route.Apply(request, httpMethod)).ToList();
+ var matchingRoutes = routesApplied.Where(x => x.Matches).ToList();
+ if (matchingRoutes.Count == 0)
{
- var errors = string.Join(String.Empty, routesApplied.Select(x => "\r\n\t{0}:\t{1}".Fmt(x.Route.Path, x.Result.FailReason)).ToArray());
+ var errors = string.Join(String.Empty, routesApplied.Select(x => "\r\n\t{0}:\t{1}".Fmt(x.Route.Path, x.FailReason)).ToArray());
var errMsg = "None of the given rest routes matches '{0}' request:{1}"
.Fmt(requestType.Name, errors);
throw new InvalidOperationException(errMsg);
}
- var matchingRoute = matchingRoutes[0]; // hack to determine variable type.
+ RouteResolutionResult matchingRoute;
if (matchingRoutes.Count > 1)
{
- var mostSpecificRoute = FindMostSpecificRoute(matchingRoutes.Select(x => x.Route));
- if (mostSpecificRoute == null)
+ matchingRoute = FindMostSpecificRoute(matchingRoutes);
+ if (matchingRoute == null)
{
var errors = String.Join(String.Empty, matchingRoutes.Select(x => "\r\n\t" + x.Route.Path).ToArray());
var errMsg = "Ambiguous matching routes found for '{0}' request:{1}".Fmt(requestType.Name, errors);
throw new InvalidOperationException(errMsg);
}
-
- matchingRoute = matchingRoutes.Single(x => x.Route == mostSpecificRoute);
}
else
{
- matchingRoute = matchingRoutes.Single();
+ matchingRoute = matchingRoutes[0];
}
- var url = matchingRoute.Result.Uri;
+ var url = matchingRoute.Uri;
if (httpMethod == HttpMethod.Get || httpMethod == HttpMethod.Delete)
{
var queryParams = matchingRoute.Route.FormatQueryParameters(request);
@@ -97,50 +95,46 @@ private static List<RestRoute> GetRoutesForType(Type requestType)
return restRoutes;
}
- private static RestRoute FindMostSpecificRoute(IEnumerable<RestRoute> routes)
+ private static RouteResolutionResult FindMostSpecificRoute(IEnumerable<RouteResolutionResult> routes)
{
- routes = routes.ToList();
- var mostSpecificRoute = routes.OrderBy(p => p.Variables.Count).Last();
-
- // We may find several different routes {code}/{id} and {code}/{name} having the same number of variables.
- // Such case will be handled by the next check.
- var allPathesAreSubsetsOfMostSpecific = routes
- .All(route => !route.Variables.Except(mostSpecificRoute.Variables).Any());
- if (!allPathesAreSubsetsOfMostSpecific)
- {
- return null;
- }
-
- // Choose
- // /product-lines/{productId}/{lineNumber}
- // over
- // /products/{productId}/product-lines/{lineNumber}
- // (shortest one)
- var shortestPath = routes
- .Where(p => p.Variables.Count == mostSpecificRoute.Variables.Count)
- .OrderBy(path => path.Path.Length)
- .First();
-
- return shortestPath;
- }
-
- public static string ToQueryString(this IEnumerable<PropertyInfo> propertyInfos, object request)
- {
- var parameters = String.Empty;
- foreach (var property in propertyInfos)
+ RouteResolutionResult bestMatch = default(RouteResolutionResult);
+ var otherMatches = new List<RouteResolutionResult>();
+
+ foreach (var route in routes)
{
- var value = property.GetValue(request, null);
- if (value == null)
+ if (bestMatch == null)
{
- continue;
+ bestMatch = route;
+ }
+ else if (route.VariableCount > bestMatch.VariableCount)
+ {
+ otherMatches.Clear();
+ bestMatch = route;
+ }
+ else if (route.VariableCount == bestMatch.VariableCount)
+ {
+ // Choose
+ // /product-lines/{productId}/{lineNumber}
+ // over
+ // /products/{productId}/product-lines/{lineNumber}
+ // (shortest one)
+ if (route.PathLength < bestMatch.PathLength)
+ {
+ otherMatches.Add(bestMatch);
+ bestMatch = route;
+ }
+ else
+ {
+ otherMatches.Add(route);
+ }
}
- parameters += "&{0}={1}".Fmt(property.Name.ToCamelCase(), RestRoute.FormatQueryParameterValue(value));
- }
- if (!String.IsNullOrEmpty(parameters))
- {
- parameters = parameters.Substring(1);
}
- return parameters;
+
+ // We may find several different routes {code}/{id} and {code}/{name} having the same number of variables.
+ // Such case will be handled by the next check.
+ return bestMatch == null || otherMatches.All(r => r.HasSameVariables(bestMatch))
+ ? bestMatch
+ : null;
}
}
@@ -173,20 +167,16 @@ private static string FormatValue(object value)
private const string VariablePostfix = "}";
private const char VariablePostfixChar = '}';
- private readonly Dictionary<string, PropertyInfo> queryProperties = new Dictionary<string, PropertyInfo>(StringComparer.InvariantCultureIgnoreCase);
- private readonly Dictionary<string, PropertyInfo> variablesMap = new Dictionary<string, PropertyInfo>(StringComparer.InvariantCultureIgnoreCase);
+ private readonly IDictionary<string, PropertyInfo> queryProperties;
+ private readonly IDictionary<string, PropertyInfo> variablesMap = new Dictionary<string, PropertyInfo>(StringComparer.InvariantCultureIgnoreCase);
public RestRoute(Type type, string path, string verbs)
{
this.HttpMethods = (verbs ?? string.Empty).Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
this.Type = type;
this.Path = path;
- foreach (var propertyInfo in GetRequestProperties(type))
- {
- this.queryProperties[propertyInfo.Name] = propertyInfo;
- }
-
+ this.queryProperties = GetQueryProperties(type);
foreach (var variableName in GetUrlVariables(path))
{
PropertyInfo propertyInfo;
@@ -223,12 +213,12 @@ public RouteResolutionResult Apply(object request, string httpMethod)
{
if (!this.IsValid)
{
- return RouteResolutionResult.Error(this.ErrorMsg);
+ return RouteResolutionResult.Error(this, this.ErrorMsg);
}
if (HttpMethods != null && HttpMethods.Length != 0 && httpMethod != null && !HttpMethods.Contains(httpMethod) && !HttpMethods.Contains("ANY"))
{
- return RouteResolutionResult.Error("Allowed HTTP methods '{0}' does not support the specified '{1}' method."
+ return RouteResolutionResult.Error(this, "Allowed HTTP methods '{0}' does not support the specified '{1}' method."
.Fmt(HttpMethods.Join(", "), httpMethod));
}
@@ -252,35 +242,67 @@ public RouteResolutionResult Apply(object request, string httpMethod)
if (unmatchedVariables.Any())
{
var errMsg = "Could not match following variables: " + string.Join(",", unmatchedVariables.ToArray());
- return RouteResolutionResult.Error(errMsg);
+ return RouteResolutionResult.Error(this, errMsg);
}
- return RouteResolutionResult.Success(uri);
+ return RouteResolutionResult.Success(this, uri);
}
public string FormatQueryParameters(object request)
{
- return this.queryProperties.Values.ToQueryString(request);
+ return GetQueryString(request, this.queryProperties);
+ }
+
+ internal static string GetQueryString(object request, IDictionary<string, PropertyInfo> propertyMap)
+ {
+ var result = new StringBuilder();
+
+ foreach (var queryProperty in propertyMap)
+ {
+ var value = queryProperty.Value.GetValue(request, null);
+ if (value == null)
+ {
+ continue;
+ }
+
+ result.Append(queryProperty.Key)
+ .Append('=')
+ .Append(RestRoute.FormatQueryParameterValue(value))
+ .Append('&');
+ }
+
+ if (result.Length > 0) result.Length -= 1;
+ return result.ToString();
}
- private static IEnumerable<PropertyInfo> GetRequestProperties(Type requestType)
+ internal static IDictionary<string, PropertyInfo> GetQueryProperties(Type requestType)
{
+ var result = new Dictionary<string, PropertyInfo>(StringComparer.InvariantCultureIgnoreCase);
var hasDataContract = requestType.HasAttr<DataContractAttribute>();
foreach (var propertyInfo in requestType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
+ var propertyName = propertyInfo.Name;
+
if (!propertyInfo.CanRead) continue;
if (hasDataContract)
{
if (!propertyInfo.IsDefined(typeof(DataMemberAttribute), true)) continue;
+ var dataMember = (DataMemberAttribute)propertyInfo.GetCustomAttributes(typeof(DataMemberAttribute), true)[0];
+ if (!string.IsNullOrEmpty(dataMember.Name))
+ {
+ propertyName = dataMember.Name;
+ }
}
else
{
if (propertyInfo.IsDefined(typeof(IgnoreDataMemberAttribute), true)) continue;
}
- yield return propertyInfo;
+ result[propertyName.ToCamelCase()] = propertyInfo;
}
+
+ return result;
}
private IEnumerable<string> GetUrlVariables(string path)
@@ -322,27 +344,42 @@ private void AppendError(string msg)
this.ErrorMsg += "\r\n" + msg;
}
}
+ }
+
+ public class RouteResolutionResult
+ {
+ public string FailReason { get; private set; }
+ public string Uri { get; private set; }
+ public RestRoute Route { get; private set; }
- public class RouteResolutionResult
+ public bool Matches
{
- public string FailReason { get; private set; }
+ get { return string.IsNullOrEmpty(this.FailReason); }
+ }
- public string Uri { get; private set; }
+ public static RouteResolutionResult Error(RestRoute route, string errorMsg)
+ {
+ return new RouteResolutionResult { Route = route, FailReason = errorMsg };
+ }
- public bool Matches
- {
- get { return string.IsNullOrEmpty(this.FailReason); }
- }
+ public static RouteResolutionResult Success(RestRoute route, string uri)
+ {
+ return new RouteResolutionResult { Route = route, Uri = uri };
+ }
- public static RouteResolutionResult Error(string errorMsg)
- {
- return new RouteResolutionResult { FailReason = errorMsg };
- }
+ internal int VariableCount
+ {
+ get { return Route.Variables.Count; }
+ }
- public static RouteResolutionResult Success(string uri)
- {
- return new RouteResolutionResult { Uri = uri };
- }
+ internal int PathLength
+ {
+ get { return Route.Path.Length; }
+ }
+
+ internal bool HasSameVariables(RouteResolutionResult other)
+ {
+ return Route.Variables.All(v => other.Route.Variables.Contains(v));
}
}
}
@@ -45,6 +45,19 @@ public class RequestWithDataMembers : IReturn
public string Excluded { get; set; }
}
+ [DataContract]
+ [Route("/route/{Key}")]
+ public class RequestWithNamedDataMembers : IReturn
+ {
+ [DataMember(Name = "Key")]
+ public long Id { get; set; }
+
+ [DataMember(Name = "Inc")]
+ public string Included { get; set; }
+
+ public string Excluded { get; set; }
+ }
+
[TestFixture]
public class UrlExtensionTests
{
@@ -75,5 +88,12 @@ public void Can_include_only_data_members_on_querystring()
var url = new RequestWithDataMembers { Id = 1, Included = "Yes", Excluded = "No" }.ToUrl("GET");
Assert.That(url, Is.EqualTo("/route/1?included=Yes"));
}
+
+ [Test]
+ public void Use_data_member_names_on_querystring()
+ {
+ var url = new RequestWithNamedDataMembers { Id = 1, Included = "Yes", Excluded = "No" }.ToUrl("GET");
+ Assert.That(url, Is.EqualTo("/route/1?inc=Yes"));
+ }
}
}

0 comments on commit 4599e62

Please sign in to comment.