diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/IParameterTransformer.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/IOutboundParameterTransformer.cs similarity index 56% rename from src/Microsoft.AspNetCore.Routing.Abstractions/IParameterTransformer.cs rename to src/Microsoft.AspNetCore.Routing.Abstractions/IOutboundParameterTransformer.cs index 4f49892003..bcc9e21c4d 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/IParameterTransformer.cs +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/IOutboundParameterTransformer.cs @@ -4,15 +4,16 @@ namespace Microsoft.AspNetCore.Routing { /// - /// Defines the contract that a class must implement to transform parameter values. + /// Defines the contract that a class must implement to transform route values while building + /// a URI. /// - public interface IParameterTransformer : IParameterPolicy + public interface IOutboundParameterTransformer : IParameterPolicy { /// - /// Transforms the specified parameter value. + /// Transforms the specified route value to a string for inclusion in a URI. /// - /// The parameter value to transform. + /// The route value to transform. /// The transformed value. - string Transform(string value); + string TransformOutbound(object value); } } diff --git a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs index e251fdff59..c303c5dcb2 100644 --- a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs +++ b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs @@ -280,13 +280,40 @@ namespace Microsoft.AspNetCore.Routing private TemplateBinder CreateTemplateBinder(RouteEndpoint endpoint) { + // Now create the constraints and parameter transformers from the pattern + var policies = new List<(string parameterName, IParameterPolicy policy)>(); + foreach (var kvp in endpoint.RoutePattern.ParameterPolicies) + { + var parameterName = kvp.Key; + + // It's possible that we don't have an actual route parameter, we need to support that case. + var parameter = endpoint.RoutePattern.GetParameter(parameterName); + + // Use the first parameter transformer per parameter + var foundTransformer = false; + for (var i = 0; i < kvp.Value.Count; i++) + { + var parameterPolicy = _parameterPolicyFactory.Create(parameter, kvp.Value[i]); + if (!foundTransformer && parameterPolicy is IOutboundParameterTransformer parameterTransformer) + { + policies.Add((parameterName, parameterTransformer)); + foundTransformer = true; + } + + if (parameterPolicy is IRouteConstraint constraint) + { + policies.Add((parameterName, constraint)); + } + } + } + return new TemplateBinder( UrlEncoder.Default, _uriBuildingContextPool, endpoint.RoutePattern, new RouteValueDictionary(endpoint.RoutePattern.Defaults), endpoint.Metadata.GetMetadata()?.RequiredValues.Keys, - _parameterPolicyFactory); + policies); } // Internal for testing @@ -312,11 +339,10 @@ namespace Microsoft.AspNetCore.Routing return false; } - if (!MatchesConstraints(httpContext, endpoint, templateValuesResult.CombinedValues)) + if (!templateBinder.TryProcessConstraints(httpContext, templateValuesResult.CombinedValues, out var parameterName, out var constraint)) { result = default; - - // MatchesConstraints does its own logging, so we're not logging here. + Log.TemplateFailedConstraint(_logger, endpoint, parameterName, constraint, templateValuesResult.CombinedValues); return false; } @@ -330,36 +356,6 @@ namespace Microsoft.AspNetCore.Routing return true; } - private bool MatchesConstraints( - HttpContext httpContext, - RouteEndpoint endpoint, - RouteValueDictionary routeValues) - { - if (routeValues == null) - { - throw new ArgumentNullException(nameof(routeValues)); - } - - foreach (var kvp in endpoint.RoutePattern.ParameterPolicies) - { - var parameter = endpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok - var constraintReferences = kvp.Value; - for (var i = 0; i < constraintReferences.Count; i++) - { - var constraintReference = constraintReferences[i]; - var parameterPolicy = _parameterPolicyFactory.Create(parameter, constraintReference); - if (parameterPolicy is IRouteConstraint routeConstraint - && !routeConstraint.Match(httpContext, NullRouter.Instance, kvp.Key, routeValues, RouteDirection.UrlGeneration)) - { - Log.TemplateFailedConstraint(_logger, endpoint, kvp.Key, routeConstraint, routeValues); - return false; - } - } - } - - return true; - } - // Also called from DefaultLinkGenerationTemplate public static RouteValueDictionary GetAmbientValues(HttpContext httpContext) { diff --git a/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs b/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs index e766cf13b4..1f61db30b2 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, parameterTransformer: null); + return Accept(value, encodeSlashes: true); } - public bool Accept(string value, bool encodeSlashes, IParameterTransformer parameterTransformer) + public bool Accept(string value, bool encodeSlashes) { if (string.IsNullOrEmpty(value)) { @@ -80,31 +80,31 @@ 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) { value = value.ToLowerInvariant(); } - for (var i = 0; i < _buffer.Count; i++) + var buffer = _buffer; + for (var i = 0; i < buffer.Count; i++) { - if (_buffer[i].RequiresEncoding) + var bufferValue = buffer[i].Value; + if (LowercaseUrls) { - EncodeValue(_buffer[i].Value); + bufferValue = bufferValue.ToLowerInvariant(); + } + + if (buffer[i].RequiresEncoding) + { + EncodeValue(bufferValue); } else { - _path.Append(_buffer[i].Value); + _path.Append(bufferValue); } } - _buffer.Clear(); + buffer.Clear(); if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) { diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs index 22fb79bc49..40755676fc 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs @@ -20,10 +20,11 @@ namespace Microsoft.AspNetCore.Routing.Template { private readonly UrlEncoder _urlEncoder; private readonly ObjectPool _pool; - private readonly ParameterPolicyFactory _parameterPolicyFactory; + private readonly (string parameterName, IRouteConstraint constraint)[] _constraints; private readonly RouteValueDictionary _defaults; private readonly KeyValuePair[] _filters; + private readonly (string parameterName, IOutboundParameterTransformer transformer)[] _parameterTransformers; private readonly RoutePattern _pattern; private readonly string[] _requiredKeys; @@ -44,7 +45,7 @@ namespace Microsoft.AspNetCore.Routing.Template ObjectPool pool, RouteTemplate template, RouteValueDictionary defaults) - : this(urlEncoder, pool, template?.ToRoutePattern(), defaults, requiredKeys: null, parameterPolicyFactory: null) + : this(urlEncoder, pool, template?.ToRoutePattern(), defaults, requiredKeys: null, parameterPolicies: null) { } @@ -56,14 +57,16 @@ namespace Microsoft.AspNetCore.Routing.Template /// The to bind values to. /// The default values for . Optional. /// Keys used to determine if the ambient values apply. Optional. - /// The . + /// + /// A list of (, ) pairs to evalute when producing a URI. + /// public TemplateBinder( UrlEncoder urlEncoder, ObjectPool pool, RoutePattern pattern, RouteValueDictionary defaults, IEnumerable requiredKeys, - ParameterPolicyFactory parameterPolicyFactory) + IEnumerable<(string parameterName, IParameterPolicy policy)> parameterPolicies) { if (urlEncoder == null) { @@ -84,7 +87,6 @@ namespace Microsoft.AspNetCore.Routing.Template _pool = pool; _pattern = pattern; _defaults = defaults; - _parameterPolicyFactory = parameterPolicyFactory; _requiredKeys = requiredKeys?.ToArray() ?? Array.Empty(); for (var i = 0; i < _requiredKeys.Length; i++) @@ -101,13 +103,21 @@ namespace Microsoft.AspNetCore.Routing.Template // 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. var filters = new RouteValueDictionary(_defaults); - foreach (var parameter in _pattern.Parameters) + for (var i = 0; i < pattern.Parameters.Count; i++) { - filters.Remove(parameter.Name); + filters.Remove(pattern.Parameters[i].Name); } - _filters = filters.ToArray(); - + + _constraints = parameterPolicies + ?.Where(p => p.policy is IRouteConstraint) + .Select(p => (p.parameterName, (IRouteConstraint)p.policy)) + .ToArray() ?? Array.Empty<(string, IRouteConstraint)>(); + _parameterTransformers = parameterPolicies + ?.Where(p => p.policy is IOutboundParameterTransformer) + .Select(p => (p.parameterName, (IOutboundParameterTransformer)p.policy)) + .ToArray() ?? Array.Empty<(string, IOutboundParameterTransformer)>(); + _slots = AssignSlots(_pattern, _filters); } @@ -351,6 +361,29 @@ namespace Microsoft.AspNetCore.Routing.Template }; } + // Step 1.5: Process constraints + // + // Processes the constraints **if** they were passed in to the TemplateBinder constructor. + // Returns true on success + // Returns false + sets the name/constraint for logging on failure. + public bool TryProcessConstraints(HttpContext httpContext, RouteValueDictionary combinedValues, out string parameterName, out IRouteConstraint constraint) + { + var constraints = _constraints; + for (var i = 0; i < constraints.Length; i++) + { + (parameterName, constraint) = constraints[i]; + + if (!constraint.Match(httpContext, NullRouter.Instance, parameterName, combinedValues, RouteDirection.UrlGeneration)) + { + return false; + } + } + + parameterName = null; + constraint = null; + return true; + } + // Step 2: If the route is a match generate the appropriate URI public string BindValues(RouteValueDictionary acceptedValues) { @@ -398,6 +431,18 @@ namespace Microsoft.AspNetCore.Routing.Template private bool TryBindValuesCore(UriBuildingContext context, RouteValueDictionary acceptedValues) { + // If we have any output parameter transformers, allow them a chance to influence the parameter values + // before we build the URI. + var parameterTransformers = _parameterTransformers; + for (var i = 0; i < parameterTransformers.Length; i++) + { + (var parameterName, var transformer) = parameterTransformers[i]; + if (acceptedValues.TryGetValue(parameterName, out var value)) + { + acceptedValues[parameterName] = transformer.TransformOutbound(value); + } + } + for (var i = 0; i < _pattern.PathSegments.Count; i++) { Debug.Assert(context.BufferState == SegmentState.Beginning); @@ -460,7 +505,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, GetParameterTransformer(parameterPart))) + if (!context.Accept(converted, parameterPart.EncodeSlashes)) { if (j != 0 && parameterPart.IsOptional && (separatorPart = segment.Parts[j - 1] as RoutePatternSeparatorPart) != null) { @@ -505,26 +550,6 @@ 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/DefaultLinkGeneratorProcessTemplateTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs index 1e603824f2..497c018239 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs @@ -277,6 +277,54 @@ namespace Microsoft.AspNetCore.Routing Assert.Equal(string.Empty, result.query.ToUriComponent()); } + // Regression test for https://github.com/aspnet/Routing/issues/802 + [Fact] + public void TryProcessTemplate_GeneratesLowercaseUrl_Includes_BufferedValues_SetOnRouteOptions() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("Foo/{bar=BAR}/{id?}"); + var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }, endpoints: new[] { endpoint, }); + var httpContext = CreateHttpContext(); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + explicitValues: new RouteValueDictionary(new { id = "18" }), + options: null, + out var result); + + // Assert + Assert.True(success); + Assert.Equal("/foo/bar/18", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } + + // Regression test for https://github.com/aspnet/Routing/issues/802 + [Fact] + public void TryProcessTemplate_ParameterPolicy_Includes_BufferedValues() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("Foo/{bar=BAR}/{id?}", policies: new { bar = new SlugifyParameterTransformer(), }); + var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }, endpoints: new[] { endpoint, }); + var httpContext = CreateHttpContext(); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + explicitValues: new RouteValueDictionary(new { id = "18" }), + options: null, + out var result); + + // Assert + Assert.True(success); + Assert.Equal("/foo/bar/18", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } + // Regression test for aspnet/Routing#435 // // In this issue we used to lowercase URLs after parameters were encoded, meaning that if a character needed @@ -531,7 +579,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( "{p1}/{p2}", defaults: new { p2 = "catchall" }, - constraints: new { p2 = "\\d{4}" }); + policies: new { p2 = "\\d{4}" }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = CreateHttpContext(ambientValues: new { }); @@ -555,7 +603,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( "{p1}/{p2}", defaults: new { p2 = "catchall" }, - constraints: new { p2 = new RegexRouteConstraint("\\d{4}"), }); + policies: new { p2 = new RegexRouteConstraint("\\d{4}"), }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = CreateHttpContext(ambientValues: new { }); @@ -581,7 +629,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( "{p1}/{*p2}", defaults: new { p2 = "catchall" }, - constraints: new { p2 = new RegexRouteConstraint("\\d{4}") }); + policies: new { p2 = new RegexRouteConstraint("\\d{4}") }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = CreateHttpContext(ambientValues: new { }); @@ -605,7 +653,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( "{p1}/{*p2}", defaults: new { p2 = "catchall" }, - constraints: new { p2 = new RegexRouteConstraint("\\d{4}") }); + policies: new { p2 = new RegexRouteConstraint("\\d{4}") }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = CreateHttpContext(ambientValues: new { }); @@ -642,7 +690,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( "{p1}/{p2}", defaults: new { p2 = "catchall" }, - constraints: new { p2 = target.Object }); + policies: new { p2 = target.Object }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = CreateHttpContext(ambientValues: new { }); @@ -674,7 +722,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( template: "slug/Home/Store", defaults: new { controller = "Home", action = "Store" }, - constraints: new { c = constraint }); + policies: new { c = constraint }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = CreateHttpContext( ambientValues: new { controller = "Home", action = "Blog", extra = "42" }); @@ -708,7 +756,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( template: "slug/Home/Store", defaults: new { controller = "Home", action = "Store", otherthing = "17" }, - constraints: new { c = constraint }); + policies: new { c = constraint }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Blog" }); var expectedValues = new RouteValueDictionary( @@ -740,7 +788,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( template: "slug/{controller}/{action}", defaults: new { action = "Index" }, - constraints: new { c = constraint, }); + policies: new { c = constraint, }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Blog" }); var expectedValues = new RouteValueDictionary( @@ -772,7 +820,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( template: "slug/Home/Store", defaults: new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" }, - constraints: new { c = constraint, }); + policies: new { c = constraint, }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = CreateHttpContext( ambientValues: new { controller = "Home", action = "Blog", otherthing = "17" }); @@ -806,7 +854,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( template: "Home/Index/{id:int}", defaults: new { controller = "Home", action = "Index" }, - constraints: new { }); + policies: new { }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null; @@ -832,7 +880,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( template: "Home/Index/{id}", defaults: new { controller = "Home", action = "Index" }, - constraints: new { id = "int" }); + policies: new { id = "int" }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = CreateHttpContext(ambientValues: new { }); @@ -858,7 +906,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( template: "Home/Index/{id:int?}", defaults: new { controller = "Home", action = "Index" }, - constraints: new { }); + policies: new { }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null; @@ -884,7 +932,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( template: "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index" }, - constraints: new { id = "int" }); + policies: new { id = "int" }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = CreateHttpContext(ambientValues: new { }); @@ -910,7 +958,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( template: "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index" }, - constraints: new { id = "int" }); + policies: new { id = "int" }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = CreateHttpContext(ambientValues: new { }); @@ -936,7 +984,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( template: "Home/Index/{id:int:range(1,20)}", defaults: new { controller = "Home", action = "Index" }, - constraints: new { }); + policies: new { }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null; @@ -964,7 +1012,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( template: "Home/Index/{id:int:range(1,20)}", defaults: new { controller = "Home", action = "Index" }, - constraints: new { }); + policies: new { }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null; @@ -989,7 +1037,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateRouteEndpoint( template: "Home/Index/{name}", defaults: new { controller = "Home", action = "Index" }, - constraints: new { name = constraint }); + policies: new { name = constraint }); var linkGenerator = CreateLinkGenerator(endpoint); var httpContext = CreateHttpContext(ambientValues: new { }); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs index c02aec833a..f64297e791 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs @@ -298,11 +298,11 @@ namespace Microsoft.AspNetCore.Routing Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", path); } - private class UpperCaseParameterTransform : IParameterTransformer + private class UpperCaseParameterTransform : IOutboundParameterTransformer { - public string Transform(string value) + public string TransformOutbound(object value) { - return value?.ToUpperInvariant(); + return value?.ToString()?.ToUpperInvariant(); } } @@ -329,6 +329,29 @@ namespace Microsoft.AspNetCore.Routing Assert.Equal("/HOME/Test", link); } + [Fact] + public void GetLink_ParameterTransformer_ForQueryString() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller:upper-case}/{name}", policies: new { c = new UpperCaseParameterTransform(), }); + + var routeOptions = new RouteOptions(); + routeOptions.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); + + Action configure = (s) => + { + s.AddSingleton(typeof(UpperCaseParameterTransform), new UpperCaseParameterTransform()); + }; + + var linkGenerator = CreateLinkGenerator(routeOptions, configure, endpoint); + + // Act + var link = linkGenerator.GetPathByRouteValues(routeName: null, new { controller = "Home", name = "Test", c = "hithere", }); + + // Assert + Assert.Equal("/HOME/Test?c=HITHERE", link); + } + // Includes characters that need to be encoded [Fact] public void GetPathByAddress_WithHttpContext_WithPathBaseAndFragment() diff --git a/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs b/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs index 43f4446479..d7ffcb16b8 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Routing public static RouteEndpoint CreateRouteEndpoint( string template, object defaults = null, - object constraints = null, + object policies = null, object requiredValues = null, int order = 0, string displayName = null, @@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Routing return new RouteEndpoint( TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse(template, defaults, constraints), + RoutePatternFactory.Parse(template, defaults, policies), order, new EndpointMetadataCollection(d), displayName); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs index e77c3cf233..5817d5ae03 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.TestObjects; @@ -1294,12 +1295,8 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests public void BindValues_ParameterTransformer() { // Arrange - var routeOptions = new RouteOptions(); - routeOptions.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); - var parameterPolicyFactory = new DefaultParameterPolicyFactory( - Options.Create(routeOptions), - new ServiceCollection().BuildServiceProvider()); 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" }); @@ -1310,7 +1307,7 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests RoutePatternFactory.Parse(template), defaults, requiredKeys: defaults.Keys, - parameterPolicyFactory); + parameterPolicies: new (string, IParameterPolicy)[] { ("param", new LengthRouteConstraint(500)), ("param", new SlugifyParameterTransformer()), }); // Act var result = binder.GetValues(ambientValues, explicitValues); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/TestObjects/SlugifyParameterTransformer.cs b/test/Microsoft.AspNetCore.Routing.Tests/TestObjects/SlugifyParameterTransformer.cs index acf07d8cc8..625a5c3137 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/TestObjects/SlugifyParameterTransformer.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/TestObjects/SlugifyParameterTransformer.cs @@ -1,17 +1,16 @@ // 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 class SlugifyParameterTransformer : IOutboundParameterTransformer { - public string Transform(string value) + public string TransformOutbound(object value) { // Slugify value - return Regex.Replace(value, "([a-z])([A-Z])", "$1-$2").ToLower(); + return value == null ? null : Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower(); } } }