Add IParameterTransformer (#750)

This commit is contained in:
James Newton-King 2018-09-12 21:45:25 +12:00 committed by GitHub
parent e5cc4564cb
commit cee960f3c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 272 additions and 34 deletions

View File

@ -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
{
/// <summary>
/// Defines the contract that a class must implement to transform parameter values.
/// </summary>
public interface IParameterTransformer : IParameterPolicy
{
/// <summary>
/// Transforms the specified parameter value.
/// </summary>
/// <param name="value">The parameter value to transform.</param>
/// <returns>The transformed value.</returns>
string Transform(string value);
}
}

View File

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

View File

@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Constraints
{
/// <summary>
/// Constrains a route parameter to contain only a specified strign.
/// Constrains a route parameter to contain only a specified string.
/// </summary>
public class StringRouteConstraint : IRouteConstraint
{

View File

@ -56,7 +56,12 @@ namespace Microsoft.AspNetCore.Routing
throw new ArgumentNullException(nameof(inlineConstraint));
}
return ParameterPolicyActivator.ResolveParameterPolicy<IRouteConstraint>(_inlineConstraintMap, _serviceProvider, inlineConstraint, out _);
// This will return null if the text resolves to a non-IRouteConstraint
return ParameterPolicyActivator.ResolveParameterPolicy<IRouteConstraint>(
_inlineConstraintMap,
_serviceProvider,
inlineConstraint,
out _);
}
}
}

View File

@ -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<IRouteValuesAddressMetadata>();
var templateValuesResult = templateBinder.GetValues(

View File

@ -44,7 +44,12 @@ namespace Microsoft.AspNetCore.Routing
throw new ArgumentNullException(nameof(inlineText));
}
var parameterPolicy = ParameterPolicyActivator.ResolveParameterPolicy<IParameterPolicy>(_options.ConstraintMap, _serviceProvider, inlineText, out var parameterPolicyKey);
var parameterPolicy = ParameterPolicyActivator.ResolveParameterPolicy<IParameterPolicy>(
_options.ConstraintMap,
_serviceProvider,
inlineText,
out var parameterPolicyKey);
if (parameterPolicy == null)
{
throw new InvalidOperationException(Resources.FormatRoutePattern_ConstraintReferenceNotFound(

View File

@ -13,7 +13,11 @@ namespace Microsoft.AspNetCore.Routing.Internal
{
internal static class ParameterPolicyActivator
{
public static T ResolveParameterPolicy<T>(IDictionary<string, Type> inlineParameterPolicyMap, IServiceProvider serviceProvider, string inlineParameterPolicy, out string parameterPolicyKey)
public static T ResolveParameterPolicy<T>(
IDictionary<string, Type> 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

View File

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

View File

@ -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<IInlineConstraintResolver>();
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<IInlineConstraintResolver>();
var parameterPolicyFactory = serviceProvider
.GetRequiredService<ParameterPolicyFactory>();
// 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;
}
}
}
}

View File

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

View File

@ -20,6 +20,7 @@ namespace Microsoft.AspNetCore.Routing.Template
private readonly ObjectPool<UriBuildingContext> _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<UriBuildingContext> 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
/// <param name="pool">The <see cref="ObjectPool{T}"/>.</param>
/// <param name="pattern">The <see cref="RoutePattern"/> to bind values to.</param>
/// <param name="defaults">The default values for <paramref name="pattern"/>.</param>
/// <param name="parameterPolicyFactory">The <see cref="ParameterPolicyFactory"/>.</param>
public TemplateBinder(
UrlEncoder urlEncoder,
ObjectPool<UriBuildingContext> 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);

View File

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

View File

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

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Routing
return httpContext;
}
private ServiceCollection GetBasicServices()
protected ServiceCollection GetBasicServices()
{
var services = new ServiceCollection();
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();

View File

@ -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<IInlineConstraintResolver>(_inlineConstraintResolver);
services.AddSingleton<RoutingMarkerService>();
services.AddSingleton<ParameterPolicyFactory, DefaultParameterPolicyFactory>();
services.Configure<RouteOptions>(ConfigureRouteOptions);
var applicationBuilder = Mock.Of<IApplicationBuilder>();
applicationBuilder.ApplicationServices = services.BuildServiceProvider();
@ -1837,12 +1854,24 @@ namespace Microsoft.AspNetCore.Routing
private static IInlineConstraintResolver GetInlineConstraintResolver()
{
var routeOptions = new Mock<IOptions<RouteOptions>>();
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<IOptions<RouteOptions>>();
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
{
}
}
}

View File

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

View File

@ -132,6 +132,8 @@ namespace Microsoft.AspNetCore.Routing.Tests
var services = new ServiceCollection();
services.AddSingleton<IInlineConstraintResolver>(_inlineConstraintResolver);
services.AddSingleton<RoutingMarkerService>();
services.AddSingleton<ParameterPolicyFactory, DefaultParameterPolicyFactory>();
services.Configure<RouteOptions>(options => { });
var applicationBuilder = Mock.Of<IApplicationBuilder>();
applicationBuilder.ApplicationServices = services.BuildServiceProvider();