diff --git a/src/Microsoft.AspNetCore.Routing/DefaultInlineConstraintResolver.cs b/src/Microsoft.AspNetCore.Routing/DefaultInlineConstraintResolver.cs index 5d335f0dd9..7426516db2 100644 --- a/src/Microsoft.AspNetCore.Routing/DefaultInlineConstraintResolver.cs +++ b/src/Microsoft.AspNetCore.Routing/DefaultInlineConstraintResolver.cs @@ -69,12 +69,25 @@ namespace Microsoft.AspNetCore.Routing if (!typeof(IRouteConstraint).GetTypeInfo().IsAssignableFrom(constraintType.GetTypeInfo())) { - throw new InvalidOperationException( + throw new RouteCreationException( Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint( constraintType, constraintKey, typeof(IRouteConstraint).Name)); } - return CreateConstraint(constraintType, argumentString); + try + { + return CreateConstraint(constraintType, argumentString); + } + catch (RouteCreationException) + { + throw; + } + catch (Exception exception) + { + throw new RouteCreationException( + $"An error occurred while trying to create an instance of route constraint '{constraintType.FullName}'.", + exception); + } } private static IRouteConstraint CreateConstraint(Type constraintType, string argumentString) @@ -107,7 +120,7 @@ namespace Microsoft.AspNetCore.Routing if (constructorMatches == 0) { - throw new InvalidOperationException( + throw new RouteCreationException( Resources.FormatDefaultInlineConstraintResolver_CouldNotFindCtor( constraintTypeInfo.Name, arguments.Length)); } @@ -118,7 +131,7 @@ namespace Microsoft.AspNetCore.Routing } else { - throw new InvalidOperationException( + throw new RouteCreationException( Resources.FormatDefaultInlineConstraintResolver_AmbiguousCtors( constraintTypeInfo.Name, arguments.Length)); } diff --git a/src/Microsoft.AspNetCore.Routing/MapRouteRouteBuilderExtensions.cs b/src/Microsoft.AspNetCore.Routing/MapRouteRouteBuilderExtensions.cs index cf46749f3d..d3cc115e6d 100644 --- a/src/Microsoft.AspNetCore.Routing/MapRouteRouteBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Routing/MapRouteRouteBuilderExtensions.cs @@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Builder /// The name of the route. /// The URL pattern of the route. /// - /// An object that contains default values for route parameters. The object's properties represent the names + /// An object that contains default values for route parameters. The object's properties represent the names /// and values of the default values. /// /// A reference to this instance after the operation has completed. @@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.Builder { if (routeBuilder.DefaultHandler == null) { - throw new InvalidOperationException(Resources.DefaultHandler_MustBeSet); + throw new RouteCreationException(Resources.DefaultHandler_MustBeSet); } var inlineConstraintResolver = routeBuilder diff --git a/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs index 6abdc4ea48..9dd91e7f85 100644 --- a/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs @@ -442,6 +442,22 @@ namespace Microsoft.AspNetCore.Routing return string.Format(CultureInfo.CurrentCulture, GetString("UnableToFindServices"), p0, p1, p2); } + /// + /// An error occurred while creating the route with name '{0}' and template '{1}'. + /// + internal static string TemplateRoute_Exception + { + get { return GetString("TemplateRoute_Exception"); } + } + + /// + /// An error occurred while creating the route with name '{0}' and template '{1}'. + /// + internal static string FormatTemplateRoute_Exception(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_Exception"), p0, p1); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Routing/Resources.resx b/src/Microsoft.AspNetCore.Routing/Resources.resx index 97217dfa25..6af7dff663 100644 --- a/src/Microsoft.AspNetCore.Routing/Resources.resx +++ b/src/Microsoft.AspNetCore.Routing/Resources.resx @@ -198,4 +198,7 @@ Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. + + An error occurred while creating the route with name '{0}' and template '{1}'. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Routing/RouteBase.cs b/src/Microsoft.AspNetCore.Routing/RouteBase.cs index 05b70c6cfb..f05d2d5470 100644 --- a/src/Microsoft.AspNetCore.Routing/RouteBase.cs +++ b/src/Microsoft.AspNetCore.Routing/RouteBase.cs @@ -46,12 +46,19 @@ namespace Microsoft.AspNetCore.Routing ConstraintResolver = constraintResolver; DataTokens = dataTokens ?? new RouteValueDictionary(); - // Data we parse from the template will be used to fill in the rest of the constraints or - // defaults. The parser will throw for invalid routes. - ParsedTemplate = TemplateParser.Parse(template); + try + { + // Data we parse from the template will be used to fill in the rest of the constraints or + // defaults. The parser will throw for invalid routes. + ParsedTemplate = TemplateParser.Parse(template); - Constraints = GetConstraints(constraintResolver, ParsedTemplate, constraints); - Defaults = GetDefaults(ParsedTemplate, defaults); + Constraints = GetConstraints(constraintResolver, ParsedTemplate, constraints); + Defaults = GetDefaults(ParsedTemplate, defaults); + } + catch (Exception exception) + { + throw new RouteCreationException(Resources.FormatTemplateRoute_Exception(name, template), exception); + } } public virtual IDictionary Constraints { get; protected set; } diff --git a/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs b/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs index bd2dfca275..c230ed0e96 100644 --- a/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs +++ b/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs @@ -111,7 +111,7 @@ namespace Microsoft.AspNetCore.Routing var regexPattern = value as string; if (regexPattern == null) { - throw new InvalidOperationException( + throw new RouteCreationException( Resources.FormatRouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint( key, value, diff --git a/src/Microsoft.AspNetCore.Routing/RouteCreationException.cs b/src/Microsoft.AspNetCore.Routing/RouteCreationException.cs new file mode 100644 index 0000000000..0c47e7e412 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/RouteCreationException.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// The exception that is thrown for invalid routes or constraints. + /// + public class RouteCreationException : Exception + { + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public RouteCreationException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + public RouteCreationException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/DefaultInlineConstraintResolverTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/DefaultInlineConstraintResolverTest.cs index 128d50ee35..bba78fb6b2 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/DefaultInlineConstraintResolverTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/DefaultInlineConstraintResolverTest.cs @@ -46,8 +46,9 @@ namespace Microsoft.AspNetCore.Routing.Tests public void ResolveConstraint_IntConstraintWithArgument_Throws() { // Arrange, Act & Assert - var ex = Assert.Throws( + var ex = Assert.Throws( () => _constraintResolver.ResolveConstraint("int(5)")); + Assert.Equal("Could not find a constructor for constraint type 'IntRouteConstraint'" + " with the following number of parameters: 1.", ex.Message); @@ -276,7 +277,7 @@ namespace Microsoft.AspNetCore.Routing.Tests var resolver = GetInlineConstraintResolver(routeOptions); // Act & Assert - var ex = Assert.Throws(() => resolver.ResolveConstraint("custom")); + 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); @@ -291,7 +292,7 @@ namespace Microsoft.AspNetCore.Routing.Tests var resolver = GetInlineConstraintResolver(routeOptions); // Act & Assert - var ex = Assert.Throws(() => resolver.ResolveConstraint("custom(5,6)")); + var ex = Assert.Throws(() => resolver.ResolveConstraint("custom(5,6)")); Assert.Equal("The constructor to use for activating the constraint type 'MultiConstructorRouteConstraint' is ambiguous." + " Multiple constructors were found with the following number of parameters: 2.", ex.Message); @@ -317,7 +318,7 @@ namespace Microsoft.AspNetCore.Routing.Tests { // Arrange // Act & Assert - var ex = Assert.Throws(() => _constraintResolver.ResolveConstraint("int(5,6)")); + var ex = Assert.Throws(() => _constraintResolver.ResolveConstraint("int(5,6)")); Assert.Equal("Could not find a constructor for constraint type 'IntRouteConstraint'" + " with the following number of parameters: 2.", ex.Message); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/RouteConstraintBuilderTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/RouteConstraintBuilderTest.cs index 8aceb54da0..fefd735a81 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/RouteConstraintBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/RouteConstraintBuilderTest.cs @@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Routing var builder = CreateBuilder("{controller}/{action}"); // Act & Assert - ExceptionAssert.Throws( + ExceptionAssert.Throws( () => builder.AddConstraint("controller", 5), "The constraint entry 'controller' - '5' on the route " + "'{controller}/{action}' must have a string value or be of a type which implements '" + @@ -117,7 +117,7 @@ namespace Microsoft.AspNetCore.Routing [Fact] public void AddResolvedConstraint_SetOptionalParameter_AfterAddingTheParameter() { - var builder = CreateBuilder("{controller}/{action}/{id}"); + var builder = CreateBuilder("{controller}/{action}/{id}"); builder.AddResolvedConstraint("id", "int"); builder.SetOptional("id"); @@ -140,7 +140,7 @@ namespace Microsoft.AspNetCore.Routing Assert.Equal(1, result.Count); Assert.Equal("name", result.First().Key); Assert.IsType(Assert.Single(result).Value); - var optionalConstraint = (OptionalRouteConstraint)result.First().Value; + var optionalConstraint = (OptionalRouteConstraint)result.First().Value; var compositeConstraint = Assert.IsType(optionalConstraint.InnerConstraint); ; Assert.Equal(compositeConstraint.Constraints.Count(), 2); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/RouteTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/RouteTest.cs index ac6333a0f6..ceab3f1d3d 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/RouteTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/RouteTest.cs @@ -30,11 +30,8 @@ namespace Microsoft.AspNetCore.Routing // Arrange var template = @"{controller}/{action}/ {p1:regex(abc} "; var mockTarget = new Mock(MockBehavior.Strict); - var expected = "The constraint entry 'p1' - 'regex(abc' on the route " + - "'{controller}/{action}/ {p1:regex(abc} ' could not be resolved by the constraint resolver of type " + - "'IInlineConstraintResolverProxy'."; - var exception = Assert.Throws( + var exception = Assert.Throws( () => new Route( mockTarget.Object, template, @@ -43,7 +40,15 @@ namespace Microsoft.AspNetCore.Routing dataTokens: null, inlineConstraintResolver: _inlineConstraintResolver)); + var expected = "An error occurred while creating the route with name '' and template" + + $" '{template}'."; Assert.Equal(expected, exception.Message); + + Assert.NotNull(exception.InnerException); + expected = "The constraint entry 'p1' - 'regex(abc' on the route " + + "'{controller}/{action}/ {p1:regex(abc} ' could not be resolved by the constraint resolver of type " + + "'IInlineConstraintResolverProxy'."; + Assert.Equal(expected, exception.InnerException.Message); } [Fact] @@ -1460,14 +1465,21 @@ namespace Microsoft.AspNetCore.Routing var routeBuilder = CreateRouteBuilder(); // Assert - ExceptionAssert.Throws( + var expectedMessage = "An error occurred while creating the route with name 'mockName' and template" + + " '{controller}/{action}'."; + + var exception = ExceptionAssert.Throws( () => routeBuilder.MapRoute("mockName", "{controller}/{action}", defaults: null, constraints: new { controller = "a.*", action = 17 }), - "The constraint entry 'action' - '17' on the route '{controller}/{action}' " + + expectedMessage); + + expectedMessage = "The constraint entry 'action' - '17' on the route '{controller}/{action}' " + "must have a string value or be of a type which implements '" + - typeof(IRouteConstraint) + "'."); + typeof(IRouteConstraint) + "'."; + Assert.NotNull(exception.InnerException); + Assert.Equal(expectedMessage, exception.InnerException.Message); } [Fact] diff --git a/test/Microsoft.AspNetCore.Routing.Tests/TemplateParserDefaultValuesTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/TemplateParserDefaultValuesTests.cs index 10c5651e84..96c126fd30 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/TemplateParserDefaultValuesTests.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/TemplateParserDefaultValuesTests.cs @@ -58,46 +58,54 @@ namespace Microsoft.AspNetCore.Routing.Tests var routeBuilder = CreateRouteBuilder(); // Act & Assert - var ex = Assert.Throws( + var ex = Assert.Throws( () => routeBuilder.MapRoute("mockName", "{controller}/{action}/{id:int=12}", defaults: new { id = 13 }, constraints: null)); - var message = "The route parameter 'id' 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."; + var message = "An error occurred while creating the route with name 'mockName' and template" + + " '{controller}/{action}/{id:int=12}'."; Assert.Equal(message, ex.Message); + + Assert.NotNull(ex.InnerException); + message = "The route parameter 'id' 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."; + Assert.Equal(message, ex.InnerException.Message); } [Fact] public void EmptyDefaultValue_WithOptionalParameter_Throws() { // Arrange - var message = "An optional parameter cannot have default value." + Environment.NewLine + - "Parameter name: routeTemplate"; var routeBuilder = CreateRouteBuilder(); // Act & Assert - var ex = Assert.Throws( + var ex = Assert.Throws( () => routeBuilder.MapRoute("mockName", "{controller}/{action}/{id:int=?}", defaults: new { id = 13 }, constraints: null)); + var message = "An error occurred while creating the route with name 'mockName' and template" + + " '{controller}/{action}/{id:int=?}'."; Assert.Equal(message, ex.Message); + + Assert.NotNull(ex.InnerException); + message = "An optional parameter cannot have default value." + Environment.NewLine + + "Parameter name: routeTemplate"; + Assert.Equal(message, ex.InnerException.Message); } [Fact] public void NonEmptyDefaultValue_WithOptionalParameter_Throws() { // Arrange - var message = "An optional parameter cannot have default value." + Environment.NewLine + - "Parameter name: routeTemplate"; var routeBuilder = CreateRouteBuilder(); // Act & Assert - var ex = Assert.Throws(() => + var ex = Assert.Throws(() => { routeBuilder.MapRoute( "mockName", @@ -106,7 +114,14 @@ namespace Microsoft.AspNetCore.Routing.Tests constraints: null); }); + var message = "An error occurred while creating the route with name 'mockName' and template" + + " '{controller}/{action}/{id:int=12?}'."; Assert.Equal(message, ex.Message); + + Assert.NotNull(ex.InnerException); + message = "An optional parameter cannot have default value." + Environment.NewLine + + "Parameter name: routeTemplate"; + Assert.Equal(message, ex.InnerException.Message); } private static IRouteBuilder CreateRouteBuilder()