Updates to Parameter Transformer

- Rename -> IOutboundParameterTransformer
- Make it operate on object
- Implementing caching for constraints/tranformers for link generation
(cached as part of TemplateBinder)
This commit is contained in:
Ryan Nowak 2018-09-18 19:09:04 -07:00
parent df27b3ec10
commit a657c3bdf2
9 changed files with 204 additions and 115 deletions

View File

@ -4,15 +4,16 @@
namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// 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.
/// </summary>
public interface IParameterTransformer : IParameterPolicy
public interface IOutboundParameterTransformer : IParameterPolicy
{
/// <summary>
/// Transforms the specified parameter value.
/// Transforms the specified route value to a string for inclusion in a URI.
/// </summary>
/// <param name="value">The parameter value to transform.</param>
/// <param name="value">The route value to transform.</param>
/// <returns>The transformed value.</returns>
string Transform(string value);
string TransformOutbound(object value);
}
}

View File

@ -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<IRouteValuesAddressMetadata>()?.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)
{

View File

@ -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)
{

View File

@ -20,10 +20,11 @@ namespace Microsoft.AspNetCore.Routing.Template
{
private readonly UrlEncoder _urlEncoder;
private readonly ObjectPool<UriBuildingContext> _pool;
private readonly ParameterPolicyFactory _parameterPolicyFactory;
private readonly (string parameterName, IRouteConstraint constraint)[] _constraints;
private readonly RouteValueDictionary _defaults;
private readonly KeyValuePair<string, object>[] _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<UriBuildingContext> 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
/// <param name="pattern">The <see cref="RoutePattern"/> to bind values to.</param>
/// <param name="defaults">The default values for <paramref name="pattern"/>. Optional.</param>
/// <param name="requiredKeys">Keys used to determine if the ambient values apply. Optional.</param>
/// <param name="parameterPolicyFactory">The <see cref="ParameterPolicyFactory"/>.</param>
/// <param name="parameterPolicies">
/// A list of (<see cref="string"/>, <see cref="IParameterPolicy"/>) pairs to evalute when producing a URI.
/// </param>
public TemplateBinder(
UrlEncoder urlEncoder,
ObjectPool<UriBuildingContext> pool,
RoutePattern pattern,
RouteValueDictionary defaults,
IEnumerable<string> 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<string>();
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);

View File

@ -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 { });

View File

@ -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<IServiceCollection> 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()

View File

@ -13,7 +13,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,
@ -27,7 +27,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);

View File

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

View File

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