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