diff --git a/src/Microsoft.AspNet.Routing/Constraints/CompositeRouteConstraint.cs b/src/Microsoft.AspNet.Routing/Constraints/CompositeRouteConstraint.cs new file mode 100644 index 0000000000..ff9ace0bb6 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Constraints/CompositeRouteConstraint.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Routing.Constraints +{ + /// + /// Constrains a route by several child constraints. + /// + public class CompositeRouteConstraint : IRouteConstraint + { + /// + /// Initializes a new instance of the class. + /// + /// The child constraints that must match for this constraint to match. + public CompositeRouteConstraint([NotNull] IEnumerable constraints) + { + Constraints = constraints; + } + + /// + /// Gets the child constraints that must match for this constraint to match. + /// + public IEnumerable Constraints { get; private set; } + + /// + /// Calls Match on the child constraints. + /// The call returns as soon as one of the child constraints does not match. + /// + /// The HTTP context associated with the current call. + /// The route that is being constrained. + /// The route key used for the constraint. + /// The route value dictionary. + /// The direction of the routing, + /// i.e. incoming request or URL generation. + /// True if all the constraints Match, + /// false as soon as one of the child constraints does not match. + /// + /// There is no guarantee for the order in which child constraints are invoked, + /// also the method returns as soon as one of the constraints does not match. + /// + public bool Match([NotNull] HttpContext httpContext, + [NotNull] IRouter route, + [NotNull] string routeKey, + [NotNull] IDictionary values, + RouteDirection routeDirection) + { + foreach (var constraint in Constraints) + { + if (!constraint.Match(httpContext, route, routeKey, values, routeDirection)) + { + return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Constraints/IntRouteConstraint.cs b/src/Microsoft.AspNet.Routing/Constraints/IntRouteConstraint.cs new file mode 100644 index 0000000000..e0cd79fedb --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Constraints/IntRouteConstraint.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Routing.Constraints +{ + /// + /// Constrains a route parameter to represent only 32-bit integer values. + /// + public class IntRouteConstraint : IRouteConstraint + { + /// + public bool Match([NotNull] HttpContext httpContext, + [NotNull] IRouter route, + [NotNull] string routeKey, + [NotNull] IDictionary values, + RouteDirection routeDirection) + { + object value; + if (values.TryGetValue(routeKey, out value) && value != null) + { + if (value is int) + { + return true; + } + + int result; + string valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return Int32.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out result); + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/RegexConstraint.cs b/src/Microsoft.AspNet.Routing/Constraints/RegexConstraint.cs similarity index 96% rename from src/Microsoft.AspNet.Routing/RegexConstraint.cs rename to src/Microsoft.AspNet.Routing/Constraints/RegexConstraint.cs index 4d4c128eff..a49c66524b 100644 --- a/src/Microsoft.AspNet.Routing/RegexConstraint.cs +++ b/src/Microsoft.AspNet.Routing/Constraints/RegexConstraint.cs @@ -7,7 +7,7 @@ using System.Globalization; using System.Text.RegularExpressions; using Microsoft.AspNet.Http; -namespace Microsoft.AspNet.Routing +namespace Microsoft.AspNet.Routing.Constraints { public class RegexConstraint : IRouteConstraint { diff --git a/src/Microsoft.AspNet.Routing/DefaultInlineConstraintResolver.cs b/src/Microsoft.AspNet.Routing/DefaultInlineConstraintResolver.cs new file mode 100644 index 0000000000..fc04fff664 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/DefaultInlineConstraintResolver.cs @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using Microsoft.AspNet.Routing.Constraints; + +namespace Microsoft.AspNet.Routing +{ + /// + /// The default implementation of . Resolves constraints by parsing + /// a constraint key and constraint arguments, using a map to resolve the constraint type, and calling an + /// appropriate constructor for the constraint type. + /// + public class DefaultInlineConstraintResolver : IInlineConstraintResolver + { + private readonly IDictionary _inlineConstraintMap = GetDefaultConstraintMap(); + + /// + /// Gets the mutable dictionary that maps constraint keys to a particular constraint type. + /// + public IDictionary ConstraintMap + { + get + { + return _inlineConstraintMap; + } + } + + private static IDictionary GetDefaultConstraintMap() + { + return new Dictionary(StringComparer.OrdinalIgnoreCase) + { + // Type-specific constraints + { "int", typeof(IntRouteConstraint) }, + }; + } + + /// + /// + /// A typical constraint looks like the following + /// "exampleConstraint(arg1, arg2, 12)". + /// Here if the type registered for exampleConstraint has a single constructor with one argument, + /// The entire string "arg1, arg2, 12" will be treated as a single argument. + /// In all other cases arguments are split at comma. + /// + public virtual IRouteConstraint ResolveConstraint([NotNull] string inlineConstraint) + { + string constraintKey; + string argumentString; + int indexOfFirstOpenParens = inlineConstraint.IndexOf('('); + if (indexOfFirstOpenParens >= 0 && inlineConstraint.EndsWith(")", StringComparison.Ordinal)) + { + constraintKey = inlineConstraint.Substring(0, indexOfFirstOpenParens); + argumentString = inlineConstraint.Substring(indexOfFirstOpenParens + 1, + inlineConstraint.Length - indexOfFirstOpenParens - 2); + } + else + { + constraintKey = inlineConstraint; + argumentString = null; + } + + Type constraintType; + if (!_inlineConstraintMap.TryGetValue(constraintKey, out constraintType)) + { + // Cannot resolve the constraint key + return null; + } + + if (!typeof(IRouteConstraint).GetTypeInfo().IsAssignableFrom(constraintType.GetTypeInfo())) + { + throw new InvalidOperationException( + Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint( + constraintType, constraintKey, typeof(IRouteConstraint).Name)); + } + + return CreateConstraint(constraintType, argumentString); + } + + private static IRouteConstraint CreateConstraint(Type constraintType, string argumentString) + { + // No arguments - call the default constructor + if (argumentString == null) + { + return (IRouteConstraint)Activator.CreateInstance(constraintType); + } + + var constraintTypeInfo = constraintType.GetTypeInfo(); + ConstructorInfo activationConstructor = null; + object[] parameters = null; + var constructors = constraintTypeInfo.DeclaredConstructors.ToArray(); + + // If there is only one constructor and it has a single parameter, pass the argument string directly + // This is necessary for the Regex RouteConstraint to ensure that patterns are not split on commas. + if (constructors.Length == 1 && constructors[0].GetParameters().Length == 1) + { + activationConstructor = constructors[0]; + parameters = ConvertArguments(activationConstructor.GetParameters(), new string[] { argumentString }); + } + else + { + string[] arguments = argumentString.Split(',').Select(argument => argument.Trim()).ToArray(); + + var matchingConstructors = constructors.Where(ci => ci.GetParameters().Length == arguments.Length).ToArray(); + var constructorMatches = matchingConstructors.Length; + + if (constructorMatches == 0) + { + throw new InvalidOperationException( + Resources.FormatDefaultInlineConstraintResolver_CouldNotFindCtor( + constraintTypeInfo.Name, argumentString.Length)); + } + else if (constructorMatches == 1) + { + activationConstructor = matchingConstructors[0]; + parameters = ConvertArguments(activationConstructor.GetParameters(), arguments); + } + else + { + throw new InvalidOperationException( + Resources.FormatDefaultInlineConstraintResolver_AmbiguousCtors( + constraintTypeInfo.Name, argumentString.Length)); + } + } + + return (IRouteConstraint)activationConstructor.Invoke(parameters); + } + + private static object[] ConvertArguments(ParameterInfo[] parameterInfos, string[] arguments) + { + var parameters = new object[parameterInfos.Length]; + for (int i = 0; i < parameterInfos.Length; i++) + { + var parameter = parameterInfos[i]; + var parameterType = parameter.ParameterType; + parameters[i] = Convert.ChangeType(arguments[i], parameterType, CultureInfo.InvariantCulture); + } + + return parameters; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/IInlineConstraintResolver.cs b/src/Microsoft.AspNet.Routing/IInlineConstraintResolver.cs new file mode 100644 index 0000000000..f2011e1db3 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IInlineConstraintResolver.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing +{ + /// + /// Defines an abstraction for resolving inline constraints as instances of . + /// + public interface IInlineConstraintResolver + { + /// + /// Resolves the inline constraint. + /// + /// The inline constraint to resolve. + /// The the inline constraint was resolved to. + IRouteConstraint ResolveConstraint([NotNull] string inlineConstraint); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/IRouteCollection.cs b/src/Microsoft.AspNet.Routing/IRouteCollection.cs index 072e8341b6..18b5820d94 100644 --- a/src/Microsoft.AspNet.Routing/IRouteCollection.cs +++ b/src/Microsoft.AspNet.Routing/IRouteCollection.cs @@ -7,6 +7,8 @@ namespace Microsoft.AspNet.Routing { IRouter DefaultHandler { get; set; } + IInlineConstraintResolver InlineConstraintResolver { get; set; } + void Add(IRouter router); } } diff --git a/src/Microsoft.AspNet.Routing/InlineRouteParameterParser.cs b/src/Microsoft.AspNet.Routing/InlineRouteParameterParser.cs new file mode 100644 index 0000000000..5209b40e87 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/InlineRouteParameterParser.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Text.RegularExpressions; +using Microsoft.AspNet.Routing.Template; +using Microsoft.AspNet.Routing.Constraints; + +namespace Microsoft.AspNet.Routing +{ + public static class InlineRouteParameterParser + { + // One or more characters, matches "id" + private const string ParameterNamePattern = @"(?.+?)"; + + // Zero or more inline constraints that start with a colon followed by zero or more characters + // Optionally the constraint can have arguments within parentheses + // - necessary to capture characters like ":" and "}" + // Matches ":int", ":length(2)", ":regex(\})", ":regex(:)" zero or more times + private const string ConstraintPattern = @"(:(?.*?(\(.*?\))?))*"; + + // A default value with an equal sign followed by zero or more characters + // Matches "=", "=abc" + private const string DefaultValueParameter = @"(?(=.*?))?"; + + private static readonly Regex _parameterRegex = new Regex( + "^" + ParameterNamePattern + ConstraintPattern + DefaultValueParameter + "$", + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + public static TemplatePart ParseRouteParameter([NotNull] string routeParameter, + [NotNull] IInlineConstraintResolver constraintResolver) + { + var isCatchAll = routeParameter.StartsWith("*", StringComparison.Ordinal); + var isOptional = routeParameter.EndsWith("?", StringComparison.Ordinal); + + routeParameter = isCatchAll ? routeParameter.Substring(1) : routeParameter; + routeParameter = isOptional ? routeParameter.Substring(0, routeParameter.Length - 1) : routeParameter; + + var parameterMatch = _parameterRegex.Match(routeParameter); + if (!parameterMatch.Success) + { + return TemplatePart.CreateParameter(name: string.Empty, + isCatchAll: isCatchAll, + isOptional: isOptional, + defaultValue: null, + inlineConstraint: null + ); + } + + var parameterName = parameterMatch.Groups["parameterName"].Value; + + // Add the default value if present + var defaultValueGroup = parameterMatch.Groups["defaultValue"]; + var defaultValue = GetDefaultValue(defaultValueGroup); + + // Register inline constraints if present + var constraintGroup = parameterMatch.Groups["constraint"]; + var inlineConstraint = GetInlineConstraint(constraintGroup, constraintResolver); + + return TemplatePart.CreateParameter(parameterName, + isCatchAll, + isOptional, + defaultValue, + inlineConstraint); + } + + private static string GetDefaultValue(Group defaultValueGroup) + { + if (defaultValueGroup.Success) + { + var defaultValueMatch = defaultValueGroup.Value; + + // Strip out the equal sign at the beginning + Contract.Assert(defaultValueMatch.StartsWith("=", StringComparison.Ordinal)); + return defaultValueMatch.Substring(1); + } + + return null; + } + + private static IRouteConstraint GetInlineConstraint(Group constraintGroup, + IInlineConstraintResolver constraintResolver) + { + var parameterConstraints = new List(); + foreach (Capture constraintCapture in constraintGroup.Captures) + { + var inlineConstraint = constraintCapture.Value; + var constraint = constraintResolver.ResolveConstraint(inlineConstraint); + if (constraint == null) + { + throw new InvalidOperationException( + Resources.FormatInlineRouteParser_CouldNotResolveConstraint( + constraintResolver.GetType().Name, inlineConstraint)); + } + + parameterConstraints.Add(constraint); + } + + if (parameterConstraints.Count > 0) + { + var constraint = parameterConstraints.Count == 1 ? + parameterConstraints[0] : + new CompositeRouteConstraint(parameterConstraints); + return constraint; + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj b/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj index 466f9091ba..b7a6c5c243 100644 --- a/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj +++ b/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj @@ -22,7 +22,12 @@ + + + + + diff --git a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs index cfc9ad76fe..b226fdddb2 100644 --- a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Routing = new ResourceManager("Microsoft.AspNet.Routing.Resources", typeof(Resources).GetTypeInfo().Assembly); /// - /// The supplied route name '{0}' is ambiguous and matched more than one routes. + /// The supplied route name '{0}' is ambiguous and matched more than one route. /// internal static string NamedRoutes_AmbiguousRoutesFound { @@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Routing } /// - /// The supplied route name '{0}' is ambiguous and matched more than one routes. + /// The supplied route name '{0}' is ambiguous and matched more than one route. /// internal static string FormatNamedRoutes_AmbiguousRoutesFound(object p0) { @@ -42,6 +42,54 @@ namespace Microsoft.AspNet.Routing return GetString("DefaultHandler_MustBeSet"); } + /// + /// The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}. + /// + internal static string DefaultInlineConstraintResolver_AmbiguousCtors + { + get { return GetString("DefaultInlineConstraintResolver_AmbiguousCtors"); } + } + + /// + /// The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}. + /// + internal static string FormatDefaultInlineConstraintResolver_AmbiguousCtors(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_AmbiguousCtors"), p0, p1); + } + + /// + /// Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}. + /// + internal static string DefaultInlineConstraintResolver_CouldNotFindCtor + { + get { return GetString("DefaultInlineConstraintResolver_CouldNotFindCtor"); } + } + + /// + /// Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}. + /// + internal static string FormatDefaultInlineConstraintResolver_CouldNotFindCtor(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_CouldNotFindCtor"), p0, p1); + } + + /// + /// The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface. + /// + internal static string DefaultInlineConstraintResolver_TypeNotConstraint + { + get { return GetString("DefaultInlineConstraintResolver_TypeNotConstraint"); } + } + + /// + /// The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface. + /// + internal static string FormatDefaultInlineConstraintResolver_TypeNotConstraint(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_TypeNotConstraint"), p0, p1, p2); + } + /// /// The constraint entry '{0}' must have a string value or be of a type which implements '{1}'. /// @@ -74,6 +122,22 @@ namespace Microsoft.AspNet.Routing return GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment"); } + /// + /// The route parameter '{0}' has both an inline deafult value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them. + /// + internal static string TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly + { + get { return GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly"); } + } + + /// + /// The route parameter '{0}' has both an inline deafult value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them. + /// + internal static string FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly"), p0); + } + /// /// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. /// @@ -138,6 +202,22 @@ namespace Microsoft.AspNet.Routing return GetString("TemplateRoute_CatchAllCannotBeOptional"); } + /// + /// An optional parameter cannot have default value. + /// + internal static string TemplateRoute_OptionalCannotHaveDefaultValue + { + get { return GetString("TemplateRoute_OptionalCannotHaveDefaultValue"); } + } + + /// + /// An optional parameter cannot have default value. + /// + internal static string FormatTemplateRoute_OptionalCannotHaveDefaultValue() + { + return GetString("TemplateRoute_OptionalCannotHaveDefaultValue"); + } + /// /// A catch-all parameter can only appear as the last segment of the route template. /// @@ -250,12 +330,28 @@ namespace Microsoft.AspNet.Routing return string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_ValidationMustBeStringOrCustomConstraint"), p0, p1, p2); } + /// + /// The inline constraint resolver of type '{0}' was unable to resolve the following inline constraint: '{1}'. + /// + internal static string InlineRouteParser_CouldNotResolveConstraint + { + get { return GetString("InlineRouteParser_CouldNotResolveConstraint"); } + } + + /// + /// The inline constraint resolver of type '{0}' was unable to resolve the following inline constraint: '{1}'. + /// + internal static string FormatInlineRouteParser_CouldNotResolveConstraint(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("InlineRouteParser_CouldNotResolveConstraint"), p0, p1); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); System.Diagnostics.Debug.Assert(value != null); - + if (formatterNames != null) { for (var i = 0; i < formatterNames.Length; i++) diff --git a/src/Microsoft.AspNet.Routing/Resources.resx b/src/Microsoft.AspNet.Routing/Resources.resx index 0f73bb2cd9..ba89de6413 100644 --- a/src/Microsoft.AspNet.Routing/Resources.resx +++ b/src/Microsoft.AspNet.Routing/Resources.resx @@ -123,12 +123,24 @@ A default handler must be set on the RouteCollection. + + The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}. + + + Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}. + + + The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface. + The constraint entry '{0}' must have a string value or be of a type which implements '{1}'. A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. + + The route parameter '{0}' has both an inline default value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them. + A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. @@ -141,6 +153,9 @@ A catch-all parameter cannot be marked optional. + + An optional parameter cannot have default value. + A catch-all parameter can only appear as the last segment of the route template. @@ -162,4 +177,7 @@ The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'. + + The inline constraint resolver of type '{0}' was unable to resolve the following inline constraint: '{1}'. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/RouteCollection.cs b/src/Microsoft.AspNet.Routing/RouteCollection.cs index 16eef2c4b6..b14183046a 100644 --- a/src/Microsoft.AspNet.Routing/RouteCollection.cs +++ b/src/Microsoft.AspNet.Routing/RouteCollection.cs @@ -26,6 +26,8 @@ namespace Microsoft.AspNet.Routing public IRouter DefaultHandler { get; set; } + public IInlineConstraintResolver InlineConstraintResolver { get; set; } + public void Add([NotNull] IRouter router) { var namedRouter = router as INamedRouter; diff --git a/src/Microsoft.AspNet.Routing/RouteCollectionExtensions.cs b/src/Microsoft.AspNet.Routing/RouteCollectionExtensions.cs index 412e7e9236..5b17ea3e44 100644 --- a/src/Microsoft.AspNet.Routing/RouteCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Routing/RouteCollectionExtensions.cs @@ -30,14 +30,23 @@ namespace Microsoft.AspNet.Routing throw new InvalidOperationException(Resources.DefaultHandler_MustBeSet); } - routes.Add(new TemplateRoute(routes.DefaultHandler, name, template, defaults, constraints: null)); + routes.Add(new TemplateRoute(routes.DefaultHandler, + name, + template, + defaults, + constraints: null, + inlineConstraintResolver: routes.InlineConstraintResolver)); return routes; } public static IRouteCollection MapRoute(this IRouteCollection routes, string name, string template, object defaults, object constraints) { - MapRoute(routes, name, template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)); + MapRoute(routes, + name, + template, + new RouteValueDictionary(defaults), + new RouteValueDictionary(constraints)); return routes; } @@ -55,15 +64,23 @@ namespace Microsoft.AspNet.Routing return routes; } - public static IRouteCollection MapRoute(this IRouteCollection routes, string name, string template, - IDictionary defaults, IDictionary constraints) + public static IRouteCollection MapRoute(this IRouteCollection routes, + string name, + string template, + IDictionary defaults, + IDictionary constraints) { if (routes.DefaultHandler == null) { throw new InvalidOperationException(Resources.DefaultHandler_MustBeSet); } - - routes.Add(new TemplateRoute(routes.DefaultHandler, name, template, defaults, constraints)); + + routes.Add(new TemplateRoute(routes.DefaultHandler, + name, + template, + defaults, + constraints, + routes.InlineConstraintResolver)); return routes; } } diff --git a/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs b/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs index 607985627f..92948f9ab6 100644 --- a/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs +++ b/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.AspNet.Routing.Constraints; namespace Microsoft.AspNet.Routing { diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs index 288f8a5507..be6dc62012 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs @@ -3,10 +3,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; -using System.Linq; namespace Microsoft.AspNet.Routing.Template { @@ -18,7 +16,7 @@ namespace Microsoft.AspNet.Routing.Template private const char EqualsSign = '='; private const char QuestionMark = '?'; - public static Template Parse(string routeTemplate) + public static Template Parse(string routeTemplate, IInlineConstraintResolver constraintResolver) { if (routeTemplate == null) { @@ -30,7 +28,7 @@ namespace Microsoft.AspNet.Routing.Template throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, "routeTemplate"); } - var context = new TemplateParserContext(routeTemplate); + var context = new TemplateParserContext(routeTemplate, constraintResolver); var segments = new List(); while (context.Next()) @@ -174,24 +172,35 @@ namespace Microsoft.AspNet.Routing.Template } } - var rawName = context.Capture(); + var rawParameter = context.Capture(); - var isCatchAll = rawName.StartsWith("*", StringComparison.Ordinal); - var isOptional = rawName.EndsWith("?", StringComparison.Ordinal); + // At this point, we need to parse the raw name for inline constraint, + // default values and optional parameters. + var templatePart = InlineRouteParameterParser.ParseRouteParameter(rawParameter, + context.ConstraintResolver); - if (isCatchAll && isOptional) + + + if (templatePart.IsCatchAll && templatePart.IsOptional) { context.Error = Resources.TemplateRoute_CatchAllCannotBeOptional; return false; } - rawName = isCatchAll ? rawName.Substring(1) : rawName; - rawName = isOptional ? rawName.Substring(0, rawName.Length - 1) : rawName; + if (templatePart.IsOptional && templatePart.DefaultValue != null) + { + // Cannot be optional and have a default value. + // The only way to declare an optional parameter is to have a ? at the end, + // hence we cannot have both default value and optional parameter within the template. + // A workaround is to add it as a separate entry in the defaults argument. + context.Error = Resources.TemplateRoute_OptionalCannotHaveDefaultValue; + return false; + } - var parameterName = rawName; + var parameterName = templatePart.Name; if (IsValidParameterName(context, parameterName)) { - segment.Parts.Add(TemplatePart.CreateParameter(parameterName, isCatchAll, isOptional)); + segment.Parts.Add(templatePart); return true; } else @@ -392,12 +401,13 @@ namespace Microsoft.AspNet.Routing.Template private HashSet _parameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); - public TemplateParserContext(string template) + public TemplateParserContext(string template, IInlineConstraintResolver constraintResolver) { Contract.Assert(template != null); _template = template; _index = -1; + ConstraintResolver = constraintResolver; } public char Current @@ -416,6 +426,12 @@ namespace Microsoft.AspNet.Routing.Template get { return _parameterNames; } } + public IInlineConstraintResolver ConstraintResolver + { + get; + private set; + } + public bool Back() { return --_index >= 0; diff --git a/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs b/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs index 3f54995276..1ab442eb60 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs @@ -17,7 +17,11 @@ namespace Microsoft.AspNet.Routing.Template }; } - public static TemplatePart CreateParameter(string name, bool isCatchAll, bool isOptional) + public static TemplatePart CreateParameter([NotNull] string name, + bool isCatchAll, + bool isOptional, + object defaultValue, + IRouteConstraint inlineConstraint) { return new TemplatePart() { @@ -25,6 +29,8 @@ namespace Microsoft.AspNet.Routing.Template Name = name, IsCatchAll = isCatchAll, IsOptional = isOptional, + DefaultValue = defaultValue, + InlineConstraint = inlineConstraint, }; } @@ -34,6 +40,8 @@ namespace Microsoft.AspNet.Routing.Template public bool IsOptional { get; private set; } public string Name { get; private set; } public string Text { get; private set; } + public object DefaultValue { get; private set; } + public IRouteConstraint InlineConstraint { get; private set; } internal string DebuggerToString() { diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs index 985ce2dabb..30687c2024 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics.Contracts; using System.Threading.Tasks; +using Microsoft.AspNet.Routing.Constraints; namespace Microsoft.AspNet.Routing.Template { @@ -18,16 +18,17 @@ namespace Microsoft.AspNet.Routing.Template private readonly TemplateMatcher _matcher; private readonly TemplateBinder _binder; - public TemplateRoute(IRouter target, string routeTemplate) - : this(target, routeTemplate, null, null) + public TemplateRoute(IRouter target, string routeTemplate, IInlineConstraintResolver inlineConstraintResolver) + : this(target, routeTemplate, null, null, inlineConstraintResolver) { } public TemplateRoute([NotNull] IRouter target, string routeTemplate, IDictionary defaults, - IDictionary constraints) - : this(target, null, routeTemplate, defaults, constraints) + IDictionary constraints, + IInlineConstraintResolver inlineConstraintResolver) + : this(target, null, routeTemplate, defaults, constraints, inlineConstraintResolver) { } @@ -35,17 +36,20 @@ namespace Microsoft.AspNet.Routing.Template string routeName, string routeTemplate, IDictionary defaults, - IDictionary constraints) + IDictionary constraints, + IInlineConstraintResolver inlineConstraintResolver) { _target = target; _routeTemplate = routeTemplate ?? string.Empty; Name = routeName; _defaults = defaults ?? new Dictionary(StringComparer.OrdinalIgnoreCase); - _constraints = RouteConstraintBuilder.BuildConstraints(constraints, _routeTemplate); + _constraints = RouteConstraintBuilder.BuildConstraints(constraints, _routeTemplate) ?? + new Dictionary(); // The parser will throw for invalid routes. - _parsedTemplate = TemplateParser.Parse(RouteTemplate); - + _parsedTemplate = TemplateParser.Parse(RouteTemplate, inlineConstraintResolver); + UpdateInlineDefaultValuesAndConstraints(); + _matcher = new TemplateMatcher(_parsedTemplate); _binder = new TemplateBinder(_parsedTemplate, _defaults); } @@ -170,5 +174,39 @@ namespace Microsoft.AspNet.Routing.Template ProvidedValues = providedValues, }; } + + private void UpdateInlineDefaultValuesAndConstraints() + { + foreach (var parameter in _parsedTemplate.Parameters) + { + if (parameter.InlineConstraint != null) + { + IRouteConstraint constraint; + if (_constraints.TryGetValue(parameter.Name, out constraint)) + { + _constraints[parameter.Name] = + new CompositeRouteConstraint(new []{ constraint, parameter.InlineConstraint }); + } + else + { + _constraints[parameter.Name] = parameter.InlineConstraint; + } + } + + if (parameter.DefaultValue != null) + { + if (_defaults.ContainsKey(parameter.Name)) + { + throw new InvalidOperationException( + Resources. + FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(parameter.Name)); + } + else + { + _defaults[parameter.Name] = parameter.DefaultValue; + } + } + } + } } } diff --git a/test/Microsoft.AspNet.Routing.Tests/ConstraintsBuilderTests.cs b/test/Microsoft.AspNet.Routing.Tests/ConstraintsBuilderTests.cs index a5053f048b..0f922d2d7d 100644 --- a/test/Microsoft.AspNet.Routing.Tests/ConstraintsBuilderTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/ConstraintsBuilderTests.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Routing.Constraints; using Microsoft.AspNet.Testing; using Moq; using Xunit; diff --git a/test/Microsoft.AspNet.Routing.Tests/RegexConstraintTests.cs b/test/Microsoft.AspNet.Routing.Tests/RegexConstraintTests.cs index e4164536e9..a0e85ba860 100644 --- a/test/Microsoft.AspNet.Routing.Tests/RegexConstraintTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/RegexConstraintTests.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Text.RegularExpressions; using System.Threading; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Routing.Constraints; using Moq; using Xunit; diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs index e489d2b173..aaacb0a17a 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs @@ -127,7 +127,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests string expected) { // Arrange - var binder = new TemplateBinder(TemplateParser.Parse(template), defaults); + var binder = new TemplateBinder(TemplateParser.Parse(template, new DefaultInlineConstraintResolver()), + defaults); // Act & Assert var acceptedValues = binder.GetAcceptedValues(null, values); @@ -960,7 +961,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests string expected) { // Arrange - var binder = new TemplateBinder(TemplateParser.Parse(template), defaults); + var binder = new TemplateBinder(TemplateParser.Parse(template, new DefaultInlineConstraintResolver()), defaults); // Act & Assert var acceptedValues = binder.GetAcceptedValues(ambientValues, values); diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs index 5b73944912..3a8eb4bb38 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs @@ -784,13 +784,13 @@ namespace Microsoft.AspNet.Routing.Template.Tests private TemplateMatcher CreateMatcher(string template) { - return new TemplateMatcher(TemplateParser.Parse(template)); + return new TemplateMatcher(TemplateParser.Parse(template, new DefaultInlineConstraintResolver())); } private static void RunTest(string template, string path, IDictionary defaults, IDictionary expected) { // Arrange - var matcher = new TemplateMatcher(TemplateParser.Parse(template)); + var matcher = new TemplateMatcher(TemplateParser.Parse(template, new DefaultInlineConstraintResolver())); // Act var match = matcher.Match(path, defaults); diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs index f540b42aa4..72ada89942 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs @@ -10,6 +10,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests { public class TemplateRouteParserTests { + private IInlineConstraintResolver _inlineConstraintResolver = new DefaultInlineConstraintResolver(); + [Fact] public void Parse_SingleLiteral() { @@ -21,7 +23,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); // Act - var actual = TemplateParser.Parse(template); + var actual = TemplateParser.Parse(template, _inlineConstraintResolver); // Assert Assert.Equal