diff --git a/samples/RoutingSample.Web/Startup.cs b/samples/RoutingSample.Web/Startup.cs index c0f8704d97..fb3dc00172 100644 --- a/samples/RoutingSample.Web/Startup.cs +++ b/samples/RoutingSample.Web/Startup.cs @@ -1,8 +1,7 @@ using System.Text.RegularExpressions; -using Microsoft.AspNet; using Microsoft.AspNet.Builder; -using Microsoft.AspNet.Http; using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Routing.Constraints; namespace RoutingSample.Web { diff --git a/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj b/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj index b7a6c5c243..f82446805f 100644 --- a/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj +++ b/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj @@ -24,6 +24,7 @@ + @@ -33,7 +34,6 @@ - @@ -52,4 +52,4 @@ - + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs index 7e1981f3c7..be6dc62012 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs @@ -196,10 +196,6 @@ namespace Microsoft.AspNet.Routing.Template context.Error = Resources.TemplateRoute_OptionalCannotHaveDefaultValue; return false; } - // A workaround is to add it as a separate entry in the defaults argument. - context.Error = Resources.TemplateRoute_OptionalCannotHaveDefaultValue; - return false; - } var parameterName = templatePart.Name; if (IsValidParameterName(context, parameterName)) diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs index c86a880689..3979ddf413 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -43,13 +43,13 @@ namespace Microsoft.AspNet.Routing.Template _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, inlineConstraintResolver); UpdateInlineDefaultValuesAndConstraints(); - + _matcher = new TemplateMatcher(_parsedTemplate); _binder = new TemplateBinder(_parsedTemplate, _defaults); } @@ -184,14 +184,13 @@ namespace Microsoft.AspNet.Routing.Template IRouteConstraint constraint; if (_constraints.TryGetValue(parameter.Name, out constraint)) { - _constraints[parameter.Name] = - new CompositeRouteConstraint(new []{ constraint, parameter.InlineConstraint }); + _constraints[parameter.Name] = + new CompositeRouteConstraint(new[] { constraint, parameter.InlineConstraint }); } else { _constraints[parameter.Name] = parameter.InlineConstraint; } - } } if (parameter.DefaultValue != null) @@ -210,4 +209,4 @@ namespace Microsoft.AspNet.Routing.Template } } } -} +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/DefaultInlineConstraintResolverTest.cs b/test/Microsoft.AspNet.Routing.Tests/DefaultInlineConstraintResolverTest.cs new file mode 100644 index 0000000000..5e60434e23 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/DefaultInlineConstraintResolverTest.cs @@ -0,0 +1,81 @@ +// 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 Microsoft.AspNet.Http; +using Microsoft.AspNet.Routing.Constraints; +using Xunit; + +namespace Microsoft.AspNet.Routing.Tests +{ + public class DefaultInlineConstraintResolverTest + { + [Fact] + public void ResolveConstraint_IntConstraint_ResolvesCorrectly() + { + // Arrange & Act + var constraint = new DefaultInlineConstraintResolver().ResolveConstraint("int"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_IntConstraintWithArgument_Throws() + { + // Act & Assert + var ex = Assert.Throws( + () => new DefaultInlineConstraintResolver().ResolveConstraint("int(5)")); + Assert.Equal("Could not find a constructor for constraint type 'IntRouteConstraint'"+ + " with the following number of parameters: 1.", + ex.Message); + } + + [Fact] + public void ResolveConstraint_SupportsCustomConstraints() + { + // Arrange + var resolver = new DefaultInlineConstraintResolver(); + resolver.ConstraintMap.Add("custom", typeof(CustomRouteConstraint)); + + // Act + var constraint = resolver.ResolveConstraint("custom(argument)"); + + // Assert + Assert.IsType(constraint); + } + + [Fact] + public void ResolveConstraint_CustomConstraintThatDoesNotImplementIRouteConstraint_Throws() + { + // Arrange + var resolver = new DefaultInlineConstraintResolver(); + resolver.ConstraintMap.Add("custom", typeof(string)); + + // Act & Assert + var ex = Assert.Throws(() => resolver.ResolveConstraint("custom")); + Assert.Equal("The constraint type 'System.String' which is mapped to constraint key 'custom'"+ + " must implement the 'IRouteConstraint' interface.", + ex.Message); + } + + private class CustomRouteConstraint : IRouteConstraint + { + public CustomRouteConstraint(string pattern) + { + Pattern = pattern; + } + + public string Pattern { get; private set; } + public bool Match(HttpContext httpContext, + IRouter route, + string routeKey, + IDictionary values, + RouteDirection routeDirection) + { + return true; + } + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/InlineRouteParameterParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/InlineRouteParameterParserTests.cs new file mode 100644 index 0000000000..20bf7be646 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/InlineRouteParameterParserTests.cs @@ -0,0 +1,304 @@ +// 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.Linq; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Routing.Constraints; +using Microsoft.AspNet.Routing.Template; +using Xunit; + +namespace Microsoft.AspNet.Routing.Tests +{ + public class InlineRouteParameterParserTests + { + [Fact] + public void ParseRouteParameter_ConstraintAndDefault_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter("param:int=111111"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("111111", templatePart.DefaultValue); + Assert.IsType(templatePart.InlineConstraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithArgumentsAndDefault_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+)=111111"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("111111", templatePart.DefaultValue); + Assert.IsType(templatePart.InlineConstraint); + Assert.Equal(@"\d+", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); + } + + [Fact] + public void ParseRouteParameter_ConstraintAndOptional_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:int?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.True(templatePart.IsOptional); + Assert.IsType(templatePart.InlineConstraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+)?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.True(templatePart.IsOptional); + Assert.Equal(@"\d+", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); + } + + [Fact] + public void ParseRouteParameter_ChainedConstraints_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+):test(\w+)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.IsType(templatePart.InlineConstraint); + var constraint = (CompositeRouteConstraint)templatePart.InlineConstraint; + Assert.Equal(@"\d+", ((TestRouteConstraint)constraint.Constraints.ElementAt(0)).Pattern); + Assert.Equal(@"\w+", ((TestRouteConstraint)constraint.Constraints.ElementAt(1)).Pattern); + } + + [Fact] + public void ParseRouteTemplate_ConstraintsDefaultsAndOptionalsInMultipleSections_ParsedCorrectly() + { + // Arrange & Act + var template = ParseRouteTemplate(@"some/url-{p1:int:test(3)=hello}/{p2=abc}/{p3?}"); + + // Assert + var parameters = template.Parameters.ToArray(); + + var param1 = parameters[0]; + Assert.Equal("p1", param1.Name); + Assert.Equal("hello", param1.DefaultValue); + Assert.False(param1.IsOptional); + Assert.IsType(param1.InlineConstraint); + var constraint = (CompositeRouteConstraint)param1.InlineConstraint; + Assert.IsType(constraint.Constraints.ElementAt(0)); + Assert.IsType(constraint.Constraints.ElementAt(1)); + + var param2 = parameters[1]; + Assert.Equal("p2", param2.Name); + Assert.Equal("abc", param2.DefaultValue); + Assert.False(param2.IsOptional); + + var param3 = parameters[2]; + Assert.Equal("p3", param3.Name); + Assert.True(param3.IsOptional); + } + + [Fact] + public void ParseRouteParameter_NoTokens_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter("world"); + + // Assert + Assert.Equal("world", templatePart.Name); + } + + [Fact] + public void ParseRouteParameter_ParamDefault_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter("param=world"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("world", templatePart.DefaultValue); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_ClosingBraceIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\})"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.IsType(templatePart.InlineConstraint); + Assert.Equal(@"\}", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithClosingParenInPattern_ClosingParenIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\))"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.IsType(templatePart.InlineConstraint); + Assert.Equal(@"\)", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithColonInPattern_ColonIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(:)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.IsType(templatePart.InlineConstraint); + Assert.Equal(@":", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithCommaInPattern_PatternIsParsedCorrectly() + { + // Arrange + var templatePart = ParseParameter(@"param:test(\w,\w)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.IsType(templatePart.InlineConstraint); + Assert.Equal(@"\w,\w", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); + } + + [Theory] + [InlineData(",")] + [InlineData("(")] + [InlineData(")")] + [InlineData("}")] + [InlineData("{")] + public void ParseRouteParameter_MisplacedSpecialCharacterInParameter_Throws(string character) + { + // Arrange + var unresolvedConstraint = character + @"test(\w,\w)"; + var parameter = "param:" + unresolvedConstraint; + + // Act & Assert + var ex = Assert.Throws(() => ParseParameter(parameter)); + Assert.Equal(@"The inline constraint resolver of type 'DefaultInlineConstraintResolver'"+ + " was unable to resolve the following inline constraint: '"+ unresolvedConstraint + "'.", + ex.Message); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(=)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.IsType(templatePart.InlineConstraint); + Assert.Equal(@"=", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\{)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.IsType(templatePart.InlineConstraint); + Assert.Equal(@"\{", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\()"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.IsType(templatePart.InlineConstraint); + Assert.Equal(@"\(", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\?)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); + Assert.IsType(templatePart.InlineConstraint); + Assert.Equal(@"\?", ((TestRouteConstraint)templatePart.InlineConstraint).Pattern); + } + + [Theory] + [InlineData("", "")] + [InlineData("?", "")] + [InlineData("*", "")] + [InlineData(" ", " ")] + [InlineData("\t", "\t")] + [InlineData("#!@#$%Q@#@%", "#!@#$%Q@#@%")] + [InlineData(",,,", ",,,")] + public void ParseRouteParameter_ParameterWithoutInlineConstraint_ReturnsTemplatePartWithEmptyInlineValues( + string parameter, + string expectedParameterName) + { + // Arrange & Act + var templatePart = ParseParameter(parameter); + + // Assert + Assert.Equal(expectedParameterName, templatePart.Name); + Assert.Null(templatePart.InlineConstraint); + Assert.Null(templatePart.DefaultValue); + } + + + private TemplatePart ParseParameter(string routeParameter) + { + var constraintResolver = new DefaultInlineConstraintResolver(); + + // TODO: This will be removed once this is supported in product code. + constraintResolver.ConstraintMap.Add("test", typeof(TestRouteConstraint)); + var templatePart = InlineRouteParameterParser.ParseRouteParameter(routeParameter, constraintResolver); + return templatePart; + } + + private static Template.Template ParseRouteTemplate(string template) + { + var constraintResolver = new DefaultInlineConstraintResolver(); + + constraintResolver.ConstraintMap.Add("test", typeof(TestRouteConstraint)); + return TemplateParser.Parse(template, constraintResolver); + } + + private class TestRouteConstraint : IRouteConstraint + { + public TestRouteConstraint(string pattern) + { + Pattern = pattern; + } + + public string Pattern { get; private set; } + public bool Match(HttpContext httpContext, + IRouter route, + string routeKey, + IDictionary values, + RouteDirection routeDirection) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Microsoft.AspNet.Routing.Tests.kproj b/test/Microsoft.AspNet.Routing.Tests/Microsoft.AspNet.Routing.Tests.kproj index 1e7eda8fbb..fadc051a54 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Microsoft.AspNet.Routing.Tests.kproj +++ b/test/Microsoft.AspNet.Routing.Tests/Microsoft.AspNet.Routing.Tests.kproj @@ -22,8 +22,11 @@ + + + diff --git a/test/Microsoft.AspNet.Routing.Tests/RouteConstraintsTests.cs b/test/Microsoft.AspNet.Routing.Tests/RouteConstraintsTests.cs new file mode 100644 index 0000000000..3f1fa3f2ec --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/RouteConstraintsTests.cs @@ -0,0 +1,97 @@ +// 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. + +#if NET45 + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Routing.Constraints; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Routing.Tests +{ + public class RouteConstraintsTests + { + [Theory] + [InlineData(42, true)] + [InlineData("42", true)] + [InlineData(3.14, false)] + [InlineData("43.567", false)] + [InlineData("42a", false)] + public void IntRouteConstraint_Match_AppliesConstraint(object parameterValue, bool expected) + { + // Arrange + var constraint = new IntRouteConstraint(); + + // Act + var actual = TestValue(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(true, true, true)] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, false)] + public void CompoundRouteConstraint_Match_CallsMatchOnInnerConstraints(bool inner1Result, + bool inner2Result, + bool expected) + { + // Arrange + var inner1 = MockConstraintWithResult(inner1Result); + var inner2 = MockConstraintWithResult(inner2Result); + + // Act + var constraint = new CompositeRouteConstraint(new[] { inner1.Object, inner2.Object }); + var actual = TestValue(constraint, null); + + // Assert + Assert.Equal(expected, actual); + } + + static Expression> ConstraintMatchMethodExpression = + c => c.Match(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()); + + private static Mock MockConstraintWithResult(bool result) + { + var mock = new Mock(); + mock.Setup(ConstraintMatchMethodExpression) + .Returns(result) + .Verifiable(); + return mock; + } + + private static void AssertMatchWasCalled(Mock mock, Times times) + { + mock.Verify(ConstraintMatchMethodExpression, times); + } + + private static bool TestValue(IRouteConstraint constraint, object value, Action routeConfig = null) + { + var context = new Mock(); + + IRouter route = new RouteCollection(); + + if (routeConfig != null) + { + routeConfig(route); + } + + var parameterName = "fake"; + var values = new Dictionary() { { parameterName, value } }; + var routeDirection = RouteDirection.IncomingRequest; + return constraint.Match(context.Object, route, parameterName, values, routeDirection); + } + } +} + +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs index 336f714425..70efab66f1 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs @@ -4,6 +4,7 @@ #if NET45 using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Routing.Constraints; @@ -425,6 +426,49 @@ namespace Microsoft.AspNet.Routing.Template.Tests Assert.Equal(mockConstraint, constraints["action"]); } + [Fact] + public void RegisteringRouteWithOneInlineConstraintAndOneUsingConstraintArgument() + { + // Arrange + var collection = new RouteCollection(); + collection.DefaultHandler = new Mock().Object; + collection.InlineConstraintResolver = new DefaultInlineConstraintResolver(); + + collection.MapRoute("mockName", + "{controller}/{action}/{id:int}", + defaults: null, + constraints: new { id = "1*" }); + + var constraints = ((TemplateRoute)collection[0]).Constraints; + + // Assert + Assert.Equal(1, constraints.Count); + var constraint = (CompositeRouteConstraint)constraints["id"]; + Assert.IsType(constraint); + Assert.IsType(constraint.Constraints.ElementAt(0)); + Assert.IsType(constraint.Constraints.ElementAt(1)); + } + + [Fact] + public void RegisteringRoute_WithOneInlineConstraint_AddsItToConstraintCollection() + { + // Arrange + var collection = new RouteCollection(); + collection.DefaultHandler = new Mock().Object; + collection.InlineConstraintResolver = new DefaultInlineConstraintResolver(); + + collection.MapRoute("mockName", + "{controller}/{action}/{id:int}", + defaults: null, + constraints: null); + + var constraints = ((TemplateRoute)collection[0]).Constraints; + + // Assert + Assert.Equal(1, constraints.Count); + Assert.IsType(constraints["id"]); + } + [Fact] public void RegisteringRouteWithRouteName_WithNullDefaults_AddsTheRoute() {