Add IParameterTransformer support (#8329)

This commit is contained in:
James Newton-King 2018-09-12 21:46:41 +12:00 committed by GitHub
parent 105f8b47a1
commit dfae9c208a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 723 additions and 16 deletions

View File

@ -110,7 +110,8 @@ namespace Microsoft.AspNetCore.Mvc.Performance
{ {
var dataSource = new MvcEndpointDataSource( var dataSource = new MvcEndpointDataSource(
actionDescriptorCollectionProvider, actionDescriptorCollectionProvider,
new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>()))); new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
new MockParameterPolicyFactory());
return dataSource; return dataSource;
} }

View File

@ -48,8 +48,8 @@
<MicrosoftAspNetCoreRazorTagHelpersTestingSourcesPackageVersion>2.2.0-preview3-35202</MicrosoftAspNetCoreRazorTagHelpersTestingSourcesPackageVersion> <MicrosoftAspNetCoreRazorTagHelpersTestingSourcesPackageVersion>2.2.0-preview3-35202</MicrosoftAspNetCoreRazorTagHelpersTestingSourcesPackageVersion>
<MicrosoftAspNetCoreResponseCachingAbstractionsPackageVersion>2.2.0-preview3-35202</MicrosoftAspNetCoreResponseCachingAbstractionsPackageVersion> <MicrosoftAspNetCoreResponseCachingAbstractionsPackageVersion>2.2.0-preview3-35202</MicrosoftAspNetCoreResponseCachingAbstractionsPackageVersion>
<MicrosoftAspNetCoreResponseCachingPackageVersion>2.2.0-preview3-35202</MicrosoftAspNetCoreResponseCachingPackageVersion> <MicrosoftAspNetCoreResponseCachingPackageVersion>2.2.0-preview3-35202</MicrosoftAspNetCoreResponseCachingPackageVersion>
<MicrosoftAspNetCoreRoutingAbstractionsPackageVersion>2.2.0-preview3-35202</MicrosoftAspNetCoreRoutingAbstractionsPackageVersion> <MicrosoftAspNetCoreRoutingAbstractionsPackageVersion>2.2.0-a-preview3-parameter-transformers-16968</MicrosoftAspNetCoreRoutingAbstractionsPackageVersion>
<MicrosoftAspNetCoreRoutingPackageVersion>2.2.0-preview3-35202</MicrosoftAspNetCoreRoutingPackageVersion> <MicrosoftAspNetCoreRoutingPackageVersion>2.2.0-a-preview3-parameter-transformers-16968</MicrosoftAspNetCoreRoutingPackageVersion>
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.2.0-preview3-35202</MicrosoftAspNetCoreServerIISIntegrationPackageVersion> <MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.2.0-preview3-35202</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
<MicrosoftAspNetCoreServerKestrelPackageVersion>2.2.0-preview3-35202</MicrosoftAspNetCoreServerKestrelPackageVersion> <MicrosoftAspNetCoreServerKestrelPackageVersion>2.2.0-preview3-35202</MicrosoftAspNetCoreServerKestrelPackageVersion>
<MicrosoftAspNetCoreSessionPackageVersion>2.2.0-preview3-35202</MicrosoftAspNetCoreSessionPackageVersion> <MicrosoftAspNetCoreSessionPackageVersion>2.2.0-preview3-35202</MicrosoftAspNetCoreSessionPackageVersion>

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Routing;
namespace Microsoft.AspNetCore.Mvc.Routing namespace Microsoft.AspNetCore.Mvc.Routing
{ {
/// <summary> /// <summary>

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
namespace Microsoft.AspNetCore.Mvc.ApplicationModels namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{ {
@ -220,6 +221,11 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
} }
public static string ReplaceTokens(string template, IDictionary<string, string> values) public static string ReplaceTokens(string template, IDictionary<string, string> values)
{
return ReplaceTokens(template, values, routeTokenTransformer: null);
}
public static string ReplaceTokens(string template, IDictionary<string, string> values, IParameterTransformer routeTokenTransformer)
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
var state = TemplateParserState.Plaintext; var state = TemplateParserState.Plaintext;
@ -371,6 +377,11 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
throw new InvalidOperationException(message); throw new InvalidOperationException(message);
} }
if (routeTokenTransformer != null)
{
value = routeTokenTransformer.Transform(value);
}
builder.Append(value); builder.Append(value);
if (c == '[') if (c == '[')

View File

@ -0,0 +1,41 @@
// 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 Microsoft.AspNetCore.Routing;
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
/// <summary>
/// An <see cref="IActionModelConvention"/> that sets attribute routing token replacement
/// to use the specified <see cref="IParameterTransformer"/> on <see cref="ActionModel"/> selectors.
/// </summary>
public class RouteTokenTransformerConvention : IActionModelConvention
{
private readonly IParameterTransformer _parameterTransformer;
/// <summary>
/// Creates a new instance of <see cref="RouteTokenTransformerConvention"/> with the specified <see cref="IParameterTransformer"/>.
/// </summary>
/// <param name="parameterTransformer">The <see cref="IParameterTransformer"/> to use with attribute routing token replacement.</param>
public RouteTokenTransformerConvention(IParameterTransformer parameterTransformer)
{
if (parameterTransformer == null)
{
throw new ArgumentNullException(nameof(parameterTransformer));
}
_parameterTransformer = parameterTransformer;
}
public void Apply(ActionModel action)
{
if (ShouldApply(action))
{
action.Properties[typeof(IParameterTransformer)] = _parameterTransformer;
}
}
protected virtual bool ShouldApply(ActionModel action) => true;
}
}

View File

@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Resources = Microsoft.AspNetCore.Mvc.Core.Resources; using Resources = Microsoft.AspNetCore.Mvc.Core.Resources;
namespace Microsoft.AspNetCore.Mvc.Internal namespace Microsoft.AspNetCore.Mvc.Internal
@ -389,15 +390,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ {
try try
{ {
actionDescriptor.Properties.TryGetValue(typeof(IParameterTransformer), out var transformer);
var routeTokenTransformer = transformer as IParameterTransformer;
actionDescriptor.AttributeRouteInfo.Template = AttributeRouteModel.ReplaceTokens( actionDescriptor.AttributeRouteInfo.Template = AttributeRouteModel.ReplaceTokens(
actionDescriptor.AttributeRouteInfo.Template, actionDescriptor.AttributeRouteInfo.Template,
actionDescriptor.RouteValues); actionDescriptor.RouteValues,
routeTokenTransformer);
if (actionDescriptor.AttributeRouteInfo.Name != null) if (actionDescriptor.AttributeRouteInfo.Name != null)
{ {
actionDescriptor.AttributeRouteInfo.Name = AttributeRouteModel.ReplaceTokens( actionDescriptor.AttributeRouteInfo.Name = AttributeRouteModel.ReplaceTokens(
actionDescriptor.AttributeRouteInfo.Name, actionDescriptor.AttributeRouteInfo.Name,
actionDescriptor.RouteValues); actionDescriptor.RouteValues,
routeTokenTransformer);
} }
} }
catch (InvalidOperationException ex) catch (InvalidOperationException ex)

View File

@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ {
private readonly IActionDescriptorCollectionProvider _actions; private readonly IActionDescriptorCollectionProvider _actions;
private readonly MvcEndpointInvokerFactory _invokerFactory; private readonly MvcEndpointInvokerFactory _invokerFactory;
private readonly ParameterPolicyFactory _parameterPolicyFactory;
// The following are protected by this lock for WRITES only. This pattern is similar // The following are protected by this lock for WRITES only. This pattern is similar
// to DefaultActionDescriptorChangeProvider - see comments there for details on // to DefaultActionDescriptorChangeProvider - see comments there for details on
@ -33,7 +34,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public MvcEndpointDataSource( public MvcEndpointDataSource(
IActionDescriptorCollectionProvider actions, IActionDescriptorCollectionProvider actions,
MvcEndpointInvokerFactory invokerFactory) MvcEndpointInvokerFactory invokerFactory,
ParameterPolicyFactory parameterPolicyFactory)
{ {
if (actions == null) if (actions == null)
{ {
@ -45,8 +47,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(invokerFactory)); throw new ArgumentNullException(nameof(invokerFactory));
} }
if (parameterPolicyFactory == null)
{
throw new ArgumentNullException(nameof(parameterPolicyFactory));
}
_actions = actions; _actions = actions;
_invokerFactory = invokerFactory; _invokerFactory = invokerFactory;
_parameterPolicyFactory = parameterPolicyFactory;
ConventionalEndpointInfos = new List<MvcEndpointInfo>(); ConventionalEndpointInfos = new List<MvcEndpointInfo>();
@ -253,9 +261,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ {
segmentParts = segment.Parts.ToList(); segmentParts = segment.Parts.ToList();
} }
if (allParameterPolicies == null)
{
allParameterPolicies = MvcEndpointInfo.BuildParameterPolicies(routePattern.Parameters, _parameterPolicyFactory);
}
var parameterRouteValue = action.RouteValues[parameterPart.Name]; var parameterRouteValue = action.RouteValues[parameterPart.Name];
// Replace parameter with literal value
if (allParameterPolicies.TryGetValue(parameterPart.Name, out var parameterPolicies))
{
// Check if the parameter has a transformer policy
// Use the first transformer policy
for (var k = 0; k < parameterPolicies.Count; k++)
{
if (parameterPolicies[k] is IParameterTransformer parameterTransformer)
{
parameterRouteValue = parameterTransformer.Transform(parameterRouteValue);
break;
}
}
}
segmentParts[j] = RoutePatternFactory.LiteralPart(parameterRouteValue); segmentParts[j] = RoutePatternFactory.LiteralPart(parameterRouteValue);
} }
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using Microsoft.AspNetCore.Routing;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ApplicationModels namespace Microsoft.AspNetCore.Mvc.ApplicationModels

View File

@ -0,0 +1,101 @@
// 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.Collections.Generic;
using System.Reflection;
using System.Text;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Routing;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Test.ApplicationModel
{
public class RouteTokenTransformerConventionTest
{
[Fact]
public void Apply_NullAttributeRouteModel_NoOp()
{
// Arrange
var convention = new RouteTokenTransformerConvention(new TestParameterTransformer());
var model = new ActionModel(GetMethodInfo(), Array.Empty<object>());
model.Selectors.Add(new SelectorModel()
{
AttributeRouteModel = null
});
// Act
convention.Apply(model);
// Assert
Assert.Null(model.Selectors[0].AttributeRouteModel);
}
[Fact]
public void Apply_HasAttributeRouteModel_SetRouteTokenTransformer()
{
// Arrange
var transformer = new TestParameterTransformer();
var convention = new RouteTokenTransformerConvention(transformer);
var model = new ActionModel(GetMethodInfo(), Array.Empty<object>());
model.Selectors.Add(new SelectorModel()
{
AttributeRouteModel = new AttributeRouteModel()
});
// Act
convention.Apply(model);
// Assert
Assert.True(model.Properties.TryGetValue(typeof(IParameterTransformer), out var routeTokenTransformer));
Assert.Equal(transformer, routeTokenTransformer);
}
[Fact]
public void Apply_ShouldApplyFalse_NoOp()
{
// Arrange
var transformer = new TestParameterTransformer();
var convention = new CustomRouteTokenTransformerConvention(transformer);
var model = new ActionModel(GetMethodInfo(), Array.Empty<object>());
model.Selectors.Add(new SelectorModel()
{
AttributeRouteModel = new AttributeRouteModel()
});
// Act
convention.Apply(model);
// Assert
Assert.False(model.Properties.TryGetValue(typeof(IParameterTransformer), out _));
}
private MethodInfo GetMethodInfo()
{
return typeof(RouteTokenTransformerConventionTest).GetMethod(nameof(GetMethodInfo), BindingFlags.NonPublic | BindingFlags.Instance);
}
private class TestParameterTransformer : IParameterTransformer
{
public string Transform(string value)
{
return value;
}
}
private class CustomRouteTokenTransformerConvention : RouteTokenTransformerConvention
{
public CustomRouteTokenTransformerConvention(IParameterTransformer parameterTransformer) : base(parameterTransformer)
{
}
protected override bool ShouldApply(ActionModel action)
{
return false;
}
}
}
}

View File

@ -161,6 +161,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{"{controller}/{action=TestAction}/{id?}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{id?}/{*catchAll}" }}, {"{controller}/{action=TestAction}/{id?}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{id?}/{*catchAll}" }},
{"{controller}/{action}.{ext?}", new[] { "TestController/TestAction.{ext?}" }}, {"{controller}/{action}.{ext?}", new[] { "TestController/TestAction.{ext?}" }},
{"{controller}/{action=TestAction}.{ext?}", new[] { "TestController", "TestController/TestAction.{ext?}" }}, {"{controller}/{action=TestAction}.{ext?}", new[] { "TestController", "TestController/TestAction.{ext?}" }},
{"{controller:upper-case}/{action=TestAction}.{ext?}", new[] { "TESTCONTROLLER", "TESTCONTROLLER/TestAction.{ext?}" }},
}; };
return data; return data;
@ -217,6 +218,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
[InlineData("{area=TestArea}/{controller}/{action=TestAction}/{id?}", new[] { "TestArea/TestController", "TestArea/TestController/TestAction/{id?}" })] [InlineData("{area=TestArea}/{controller}/{action=TestAction}/{id?}", new[] { "TestArea/TestController", "TestArea/TestController/TestAction/{id?}" })]
[InlineData("{area=TestArea}/{controller=TestController}/{action=TestAction}/{id?}", new[] { "", "TestArea", "TestArea/TestController", "TestArea/TestController/TestAction/{id?}" })] [InlineData("{area=TestArea}/{controller=TestController}/{action=TestAction}/{id?}", new[] { "", "TestArea", "TestArea/TestController", "TestArea/TestController/TestAction/{id?}" })]
[InlineData("{area:exists}/{controller}/{action}/{id?}", new[] { "TestArea/TestController/TestAction/{id?}" })] [InlineData("{area:exists}/{controller}/{action}/{id?}", new[] { "TestArea/TestController/TestAction/{id?}" })]
[InlineData("{area:exists:upper-case}/{controller}/{action}/{id?}", new[] { "TESTAREA/TestController/TestAction/{id?}" })]
public void Endpoints_AreaSingleAction(string endpointInfoRoute, string[] finalEndpointTemplates) public void Endpoints_AreaSingleAction(string endpointInfoRoute, string[] finalEndpointTemplates)
{ {
// Arrange // Arrange
@ -230,6 +232,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); var routeOptionsSetup = new MvcCoreRouteOptionsSetup();
services.Configure<RouteOptions>(routeOptionsSetup.Configure); services.Configure<RouteOptions>(routeOptionsSetup.Configure);
services.Configure<RouteOptions>(options =>
{
options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform);
});
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute, serviceProvider: services.BuildServiceProvider())); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute, serviceProvider: services.BuildServiceProvider()));
@ -377,6 +383,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
[Theory] [Theory]
[InlineData("{controller}/{action}", new[] { "TestController1/TestAction1", "TestController1/TestAction2", "TestController1/TestAction3", "TestController2/TestAction1" })] [InlineData("{controller}/{action}", new[] { "TestController1/TestAction1", "TestController1/TestAction2", "TestController1/TestAction3", "TestController2/TestAction1" })]
[InlineData("{controller}/{action:regex((TestAction1|TestAction2))}", new[] { "TestController1/TestAction1", "TestController1/TestAction2", "TestController2/TestAction1" })] [InlineData("{controller}/{action:regex((TestAction1|TestAction2))}", new[] { "TestController1/TestAction1", "TestController1/TestAction2", "TestController2/TestAction1" })]
[InlineData("{controller}/{action:regex((TestAction1|TestAction2)):upper-case}", new[] { "TestController1/TESTACTION1", "TestController1/TESTACTION2", "TestController2/TESTACTION1" })]
public void Endpoints_MultipleActions(string endpointInfoRoute, string[] finalEndpointTemplates) public void Endpoints_MultipleActions(string endpointInfoRoute, string[] finalEndpointTemplates)
{ {
// Arrange // Arrange
@ -750,14 +757,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddSingleton(actionDescriptorCollectionProvider); services.AddSingleton(actionDescriptorCollectionProvider);
services.AddRouting(options =>
{
options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform);
});
var serviceProvider = services.BuildServiceProvider();
var dataSource = new MvcEndpointDataSource( var dataSource = new MvcEndpointDataSource(
actionDescriptorCollectionProvider, actionDescriptorCollectionProvider,
mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>()))); mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
serviceProvider.GetRequiredService<ParameterPolicyFactory>());
return dataSource; return dataSource;
} }
private class UpperCaseParameterTransform : IParameterTransformer
{
public string Transform(string value)
{
return value?.ToUpperInvariant();
}
}
private MvcEndpointInfo CreateEndpointInfo( private MvcEndpointInfo CreateEndpointInfo(
string name, string name,
string template, string template,
@ -768,13 +789,18 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ {
if (serviceProvider == null) if (serviceProvider == null)
{ {
var serviceCollection = new ServiceCollection(); var services = new ServiceCollection();
serviceCollection.AddRouting(); services.AddRouting();
services.AddSingleton(typeof(UpperCaseParameterTransform), new UpperCaseParameterTransform());
var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); var routeOptionsSetup = new MvcCoreRouteOptionsSetup();
serviceCollection.Configure<RouteOptions>(routeOptionsSetup.Configure); services.Configure<RouteOptions>(routeOptionsSetup.Configure);
services.Configure<RouteOptions>(options =>
{
options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform);
});
serviceProvider = serviceCollection.BuildServiceProvider(); serviceProvider = services.BuildServiceProvider();
} }
var parameterPolicyFactory = serviceProvider.GetRequiredService<ParameterPolicyFactory>(); var parameterPolicyFactory = serviceProvider.GetRequiredService<ParameterPolicyFactory>();

View File

@ -17,6 +17,170 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{ {
} }
[Fact]
public async Task ParameterTransformer_TokenReplacement_Found()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/_ParameterTransformer_/_Test_");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
Assert.Equal("ParameterTransformer", result.Controller);
Assert.Equal("Test", result.Action);
}
[Fact]
public async Task ParameterTransformer_TokenReplacement_NotFound()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/ParameterTransformer/Test");
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task AttributeRoutedAction_Parameters_Found()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/EndpointRouting/Index");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
Assert.Equal("EndpointRouting", result.Controller);
Assert.Equal("Index", result.Action);
}
[Fact]
public async Task AttributeRoutedAction_Parameters_DefaultValue_Found()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/EndpointRouting");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
Assert.Equal("EndpointRouting", result.Controller);
Assert.Equal("Index", result.Action);
}
[Fact]
public async Task AttributeRoutedAction_ParameterTransformer_Found()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/_EndpointRouting_/ParameterTransformer");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
Assert.Equal("EndpointRouting", result.Controller);
Assert.Equal("ParameterTransformer", result.Action);
}
[Fact]
public async Task AttributeRoutedAction_ParameterTransformer_NotFound()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/EndpointRouting/ParameterTransformer");
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task AttributeRoutedAction_ParameterTransformer_LinkToSelf()
{
// Arrange
var url = LinkFrom("http://localhost/_EndpointRouting_/ParameterTransformer").To(new { });
// Act
var response = await Client.GetAsync(url);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
Assert.Equal("EndpointRouting", result.Controller);
Assert.Equal("ParameterTransformer", result.Action);
Assert.Equal("/_EndpointRouting_/ParameterTransformer", result.Link);
}
[Fact]
public async Task AttributeRoutedAction_ParameterTransformer_LinkWithAmbientController()
{
// Arrange
var url = LinkFrom("http://localhost/_EndpointRouting_/ParameterTransformer").To(new { action = "Get", id = 5 });
// Act
var response = await Client.GetAsync(url);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
Assert.Equal("EndpointRouting", result.Controller);
Assert.Equal("ParameterTransformer", result.Action);
Assert.Equal("/_EndpointRouting_/5", result.Link);
}
[Fact]
public async Task AttributeRoutedAction_ParameterTransformer_LinkToAttributeRoutedController()
{
// Arrange
var url = LinkFrom("http://localhost/_EndpointRouting_/ParameterTransformer").To(new { action = "ShowPosts", controller = "Blog" });
// Act
var response = await Client.GetAsync(url);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
Assert.Equal("EndpointRouting", result.Controller);
Assert.Equal("ParameterTransformer", result.Action);
Assert.Equal("/Blog/ShowPosts", result.Link);
}
[Fact]
public async Task AttributeRoutedAction_ParameterTransformer_LinkToConventionalController()
{
// Arrange
var url = LinkFrom("http://localhost/_EndpointRouting_/ParameterTransformer").To(new { action = "Index", controller = "Home" });
// Act
var response = await Client.GetAsync(url);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
Assert.Equal("EndpointRouting", result.Controller);
Assert.Equal("ParameterTransformer", result.Action);
Assert.Equal("/", result.Link);
}
[Fact] [Fact]
public async override Task HasEndpointMatch() public async override Task HasEndpointMatch()
{ {
@ -123,5 +287,119 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Assert // Assert
Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode);
} }
[Fact]
public async Task ConventionalRoutedAction_ParameterTransformer()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/ConventionalTransformerRoute/_ConventionalTransformer_/Index");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
Assert.Equal("ConventionalTransformer", result.Controller);
Assert.Equal("Index", result.Action);
}
[Fact]
public async Task ConventionalRoutedAction_ParameterTransformer_NotFound()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/ConventionalTransformerRoute/ConventionalTransformer/Index");
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task ConventionalRoutedAction_ParameterTransformer_DefaultValue()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/ConventionalTransformerRoute/_ConventionalTransformer_");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
Assert.Equal("ConventionalTransformer", result.Controller);
Assert.Equal("Index", result.Action);
}
[Fact]
public async Task ConventionalRoutedAction_ParameterTransformer_WithParam()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/ConventionalTransformerRoute/_ConventionalTransformer_/Param/_value_");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
Assert.Equal("ConventionalTransformer", result.Controller);
Assert.Equal("Param", result.Action);
Assert.Equal("/ConventionalTransformerRoute/_ConventionalTransformer_/Param/_value_", Assert.Single(result.ExpectedUrls));
}
[Fact]
public async Task ConventionalRoutedAction_ParameterTransformer_LinkToConventionalController()
{
// Arrange
var url = LinkFrom("http://localhost/ConventionalTransformerRoute/_ConventionalTransformer_/Index").To(new { action = "Index", controller = "Home" });
// Act
var response = await Client.GetAsync(url);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
Assert.Equal("ConventionalTransformer", result.Controller);
Assert.Equal("Index", result.Action);
Assert.Equal("/", result.Link);
}
[Fact]
public async Task ConventionalRoutedAction_ParameterTransformer_LinkToConventionalControllerWithParam()
{
// Arrange
var url = LinkFrom("http://localhost/ConventionalTransformerRoute/_ConventionalTransformer_/Index").To(new { action = "Param", controller = "ConventionalTransformer", param = "value" });
// Act
var response = await Client.GetAsync(url);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
Assert.Equal("ConventionalTransformer", result.Controller);
Assert.Equal("Index", result.Action);
Assert.Equal("/ConventionalTransformerRoute/_ConventionalTransformer_/Param/_value_", result.Link);
}
[Fact]
public async Task ConventionalRoutedAction_ParameterTransformer_LinkToSelf()
{
// Arrange
var url = LinkFrom("http://localhost/ConventionalTransformerRoute/_ConventionalTransformer_/Index").To(new {});
// Act
var response = await Client.GetAsync(url);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
Assert.Equal("ConventionalTransformer", result.Controller);
Assert.Equal("Index", result.Action);
Assert.Equal("/ConventionalTransformerRoute/_ConventionalTransformer_", result.Link);
}
} }
} }

View File

@ -0,0 +1,38 @@
// 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.Linq;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Routing;
namespace RoutingWebSite
{
public class ControllerRouteTokenTransformerConvention : IApplicationModelConvention
{
private readonly Type _controllerType;
private readonly IParameterTransformer _parameterTransformer;
public ControllerRouteTokenTransformerConvention(Type controllerType, IParameterTransformer parameterTransformer)
{
if (parameterTransformer == null)
{
throw new ArgumentNullException(nameof(parameterTransformer));
}
_controllerType = controllerType;
_parameterTransformer = parameterTransformer;
}
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers.Where(c => c.ControllerType == _controllerType))
{
foreach (var action in controller.Actions)
{
action.Properties[typeof(IParameterTransformer)] = _parameterTransformer;
}
}
}
}
}

View File

@ -0,0 +1,27 @@
// 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.Mvc;
namespace RoutingWebSite
{
public class ConventionalTransformerController : Controller
{
private readonly TestResponseGenerator _generator;
public ConventionalTransformerController(TestResponseGenerator generator)
{
_generator = generator;
}
public IActionResult Index()
{
return _generator.Generate();
}
public IActionResult Param(string param)
{
return _generator.Generate($"/ConventionalTransformerRoute/_ConventionalTransformer_/Param/{param}");
}
}
}

View File

@ -0,0 +1,36 @@
// 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.Mvc;
namespace RoutingWebSite
{
[Route("/{controller:test-transformer}")]
public class EndpointRoutingController : Controller
{
private readonly TestResponseGenerator _generator;
public EndpointRoutingController(TestResponseGenerator generator)
{
_generator = generator;
}
[Route("/{controller}/{action=Index}")]
public IActionResult Index()
{
return _generator.Generate("/EndpointRouting/Index", "/EndpointRouting");
}
[Route("/{controller:test-transformer}/{action}")]
public IActionResult ParameterTransformer()
{
return _generator.Generate("/_EndpointRouting_/ParameterTransformer");
}
[Route("{id}")]
public IActionResult Get(int id)
{
return _generator.Generate("/_EndpointRouting_/" + id);
}
}
}

View File

@ -3,7 +3,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace RoutingWebSite.Controllers namespace RoutingWebSite
{ {
[Route("Order/[action]/{orderId?}", Name = "Order_[action]")] [Route("Order/[action]/{orderId?}", Name = "Order_[action]")]
public class OrderController : Controller public class OrderController : Controller

View File

@ -0,0 +1,27 @@
// 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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace RoutingWebSite
{
[Route("[controller]/[action]", Name = "[controller]_[action]")]
public class ParameterTransformerController : Controller
{
private readonly TestResponseGenerator _generator;
public ParameterTransformerController(TestResponseGenerator generator)
{
_generator = generator;
}
public IActionResult Test()
{
return _generator.Generate("/_ParameterTransformer_/_Test_");
}
}
}

View File

@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
namespace RoutingWebSite.Controllers namespace RoutingWebSite
{ {
public class RouteDataController : Controller public class RouteDataController : Controller
{ {

View File

@ -0,0 +1,40 @@
// 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.Linq;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
namespace RoutingWebSite
{
public class RemoveControllerActionDescriptorProvider : IActionDescriptorProvider
{
private readonly Type _controllerType;
public RemoveControllerActionDescriptorProvider(Type controllerType)
{
_controllerType = controllerType;
}
public int Order => int.MaxValue;
public void OnProvidersExecuted(ActionDescriptorProviderContext context)
{
}
public void OnProvidersExecuting(ActionDescriptorProviderContext context)
{
foreach (var item in context.Results.ToList())
{
if (item is ControllerActionDescriptor controllerActionDescriptor)
{
if (controllerActionDescriptor.ControllerTypeInfo == _controllerType)
{
context.Results.Remove(item);
}
}
}
}
}
}

View File

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace RoutingWebSite namespace RoutingWebSite
@ -14,8 +15,20 @@ namespace RoutingWebSite
// Set up application services // Set up application services
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddMvc() services
.AddMvc(options =>
{
// Add route token transformer to one controller
options.Conventions.Add(new ControllerRouteTokenTransformerConvention(
typeof(ParameterTransformerController),
new TestParameterTransformer()));
})
.SetCompatibilityVersion(CompatibilityVersion.Latest); .SetCompatibilityVersion(CompatibilityVersion.Latest);
services
.AddRouting(options =>
{
options.ConstraintMap["test-transformer"] = typeof(TestParameterTransformer);
});
services.AddScoped<TestResponseGenerator>(); services.AddScoped<TestResponseGenerator>();
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>(); services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
@ -32,6 +45,12 @@ namespace RoutingWebSite
constraints: new { controller = "DataTokens" }, constraints: new { controller = "DataTokens" },
dataTokens: new { hasDataTokens = true }); dataTokens: new { hasDataTokens = true });
routes.MapRoute(
"ConventionalTransformerRoute",
"ConventionalTransformerRoute/{controller:test-transformer}/{action=Index}/{param:test-transformer?}",
defaults: null,
constraints: new { controller = "ConventionalTransformer" });
routes.MapAreaRoute( routes.MapAreaRoute(
"flightRoute", "flightRoute",
"adminRoute", "adminRoute",

View File

@ -1,11 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace RoutingWebSite namespace RoutingWebSite
{ {
@ -14,11 +19,17 @@ namespace RoutingWebSite
// Set up application services // Set up application services
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddMvc() services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1); .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddScoped<TestResponseGenerator>(); services.AddScoped<TestResponseGenerator>();
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>(); services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
// EndpointRoutingController is not compatible with old routing
// Remove its action to avoid errors
var actionDescriptorProvider = new RemoveControllerActionDescriptorProvider(typeof(EndpointRoutingController));
services.TryAddEnumerable(ServiceDescriptor.Singleton<IActionDescriptorProvider>(actionDescriptorProvider));
} }
public void Configure(IApplicationBuilder app) public void Configure(IApplicationBuilder app)

View File

@ -0,0 +1,15 @@
// 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.Routing;
namespace RoutingWebSite
{
public class TestParameterTransformer : IParameterTransformer
{
public string Transform(string value)
{
return "_" + value + "_";
}
}
}