diff --git a/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs b/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs index e73485b638..e766cf13b4 100644 --- a/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs +++ b/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs @@ -80,6 +80,13 @@ namespace Microsoft.AspNetCore.Routing.Internal return false; } + // NOTE: call the parameter transformer before changing the case + // A transformer might use the case, e.g. AllProducts -> all-products + if (parameterTransformer != null) + { + value = parameterTransformer.Transform(value); + } + // NOTE: this needs to be above all 'EncodeValue' and _path.Append calls if (LowercaseUrls) { @@ -117,11 +124,11 @@ namespace Microsoft.AspNetCore.Routing.Internal if (_path.Length == 0 && value.Length > 0 && value[0] == '/') { _path.Append("/"); - EncodeValue(value, 1, value.Length - 1, encodeSlashes, parameterTransformer); + EncodeValue(value, 1, value.Length - 1, encodeSlashes); } else { - EncodeValue(value, encodeSlashes, parameterTransformer); + EncodeValue(value, encodeSlashes); } return true; @@ -263,24 +270,17 @@ namespace Microsoft.AspNetCore.Routing.Internal private void EncodeValue(string value) { - EncodeValue(value, encodeSlashes: true, parameterTransformer: null); + EncodeValue(value, encodeSlashes: true); } - private void EncodeValue(string value, bool encodeSlashes, IParameterTransformer parameterTransformer) + private void EncodeValue(string value, bool encodeSlashes) { - EncodeValue(value, start: 0, characterCount: value.Length, encodeSlashes, parameterTransformer); + EncodeValue(value, start: 0, characterCount: value.Length, encodeSlashes); } // For testing - internal void EncodeValue(string value, int start, int characterCount, bool encodeSlashes, IParameterTransformer parameterTransformer) + internal void EncodeValue(string value, int start, int characterCount, bool encodeSlashes) { - 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/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs index c882633d2a..d9c9f69e19 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs @@ -169,6 +169,49 @@ namespace Microsoft.AspNetCore.Routing Assert.Equal("/Home/Index/", path); } + [Fact] + public void GetPathByAddress_WithParameterTransformer() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var routeOptions = new RouteOptions(); + routeOptions.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); + + var linkGenerator = CreateLinkGenerator(routeOptions: routeOptions, services: null, endpoint1, endpoint2); + + // Act + var path = linkGenerator.GetPathByAddress( + 1, + values: new RouteValueDictionary(new { controller = "TestController", action = "Index", })); + + // Assert + Assert.Equal("/test-controller/Index", path); + } + + [Fact] + public void GetPathByAddress_WithParameterTransformer_WithLowercaseUrl() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var routeOptions = new RouteOptions(); + routeOptions.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); + + var linkGenerator = CreateLinkGenerator(routeOptions: routeOptions, services: null, endpoint1, endpoint2); + + // Act + var path = linkGenerator.GetPathByAddress( + 1, + values: new RouteValueDictionary(new { controller = "TestController", action = "Index", }), + options: new LinkOptions() { LowercaseUrls = true, }); + + // Assert + Assert.Equal("/test-controller/index", path); + } + [Fact] public void GetPathByAddress_WithHttpContext_WithLinkOptions() { diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Internal/UriBuildingContextTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Internal/UriBuildingContextTest.cs index a23c2ab940..137520051c 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, parameterTransformer: null); + uriBuilldingContext.EncodeValue(value, 0, value.Length, encodeSlashes: true); // 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, parameterTransformer: null); + uriBuilldingContext.EncodeValue(value, 0, value.Length, encodeSlashes: false); // 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, parameterTransformer: null); + uriBuilldingContext.EncodeValue(value, startIndex, characterCount, encodeSlashes: false); // Assert Assert.Equal(expected, uriBuilldingContext.ToString()); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs index 4114322207..e77c3cf233 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; @@ -1294,15 +1295,15 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests { // Arrange var routeOptions = new RouteOptions(); - routeOptions.ConstraintMap["test-transformer"] = typeof(TestParameterTransformer); + routeOptions.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); 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 expected = "/ConventionalTransformerRoute/conventional-transformer/Param/my-value"; + var template = "ConventionalTransformerRoute/conventional-transformer/Param/{param:length(500):slugify?}"; + var defaults = new RouteValueDictionary(new { controller = "ConventionalTransformer", action = "Param" }); + var ambientValues = new RouteValueDictionary(new { controller = "ConventionalTransformer", action = "Param" }); + var explicitValues = new RouteValueDictionary(new { controller = "ConventionalTransformer", action = "Param", param = "MyValue" }); var binder = new TemplateBinder( UrlEncoder.Default, new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), @@ -1319,14 +1320,6 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests 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/TestObjects/SlugifyParameterTransformer.cs b/test/Microsoft.AspNetCore.Routing.Tests/TestObjects/SlugifyParameterTransformer.cs new file mode 100644 index 0000000000..acf07d8cc8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/TestObjects/SlugifyParameterTransformer.cs @@ -0,0 +1,17 @@ +// 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; +using System.Text.RegularExpressions; + +namespace Microsoft.AspNetCore.Routing.TestObjects +{ + public class SlugifyParameterTransformer : IParameterTransformer + { + public string Transform(string value) + { + // Slugify value + return Regex.Replace(value, "([a-z])([A-Z])", "$1-$2").ToLower(); + } + } +}