diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/IParameterTransformer.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/IParameterTransformer.cs new file mode 100644 index 0000000000..4f49892003 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/IParameterTransformer.cs @@ -0,0 +1,18 @@ +// 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. + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Defines the contract that a class must implement to transform parameter values. + /// + public interface IParameterTransformer : IParameterPolicy + { + /// + /// Transforms the specified parameter value. + /// + /// The parameter value to transform. + /// The transformed value. + string Transform(string value); + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Constraints/NullRouteConstraint.cs b/src/Microsoft.AspNetCore.Routing/Constraints/NullRouteConstraint.cs new file mode 100644 index 0000000000..f61e740419 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Constraints/NullRouteConstraint.cs @@ -0,0 +1,21 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Constraints +{ + internal class NullRouteConstraint : IRouteConstraint + { + public static readonly NullRouteConstraint Instance = new NullRouteConstraint(); + + private NullRouteConstraint() + { + } + + public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) + { + return true; + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Constraints/StringRouteConstraint.cs b/src/Microsoft.AspNetCore.Routing/Constraints/StringRouteConstraint.cs index 202fcbb02c..20103058a7 100644 --- a/src/Microsoft.AspNetCore.Routing/Constraints/StringRouteConstraint.cs +++ b/src/Microsoft.AspNetCore.Routing/Constraints/StringRouteConstraint.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.Constraints { /// - /// Constrains a route parameter to contain only a specified strign. + /// Constrains a route parameter to contain only a specified string. /// public class StringRouteConstraint : IRouteConstraint { diff --git a/src/Microsoft.AspNetCore.Routing/DefaultInlineConstraintResolver.cs b/src/Microsoft.AspNetCore.Routing/DefaultInlineConstraintResolver.cs index 1ed1f8e1ab..02fd133e28 100644 --- a/src/Microsoft.AspNetCore.Routing/DefaultInlineConstraintResolver.cs +++ b/src/Microsoft.AspNetCore.Routing/DefaultInlineConstraintResolver.cs @@ -56,7 +56,12 @@ namespace Microsoft.AspNetCore.Routing throw new ArgumentNullException(nameof(inlineConstraint)); } - return ParameterPolicyActivator.ResolveParameterPolicy(_inlineConstraintMap, _serviceProvider, inlineConstraint, out _); + // This will return null if the text resolves to a non-IRouteConstraint + return ParameterPolicyActivator.ResolveParameterPolicy( + _inlineConstraintMap, + _serviceProvider, + inlineConstraint, + out _); } } } diff --git a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs index f050eecb97..dc226c2b9c 100644 --- a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs +++ b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs @@ -271,7 +271,8 @@ namespace Microsoft.AspNetCore.Routing UrlEncoder.Default, _uriBuildingContextPool, endpoint.RoutePattern, - new RouteValueDictionary(endpoint.RoutePattern.Defaults)); + new RouteValueDictionary(endpoint.RoutePattern.Defaults), + _parameterPolicyFactory); var routeValuesAddressMetadata = endpoint.Metadata.GetMetadata(); var templateValuesResult = templateBinder.GetValues( diff --git a/src/Microsoft.AspNetCore.Routing/DefaultParameterPolicyFactory.cs b/src/Microsoft.AspNetCore.Routing/DefaultParameterPolicyFactory.cs index d14bd9f113..d1837c91c2 100644 --- a/src/Microsoft.AspNetCore.Routing/DefaultParameterPolicyFactory.cs +++ b/src/Microsoft.AspNetCore.Routing/DefaultParameterPolicyFactory.cs @@ -44,7 +44,12 @@ namespace Microsoft.AspNetCore.Routing throw new ArgumentNullException(nameof(inlineText)); } - var parameterPolicy = ParameterPolicyActivator.ResolveParameterPolicy(_options.ConstraintMap, _serviceProvider, inlineText, out var parameterPolicyKey); + var parameterPolicy = ParameterPolicyActivator.ResolveParameterPolicy( + _options.ConstraintMap, + _serviceProvider, + inlineText, + out var parameterPolicyKey); + if (parameterPolicy == null) { throw new InvalidOperationException(Resources.FormatRoutePattern_ConstraintReferenceNotFound( diff --git a/src/Microsoft.AspNetCore.Routing/Internal/ParameterPolicyActivator.cs b/src/Microsoft.AspNetCore.Routing/Internal/ParameterPolicyActivator.cs index 1fc3f1a365..c04de5ba60 100644 --- a/src/Microsoft.AspNetCore.Routing/Internal/ParameterPolicyActivator.cs +++ b/src/Microsoft.AspNetCore.Routing/Internal/ParameterPolicyActivator.cs @@ -13,7 +13,11 @@ namespace Microsoft.AspNetCore.Routing.Internal { internal static class ParameterPolicyActivator { - public static T ResolveParameterPolicy(IDictionary inlineParameterPolicyMap, IServiceProvider serviceProvider, string inlineParameterPolicy, out string parameterPolicyKey) + public static T ResolveParameterPolicy( + IDictionary inlineParameterPolicyMap, + IServiceProvider serviceProvider, + string inlineParameterPolicy, + out string parameterPolicyKey) where T : IParameterPolicy { // IServiceProvider could be null @@ -51,9 +55,18 @@ namespace Microsoft.AspNetCore.Routing.Internal if (!typeof(T).IsAssignableFrom(parameterPolicyType)) { - throw new RouteCreationException( - Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint( - parameterPolicyType, parameterPolicyKey, typeof(T).Name)); + if (!typeof(IParameterPolicy).IsAssignableFrom(parameterPolicyType)) + { + // Error if type is not a parameter policy + throw new RouteCreationException( + Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint( + parameterPolicyType, parameterPolicyKey, typeof(T).Name)); + } + + // Return null if type is parameter policy but is not the exact type + // This is used by IInlineConstraintResolver for backwards compatibility + // e.g. looking for an IRouteConstraint but get a different IParameterPolicy type + return default; } try diff --git a/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs b/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs index 2b5c506af6..e73485b638 100644 --- a/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs +++ b/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs @@ -56,10 +56,10 @@ namespace Microsoft.AspNetCore.Routing.Internal public bool Accept(string value) { - return Accept(value, encodeSlashes: true); + return Accept(value, encodeSlashes: true, parameterTransformer: null); } - public bool Accept(string value, bool encodeSlashes) + public bool Accept(string value, bool encodeSlashes, IParameterTransformer parameterTransformer) { if (string.IsNullOrEmpty(value)) { @@ -117,11 +117,11 @@ namespace Microsoft.AspNetCore.Routing.Internal if (_path.Length == 0 && value.Length > 0 && value[0] == '/') { _path.Append("/"); - EncodeValue(value, 1, value.Length - 1, encodeSlashes); + EncodeValue(value, 1, value.Length - 1, encodeSlashes, parameterTransformer); } else { - EncodeValue(value, encodeSlashes); + EncodeValue(value, encodeSlashes, parameterTransformer); } return true; @@ -263,17 +263,24 @@ namespace Microsoft.AspNetCore.Routing.Internal private void EncodeValue(string value) { - EncodeValue(value, encodeSlashes: true); + EncodeValue(value, encodeSlashes: true, parameterTransformer: null); } - private void EncodeValue(string value, bool encodeSlashes) + private void EncodeValue(string value, bool encodeSlashes, IParameterTransformer parameterTransformer) { - EncodeValue(value, start: 0, characterCount: value.Length, encodeSlashes); + EncodeValue(value, start: 0, characterCount: value.Length, encodeSlashes, parameterTransformer); } // For testing - internal void EncodeValue(string value, int start, int characterCount, bool encodeSlashes) + internal void EncodeValue(string value, int start, int characterCount, bool encodeSlashes, IParameterTransformer parameterTransformer) { + if (parameterTransformer != null) + { + value = parameterTransformer.Transform(value.Substring(0, characterCount)); + start = 0; + characterCount = value.Length; + } + // Just encode everything if its ok to encode slashes if (encodeSlashes) { diff --git a/src/Microsoft.AspNetCore.Routing/MapRouteRouteBuilderExtensions.cs b/src/Microsoft.AspNetCore.Routing/MapRouteRouteBuilderExtensions.cs index ef55007af3..e5cc19aa1d 100644 --- a/src/Microsoft.AspNetCore.Routing/MapRouteRouteBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Routing/MapRouteRouteBuilderExtensions.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Builder @@ -107,10 +108,6 @@ namespace Microsoft.AspNetCore.Builder throw new RouteCreationException(Resources.FormatDefaultHandler_MustBeSet(nameof(IRouteBuilder))); } - var inlineConstraintResolver = routeBuilder - .ServiceProvider - .GetRequiredService(); - routeBuilder.Routes.Add(new Route( routeBuilder.DefaultHandler, name, @@ -118,9 +115,52 @@ namespace Microsoft.AspNetCore.Builder new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), new RouteValueDictionary(dataTokens), - inlineConstraintResolver)); + CreateInlineConstraintResolver(routeBuilder.ServiceProvider))); return routeBuilder; } + + private static IInlineConstraintResolver CreateInlineConstraintResolver(IServiceProvider serviceProvider) + { + var inlineConstraintResolver = serviceProvider + .GetRequiredService(); + + var parameterPolicyFactory = serviceProvider + .GetRequiredService(); + + // This inline constraint resolver will return a null constraint for non-IRouteConstraint + // parameter policies so Route does not error + return new BackCompatInlineConstraintResolver(inlineConstraintResolver, parameterPolicyFactory); + } + + private class BackCompatInlineConstraintResolver : IInlineConstraintResolver + { + private readonly IInlineConstraintResolver _inner; + private readonly ParameterPolicyFactory _parameterPolicyFactory; + + public BackCompatInlineConstraintResolver(IInlineConstraintResolver inner, ParameterPolicyFactory parameterPolicyFactory) + { + _inner = inner; + _parameterPolicyFactory = parameterPolicyFactory; + } + + public IRouteConstraint ResolveConstraint(string inlineConstraint) + { + var routeConstraint = _inner.ResolveConstraint(inlineConstraint); + if (routeConstraint != null) + { + return routeConstraint; + } + + var parameterPolicy = _parameterPolicyFactory.Create(null, inlineConstraint); + if (parameterPolicy != null) + { + // Logic inside Route will skip adding NullRouteConstraint + return NullRouteConstraint.Instance; + } + + return null; + } + } } } diff --git a/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs b/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs index c230ed0e96..f170ac81fa 100644 --- a/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs +++ b/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.AspNetCore.Routing.Internal; namespace Microsoft.AspNetCore.Routing { @@ -158,6 +159,11 @@ namespace Microsoft.AspNetCore.Routing _displayName, _inlineConstraintResolver.GetType().Name)); } + else if (constraint == NullRouteConstraint.Instance) + { + // A null route constraint can be returned for other parameter policy types + return; + } Add(key, constraint); } diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs index 7579e27232..d5a289edc0 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs @@ -20,6 +20,7 @@ namespace Microsoft.AspNetCore.Routing.Template private readonly ObjectPool _pool; private readonly RouteValueDictionary _defaults; + private readonly ParameterPolicyFactory _parameterPolicyFactory; private readonly RouteValueDictionary _filters; private readonly RoutePattern _pattern; @@ -35,7 +36,7 @@ namespace Microsoft.AspNetCore.Routing.Template ObjectPool pool, RouteTemplate template, RouteValueDictionary defaults) - : this(urlEncoder, pool, template?.ToRoutePattern(), defaults) + : this(urlEncoder, pool, template?.ToRoutePattern(), defaults, parameterPolicyFactory: null) { } @@ -46,11 +47,13 @@ namespace Microsoft.AspNetCore.Routing.Template /// The . /// The to bind values to. /// The default values for . + /// The . public TemplateBinder( UrlEncoder urlEncoder, ObjectPool pool, RoutePattern pattern, - RouteValueDictionary defaults) + RouteValueDictionary defaults, + ParameterPolicyFactory parameterPolicyFactory) { if (urlEncoder == null) { @@ -71,6 +74,7 @@ namespace Microsoft.AspNetCore.Routing.Template _pool = pool; _pattern = pattern; _defaults = defaults; + _parameterPolicyFactory = parameterPolicyFactory; // Any default that doesn't have a corresponding parameter is a 'filter' and if a value // is provided for that 'filter' it must match the value in defaults. @@ -265,7 +269,7 @@ namespace Microsoft.AspNetCore.Routing.Template // Example: template = {id}.{format?}. parameters: id=5 // In this case after we have generated "5.", we wont find any value // for format, so we remove '.' and generate 5. - if (!context.Accept(converted, parameterPart.EncodeSlashes)) + if (!context.Accept(converted, parameterPart.EncodeSlashes, GetParameterTransformer(parameterPart))) { if (j != 0 && parameterPart.IsOptional && (separatorPart = segment.Parts[j - 1] as RoutePatternSeparatorPart) != null) { @@ -310,6 +314,26 @@ namespace Microsoft.AspNetCore.Routing.Template return true; } + private IParameterTransformer GetParameterTransformer(RoutePatternParameterPart parameterPart) + { + if (_parameterPolicyFactory == null) + { + return null; + } + + for (var i = 0; i < parameterPart.ParameterPolicies.Count; i++) + { + // Use the first parameter transformer + var parameterPolicy = _parameterPolicyFactory.Create(parameterPart, parameterPart.ParameterPolicies[i]); + if (parameterPolicy is IParameterTransformer parameterTransformer) + { + return parameterTransformer; + } + } + + return null; + } + private bool AddQueryKeyValueToContext(UriBuildingContext context, string key, object value, bool wroteFirst) { var converted = Convert.ToString(value, CultureInfo.InvariantCulture); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs index 75b93f157e..6acd5b8656 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs @@ -253,6 +253,35 @@ namespace Microsoft.AspNetCore.Routing Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", path); } + private class UpperCaseParameterTransform : IParameterTransformer + { + public string Transform(string value) + { + return value?.ToUpperInvariant(); + } + } + + [Fact] + public void GetLink_ParameterTransformer() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller:upper-case}/{name}"); + + var routeOptions = new RouteOptions(); + routeOptions.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); + + var services = GetBasicServices(); + services.AddSingleton(typeof(UpperCaseParameterTransform), new UpperCaseParameterTransform()); + + var linkGenerator = CreateLinkGenerator(routeOptions, services, endpoint); + + // Act + var link = linkGenerator.GetPathByRouteValues(routeName: null, new { controller = "Home", name = "Test" }); + + // Assert + Assert.Equal("/HOME/Test", link); + } + // Includes characters that need to be encoded [Fact] public void GetPathByAddress_WithHttpContext_WithPathBaseAndFragment() diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Internal/UriBuildingContextTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Internal/UriBuildingContextTest.cs index 137520051c..a23c2ab940 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Internal/UriBuildingContextTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Internal/UriBuildingContextTest.cs @@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Routing.Internal var uriBuilldingContext = new UriBuildingContext(urlTestEncoder); // Act - uriBuilldingContext.EncodeValue(value, 0, value.Length, encodeSlashes: true); + uriBuilldingContext.EncodeValue(value, 0, value.Length, encodeSlashes: true, parameterTransformer: null); // Assert Assert.Equal(expected, uriBuilldingContext.ToString()); @@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Routing.Internal var uriBuilldingContext = new UriBuildingContext(urlTestEncoder); // Act - uriBuilldingContext.EncodeValue(value, 0, value.Length, encodeSlashes: false); + uriBuilldingContext.EncodeValue(value, 0, value.Length, encodeSlashes: false, parameterTransformer: null); // Assert Assert.Equal(expected, uriBuilldingContext.ToString()); @@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Routing.Internal var uriBuilldingContext = new UriBuildingContext(urlTestEncoder); // Act - uriBuilldingContext.EncodeValue(value, startIndex, characterCount, encodeSlashes: false); + uriBuilldingContext.EncodeValue(value, startIndex, characterCount, encodeSlashes: false, parameterTransformer: null); // Assert Assert.Equal(expected, uriBuilldingContext.ToString()); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorTestBase.cs b/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorTestBase.cs index 4864467409..09bca2f3cc 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorTestBase.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorTestBase.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Routing return httpContext; } - private ServiceCollection GetBasicServices() + protected ServiceCollection GetBasicServices() { var services = new ServiceCollection(); services.AddSingleton(); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/RouteTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/RouteTest.cs index 81e20b041f..668a54ee8a 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/RouteTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/RouteTest.cs @@ -1582,7 +1582,6 @@ namespace Microsoft.AspNetCore.Routing // Assert var templateRoute = (Route)routeBuilder.Routes[0]; - // Assert Assert.Equal(expectedDictionary.Count, templateRoute.DataTokens.Count); foreach (var expectedKey in expectedDictionary.Keys) { @@ -1591,6 +1590,22 @@ namespace Microsoft.AspNetCore.Routing } } + [Fact] + public void RegisteringRoute_WithParameterPolicy_AbleToAddTheRoute() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); + + // Act + routeBuilder.MapRoute("mockName", + "{controller:test-policy}/{action}"); + + // Assert + var templateRoute = (Route)routeBuilder.Routes[0]; + + Assert.Empty(templateRoute.Constraints); + } + [Fact] public void RegisteringRouteWithInvalidConstraints_Throws() { @@ -1752,6 +1767,8 @@ namespace Microsoft.AspNetCore.Routing var services = new ServiceCollection(); services.AddSingleton(_inlineConstraintResolver); services.AddSingleton(); + services.AddSingleton(); + services.Configure(ConfigureRouteOptions); var applicationBuilder = Mock.Of(); applicationBuilder.ApplicationServices = services.BuildServiceProvider(); @@ -1837,12 +1854,24 @@ namespace Microsoft.AspNetCore.Routing private static IInlineConstraintResolver GetInlineConstraintResolver() { - var routeOptions = new Mock>(); - routeOptions - .SetupGet(o => o.Value) - .Returns(new RouteOptions()); + var routeOptions = new RouteOptions(); + ConfigureRouteOptions(routeOptions); - return new DefaultInlineConstraintResolver(routeOptions.Object, new TestServiceProvider()); + var routeOptionsMock = new Mock>(); + routeOptionsMock + .SetupGet(o => o.Value) + .Returns(routeOptions); + + return new DefaultInlineConstraintResolver(routeOptionsMock.Object, new TestServiceProvider()); + } + + private static void ConfigureRouteOptions(RouteOptions options) + { + options.ConstraintMap["test-policy"] = typeof(TestPolicy); + } + + private class TestPolicy : IParameterPolicy + { } } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs index c3e4ebcb8c..9186b31e39 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; @@ -1288,6 +1289,43 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests Assert.Equal(expected, boundTemplate); } + [Fact] + public void BindValues_ParameterTransformer() + { + // Arrange + var routeOptions = new RouteOptions(); + routeOptions.ConstraintMap["test-transformer"] = typeof(TestParameterTransformer); + var parameterPolicyFactory = new DefaultParameterPolicyFactory( + Options.Create(routeOptions), + new ServiceCollection().BuildServiceProvider()); + var expected = "/ConventionalTansformerRoute/_ConventionalTansformer_/Param/_value_"; + var template = "ConventionalTansformerRoute/_ConventionalTansformer_/Param/{param:length(500):test-transformer?}"; + var defaults = new RouteValueDictionary(new { controller = "ConventionalTansformer", action = "Param" }); + var ambientValues = new RouteValueDictionary(new { controller = "ConventionalTansformer", action = "Param" }); + var explicitValues = new RouteValueDictionary(new { controller = "ConventionalTansformer", action = "Param", param = "value" }); + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + RoutePatternFactory.Parse(template), + defaults, + parameterPolicyFactory); + + // Act + var result = binder.GetValues(ambientValues, explicitValues); + var boundTemplate = binder.BindValues(result.AcceptedValues); + + // Assert + Assert.Equal(expected, boundTemplate); + } + + private class TestParameterTransformer : IParameterTransformer + { + public string Transform(string value) + { + return "_" + value + "_"; + } + } + private static IInlineConstraintResolver GetInlineConstraintResolver() { var services = new ServiceCollection().AddOptions(); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/TemplateParserDefaultValuesTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/TemplateParserDefaultValuesTests.cs index a8c09c60ff..ac3d032491 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/TemplateParserDefaultValuesTests.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/TemplateParserDefaultValuesTests.cs @@ -132,6 +132,8 @@ namespace Microsoft.AspNetCore.Routing.Tests var services = new ServiceCollection(); services.AddSingleton(_inlineConstraintResolver); services.AddSingleton(); + services.AddSingleton(); + services.Configure(options => { }); var applicationBuilder = Mock.Of(); applicationBuilder.ApplicationServices = services.BuildServiceProvider();