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();