diff --git a/samples/MvcSample.Web/Views/ApiExplorer/_ApiDescription.cshtml b/samples/MvcSample.Web/Views/ApiExplorer/_ApiDescription.cshtml index f3e73a79b2..3441b3561c 100644 --- a/samples/MvcSample.Web/Views/ApiExplorer/_ApiDescription.cshtml +++ b/samples/MvcSample.Web/Views/ApiExplorer/_ApiDescription.cshtml @@ -15,7 +15,11 @@ {
  • @parameter.Name - @(parameter?.Type?.FullName ?? "Unknown") - @parameter.Source.ToString() - - Constraint: @(parameter?.Constraint?.GetType()?.Name?.Replace("RouteConstraint", "") ?? " none") + @if (parameter.Constraints != null && parameter.Constraints.Any()) + { + var constraints = parameter.Constraints; + Write("-Constraint:" + string.Join(", ", constraints.Select(c => c.GetType()?.Name?.Replace("RouteConstraint", "")))); + } - Default value: @(parameter?.DefaultValue ?? " none")
  • } diff --git a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs b/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs index 80a85b576a..6a037efe0b 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs @@ -2,6 +2,7 @@ // 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.Mvc.ModelBinding; using Microsoft.AspNet.Routing; @@ -19,7 +20,7 @@ namespace Microsoft.AspNet.Mvc.Description public ApiParameterSource Source { get; set; } - public IRouteConstraint Constraint { get; set; } + public IEnumerable Constraints { get; set; } public object DefaultValue { get; set; } diff --git a/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs index febdaf2e29..28e176e850 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs @@ -180,7 +180,7 @@ namespace Microsoft.AspNet.Mvc.Description if (action.AttributeRouteInfo != null && action.AttributeRouteInfo.Template != null) { - return TemplateParser.Parse(action.AttributeRouteInfo.Template, _constraintResolver); + return TemplateParser.Parse(action.AttributeRouteInfo.Template); } return null; @@ -281,7 +281,7 @@ namespace Microsoft.AspNet.Mvc.Description return resourceParameter; } - private static ApiParameterDescription CreateParameterFromTemplateAndParameterDescriptor( + private ApiParameterDescription CreateParameterFromTemplateAndParameterDescriptor( TemplatePart templateParameter, ParameterDescriptor parameter) { @@ -291,7 +291,7 @@ namespace Microsoft.AspNet.Mvc.Description IsOptional = parameter.IsOptional && IsOptionalParameter(templateParameter), Name = parameter.Name, ParameterDescriptor = parameter, - Constraint = templateParameter.InlineConstraint, + Constraints = GetConstraints(_constraintResolver, templateParameter.InlineConstraints), DefaultValue = templateParameter.DefaultValue, Type = parameter.ParameterType, }; @@ -299,12 +299,23 @@ namespace Microsoft.AspNet.Mvc.Description return resourceParameter; } + private static IEnumerable GetConstraints( + IInlineConstraintResolver constraintResolver, + IEnumerable constraints) + { + return + constraints + .Select(c => constraintResolver.ResolveConstraint(c.Constraint)) + .Where(c => c != null) + .ToArray(); + } + private static bool IsOptionalParameter(TemplatePart templateParameter) { return templateParameter.IsOptional || templateParameter.DefaultValue != null; } - private static ApiParameterDescription CreateParameterFromTemplate(TemplatePart templateParameter) + private ApiParameterDescription CreateParameterFromTemplate(TemplatePart templateParameter) { return new ApiParameterDescription { @@ -312,7 +323,7 @@ namespace Microsoft.AspNet.Mvc.Description IsOptional = IsOptionalParameter(templateParameter), Name = templateParameter.Name, ParameterDescriptor = null, - Constraint = templateParameter.InlineConstraint, + Constraints = GetConstraints(_constraintResolver, templateParameter.InlineConstraints), DefaultValue = templateParameter.DefaultValue, }; } diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs index 8510f8e310..4c3b5a9626 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs @@ -21,12 +21,12 @@ namespace Microsoft.AspNet.Mvc.Routing /// /// The route constraints. /// - public IDictionary Constraints { get; set; } + public IReadOnlyDictionary Constraints { get; set; } /// /// The route defaults. /// - public IDictionary Defaults { get; set; } + public IReadOnlyDictionary Defaults { get; set; } /// /// The order of the template. diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoutePrecedence.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoutePrecedence.cs index 3f4c6dff60..1f7b7e49d5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoutePrecedence.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoutePrecedence.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.Diagnostics.Contracts; +using System.Linq; using Microsoft.AspNet.Routing.Template; namespace Microsoft.AspNet.Mvc.Routing @@ -59,7 +60,7 @@ namespace Microsoft.AspNet.Mvc.Routing // If there is a route constraint for the parameter, reduce order by 1 // Constrained parameters end up with order 2, Constrained catch alls end up with order 4 - if (part.InlineConstraint != null) + if (part.InlineConstraints != null && part.InlineConstraints.Any()) { digit--; } diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs index 0d416fbeea..c00a5106a9 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs @@ -184,7 +184,7 @@ namespace Microsoft.AspNet.Mvc.Routing if (!templateCache.TryGetValue(action.AttributeRouteInfo.Template, out parsedTemplate)) { // Parsing with throw if the template is invalid. - parsedTemplate = TemplateParser.Parse(action.AttributeRouteInfo.Template, constraintResolver); + parsedTemplate = TemplateParser.Parse(action.AttributeRouteInfo.Template); templateCache.Add(action.AttributeRouteInfo.Template, parsedTemplate); } @@ -218,9 +218,20 @@ namespace Microsoft.AspNet.Mvc.Routing routeInfo.Name = action.AttributeRouteInfo.Name; - routeInfo.Constraints = routeInfo.ParsedTemplate.Parameters - .Where(p => p.InlineConstraint != null) - .ToDictionary(p => p.Name, p => p.InlineConstraint, StringComparer.OrdinalIgnoreCase); + var constraintBuilder = new RouteConstraintBuilder(constraintResolver, routeInfo.RouteTemplate); + + foreach (var parameter in routeInfo.ParsedTemplate.Parameters) + { + if (parameter.InlineConstraints != null) + { + foreach (var inlineConstraint in parameter.InlineConstraints) + { + constraintBuilder.AddResolvedConstraint(parameter.Name, inlineConstraint.Constraint); + } + } + } + + routeInfo.Constraints = constraintBuilder.Build(); routeInfo.Defaults = routeInfo.ParsedTemplate.Parameters .Where(p => p.DefaultValue != null) @@ -233,9 +244,9 @@ namespace Microsoft.AspNet.Mvc.Routing { public ActionDescriptor ActionDescriptor { get; set; } - public IDictionary Constraints { get; set; } + public IReadOnlyDictionary Constraints { get; set; } - public IDictionary Defaults { get; set; } + public IReadOnlyDictionary Defaults { get; set; } public string ErrorMessage { get; set; } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs index 88fd3c30bd..359add36fd 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs @@ -188,7 +188,7 @@ namespace Microsoft.AspNet.Mvc.Description if (constraintType != null) { - Assert.IsType(constraintType, parameter.Constraint); + Assert.IsType(constraintType, Assert.Single(parameter.Constraints)); } if (defaultValue != null) @@ -243,7 +243,7 @@ namespace Microsoft.AspNet.Mvc.Description if (constraintType != null) { - Assert.IsType(constraintType, parameter.Constraint); + Assert.IsType(constraintType, Assert.Single(parameter.Constraints)); } if (defaultValue != null) @@ -303,7 +303,7 @@ namespace Microsoft.AspNet.Mvc.Description if (constraintType != null) { - Assert.IsType(constraintType, pathParameter.Constraint); + Assert.IsType(constraintType, Assert.Single(pathParameter.Constraints)); } if (defaultValue != null) @@ -388,11 +388,11 @@ namespace Microsoft.AspNet.Mvc.Description var description = Assert.Single(descriptions); var id1 = Assert.Single(description.ParameterDescriptions, p => p.Name == "id1"); Assert.Equal(ApiParameterSource.Path, id1.Source); - Assert.Null(id1.Constraint); + Assert.Empty(id1.Constraints); var id2 = Assert.Single(description.ParameterDescriptions, p => p.Name == "id2"); Assert.Equal(ApiParameterSource.Path, id2.Source); - Assert.IsType(id2.Constraint); + Assert.IsType(Assert.Single(id2.Constraints)); } [Fact] diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutePrecedenceTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutePrecedenceTests.cs index 8f8df78634..21064e5d6e 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutePrecedenceTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutePrecedenceTests.cs @@ -63,11 +63,7 @@ namespace Microsoft.AspNet.Mvc.Routing var options = new Mock>(); options.SetupGet(o => o.Options).Returns(new RouteOptions()); - var constraintResolver = new DefaultInlineConstraintResolver( - Mock.Of(), - options.Object); - - var parsed = TemplateParser.Parse(template, constraintResolver); + var parsed = TemplateParser.Parse(template); return AttributeRoutePrecedence.Compute(parsed); } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTests.cs index f9935549e0..add6b624a6 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTests.cs @@ -1256,18 +1256,21 @@ namespace Microsoft.AspNet.Mvc.Routing private static AttributeRouteMatchingEntry CreateMatchingEntry(IRouter router, string template, int order) { - var constraintResolver = CreateConstraintResolver(); - - var routeTemplate = TemplateParser.Parse(template, constraintResolver); + var routeGroup = string.Format("{0}&&{1}", order, template); var entry = new AttributeRouteMatchingEntry(); - entry.Route = new TemplateRoute(router, template, constraintResolver); + entry.Route = new TemplateRoute( + target: router, + routeTemplate: template, + defaults: new RouteValueDictionary(new { test_route_group = routeGroup }), + constraints: null, + dataTokens: null, + inlineConstraintResolver: CreateConstraintResolver()); + + var routeTemplate = TemplateParser.Parse(template); entry.Precedence = AttributeRoutePrecedence.Compute(routeTemplate); entry.Order = order; - string routeGroup = string.Format("{0}&&{1}", order, template); - entry.Route.Defaults.Add("test_route_group", routeGroup); - return entry; } @@ -1281,15 +1284,25 @@ namespace Microsoft.AspNet.Mvc.Routing var entry = new AttributeRouteLinkGenerationEntry(); entry.TemplateText = template; - entry.Template = TemplateParser.Parse(template, constraintResolver); + entry.Template = TemplateParser.Parse(template); var defaults = entry.Template.Parameters .Where(p => p.DefaultValue != null) .ToDictionary(p => p.Name, p => p.DefaultValue); - var constraints = entry.Template.Parameters - .Where(p => p.InlineConstraint != null) - .ToDictionary(p => p.Name, p => p.InlineConstraint); + var constraintBuilder = new RouteConstraintBuilder(CreateConstraintResolver(), template); + foreach (var parameter in entry.Template.Parameters) + { + if (parameter.InlineConstraints != null) + { + foreach (var constraint in parameter.InlineConstraints) + { + constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); + } + } + } + + var constraints = constraintBuilder.Build(); entry.Constraints = constraints; entry.Defaults = defaults; diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs index 0c26d7d42a..a75f62c00a 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs @@ -178,7 +178,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal("id", parameter.Name); Assert.False(parameter.IsOptional); Assert.Equal("Path", parameter.Source); - Assert.Null(parameter.ConstraintType); + Assert.Empty(parameter.ConstraintTypes); } [Fact] @@ -203,7 +203,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal("integer", parameter.Name); Assert.False(parameter.IsOptional); Assert.Equal("Path", parameter.Source); - Assert.Equal("IntRouteConstraint", parameter.ConstraintType); + Assert.Equal("IntRouteConstraint", Assert.Single(parameter.ConstraintTypes)); } [Fact] @@ -254,7 +254,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal("integer", parameter.Name); Assert.False(parameter.IsOptional); Assert.Equal("Path", parameter.Source); - Assert.Equal("IntRouteConstraint", parameter.ConstraintType); + Assert.Equal("IntRouteConstraint", Assert.Single(parameter.ConstraintTypes)); } [Fact] @@ -283,17 +283,17 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var month = Assert.Single(description.ParameterDescriptions, p => p.Name == "month"); Assert.False(month.IsOptional); Assert.Equal("Path", month.Source); - Assert.Equal("RangeRouteConstraint", month.ConstraintType); + Assert.Equal("RangeRouteConstraint", Assert.Single(month.ConstraintTypes)); var day = Assert.Single(description.ParameterDescriptions, p => p.Name == "day"); Assert.False(day.IsOptional); Assert.Equal("Path", day.Source); - Assert.Equal("IntRouteConstraint", day.ConstraintType); + Assert.Equal("IntRouteConstraint", Assert.Single(day.ConstraintTypes)); var year = Assert.Single(description.ParameterDescriptions, p => p.Name == "year"); Assert.False(year.IsOptional); Assert.Equal("Path", year.Source); - Assert.Equal("IntRouteConstraint", year.ConstraintType); + Assert.Equal("IntRouteConstraint", Assert.Single(year.ConstraintTypes)); } [Fact] @@ -321,17 +321,17 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var month = Assert.Single(description.ParameterDescriptions, p => p.Name == "month"); Assert.False(month.IsOptional); Assert.Equal("Path", month.Source); - Assert.Equal("RangeRouteConstraint", month.ConstraintType); + Assert.Equal("RangeRouteConstraint", Assert.Single(month.ConstraintTypes)); var day = Assert.Single(description.ParameterDescriptions, p => p.Name == "day"); Assert.False(day.IsOptional); Assert.Equal("Path", day.Source); - Assert.Equal("IntRouteConstraint", day.ConstraintType); + Assert.Equal("IntRouteConstraint", Assert.Single(day.ConstraintTypes)); var year = Assert.Single(description.ParameterDescriptions, p => p.Name == "year"); Assert.True(year.IsOptional); Assert.Equal("Path", year.Source); - Assert.Equal("IntRouteConstraint", year.ConstraintType); + Assert.Equal("IntRouteConstraint", Assert.Single(year.ConstraintTypes)); } [Fact] @@ -749,7 +749,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests public string Type { get; set; } - public string ConstraintType { get; set; } + public string[] ConstraintTypes { get; set; } } // Used to serialize data between client and server diff --git a/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs b/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs index 8c23c32e1e..e672b4e9aa 100644 --- a/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs +++ b/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Linq; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc.Description; @@ -56,7 +57,7 @@ namespace ApiExplorer Name = parameter.Name, Source = parameter.Source.ToString(), Type = parameter?.Type?.FullName, - ConstraintType = parameter?.Constraint?.GetType()?.Name, + ConstraintTypes = parameter?.Constraints?.Select(c => c.GetType().Name).ToArray(), }; data.ParameterDescriptions.Add(parameterData); @@ -103,7 +104,7 @@ namespace ApiExplorer public string Type { get; set; } - public string ConstraintType { get; set; } + public string[] ConstraintTypes { get; set; } } // Used to serialize data between client and server