From dfae9c208a6752c8fed9c489d4c8848f02be35ec Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 12 Sep 2018 21:46:41 +1200 Subject: [PATCH] Add IParameterTransformer support (#8329) --- .../MvcEndpointDatasourceBenchmark.cs | 3 +- build/dependencies.props | 4 +- .../Routing/AttributeRouteInfo.cs | 2 + .../ApplicationModels/AttributeRouteModel.cs | 11 + .../ParameterTransformerConvention.cs | 41 +++ .../ControllerActionDescriptorBuilder.cs | 10 +- .../Internal/MvcEndpointDataSource.cs | 29 +- .../AttributeRouteModelTests.cs | 1 + .../RouteTokenTransformerConventionTest.cs | 101 +++++++ .../Internal/MvcEndpointDataSourceTests.cs | 38 ++- .../EndpointRoutingTest.cs | 278 ++++++++++++++++++ ...ntrollerRouteTokenTransformerConvention.cs | 38 +++ .../ConventionalTransformerController.cs | 27 ++ .../Controllers/EndpointRoutingController.cs | 36 +++ .../Controllers/OrderController.cs | 2 +- .../ParameterTransformerController.cs | 27 ++ .../Controllers/RouteDataController.cs | 2 +- ...emoveControllerActionDescriptorProvider.cs | 40 +++ test/WebSites/RoutingWebSite/Startup.cs | 21 +- .../RoutingWebSite/StartupWith21Compat.cs | 13 +- .../TestParameterTransformer.cs | 15 + 21 files changed, 723 insertions(+), 16 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ParameterTransformerConvention.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/RouteTokenTransformerConventionTest.cs create mode 100644 test/WebSites/RoutingWebSite/ControllerRouteTokenTransformerConvention.cs create mode 100644 test/WebSites/RoutingWebSite/Controllers/ConventionalTransformerController.cs create mode 100644 test/WebSites/RoutingWebSite/Controllers/EndpointRoutingController.cs create mode 100644 test/WebSites/RoutingWebSite/Controllers/ParameterTransformerController.cs create mode 100644 test/WebSites/RoutingWebSite/RemoveControllerActionDescriptorProvider.cs create mode 100644 test/WebSites/RoutingWebSite/TestParameterTransformer.cs diff --git a/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs index 62b520113b..028293dd28 100644 --- a/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs @@ -110,7 +110,8 @@ namespace Microsoft.AspNetCore.Mvc.Performance { var dataSource = new MvcEndpointDataSource( actionDescriptorCollectionProvider, - new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty()))); + new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty())), + new MockParameterPolicyFactory()); return dataSource; } diff --git a/build/dependencies.props b/build/dependencies.props index fd1818d1af..d5a1e4682b 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -48,8 +48,8 @@ 2.2.0-preview3-35202 2.2.0-preview3-35202 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 + 2.2.0-a-preview3-parameter-transformers-16968 + 2.2.0-a-preview3-parameter-transformers-16968 2.2.0-preview3-35202 2.2.0-preview3-35202 2.2.0-preview3-35202 diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/Routing/AttributeRouteInfo.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/Routing/AttributeRouteInfo.cs index 5531b44ced..f2fc4ea77b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/Routing/AttributeRouteInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/Routing/AttributeRouteInfo.cs @@ -1,6 +1,8 @@ // 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 Microsoft.AspNetCore.Mvc.Routing { /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs index 07e6a20c48..3283a630a0 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Mvc.ApplicationModels { @@ -220,6 +221,11 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels } public static string ReplaceTokens(string template, IDictionary values) + { + return ReplaceTokens(template, values, routeTokenTransformer: null); + } + + public static string ReplaceTokens(string template, IDictionary values, IParameterTransformer routeTokenTransformer) { var builder = new StringBuilder(); var state = TemplateParserState.Plaintext; @@ -371,6 +377,11 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels throw new InvalidOperationException(message); } + if (routeTokenTransformer != null) + { + value = routeTokenTransformer.Transform(value); + } + builder.Append(value); if (c == '[') diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ParameterTransformerConvention.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ParameterTransformerConvention.cs new file mode 100644 index 0000000000..d7645077af --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ParameterTransformerConvention.cs @@ -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 +{ + /// + /// An that sets attribute routing token replacement + /// to use the specified on selectors. + /// + public class RouteTokenTransformerConvention : IActionModelConvention + { + private readonly IParameterTransformer _parameterTransformer; + + /// + /// Creates a new instance of with the specified . + /// + /// The to use with attribute routing token replacement. + 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; + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs index 4c7e4aa8f2..174457bbcc 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; using Resources = Microsoft.AspNetCore.Mvc.Core.Resources; namespace Microsoft.AspNetCore.Mvc.Internal @@ -389,15 +390,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal { try { + actionDescriptor.Properties.TryGetValue(typeof(IParameterTransformer), out var transformer); + var routeTokenTransformer = transformer as IParameterTransformer; + actionDescriptor.AttributeRouteInfo.Template = AttributeRouteModel.ReplaceTokens( actionDescriptor.AttributeRouteInfo.Template, - actionDescriptor.RouteValues); + actionDescriptor.RouteValues, + routeTokenTransformer); if (actionDescriptor.AttributeRouteInfo.Name != null) { actionDescriptor.AttributeRouteInfo.Name = AttributeRouteModel.ReplaceTokens( actionDescriptor.AttributeRouteInfo.Name, - actionDescriptor.RouteValues); + actionDescriptor.RouteValues, + routeTokenTransformer); } } catch (InvalidOperationException ex) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs index 82026635ca..7f45a49b09 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs @@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal { private readonly IActionDescriptorCollectionProvider _actions; private readonly MvcEndpointInvokerFactory _invokerFactory; + private readonly ParameterPolicyFactory _parameterPolicyFactory; // The following are protected by this lock for WRITES only. This pattern is similar // to DefaultActionDescriptorChangeProvider - see comments there for details on @@ -33,7 +34,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal public MvcEndpointDataSource( IActionDescriptorCollectionProvider actions, - MvcEndpointInvokerFactory invokerFactory) + MvcEndpointInvokerFactory invokerFactory, + ParameterPolicyFactory parameterPolicyFactory) { if (actions == null) { @@ -45,8 +47,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal throw new ArgumentNullException(nameof(invokerFactory)); } + if (parameterPolicyFactory == null) + { + throw new ArgumentNullException(nameof(parameterPolicyFactory)); + } + _actions = actions; _invokerFactory = invokerFactory; + _parameterPolicyFactory = parameterPolicyFactory; ConventionalEndpointInfos = new List(); @@ -253,9 +261,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal { segmentParts = segment.Parts.ToList(); } + if (allParameterPolicies == null) + { + allParameterPolicies = MvcEndpointInfo.BuildParameterPolicies(routePattern.Parameters, _parameterPolicyFactory); + } 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); } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/AttributeRouteModelTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/AttributeRouteModelTests.cs index c7a578850b..dc312a50bd 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/AttributeRouteModelTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/AttributeRouteModelTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using Microsoft.AspNetCore.Routing; using Xunit; namespace Microsoft.AspNetCore.Mvc.ApplicationModels diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/RouteTokenTransformerConventionTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/RouteTokenTransformerConventionTest.cs new file mode 100644 index 0000000000..243ce755c6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/RouteTokenTransformerConventionTest.cs @@ -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()); + 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()); + 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()); + 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; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs index 03d280fdef..21fd57a11b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs @@ -161,6 +161,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal {"{controller}/{action=TestAction}/{id?}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{id?}/{*catchAll}" }}, {"{controller}/{action}.{ext?}", new[] { "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; @@ -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=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:upper-case}/{controller}/{action}/{id?}", new[] { "TESTAREA/TestController/TestAction/{id?}" })] public void Endpoints_AreaSingleAction(string endpointInfoRoute, string[] finalEndpointTemplates) { // Arrange @@ -230,6 +232,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); services.Configure(routeOptionsSetup.Configure); + services.Configure(options => + { + options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); + }); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute, serviceProvider: services.BuildServiceProvider())); @@ -377,6 +383,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal [Theory] [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)):upper-case}", new[] { "TestController1/TESTACTION1", "TestController1/TESTACTION2", "TestController2/TESTACTION1" })] public void Endpoints_MultipleActions(string endpointInfoRoute, string[] finalEndpointTemplates) { // Arrange @@ -750,14 +757,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal var services = new ServiceCollection(); services.AddSingleton(actionDescriptorCollectionProvider); + services.AddRouting(options => + { + options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); + }); + var serviceProvider = services.BuildServiceProvider(); var dataSource = new MvcEndpointDataSource( actionDescriptorCollectionProvider, - mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty()))); + mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty())), + serviceProvider.GetRequiredService()); return dataSource; } + private class UpperCaseParameterTransform : IParameterTransformer + { + public string Transform(string value) + { + return value?.ToUpperInvariant(); + } + } + private MvcEndpointInfo CreateEndpointInfo( string name, string template, @@ -768,13 +789,18 @@ namespace Microsoft.AspNetCore.Mvc.Internal { if (serviceProvider == null) { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddRouting(); + var services = new ServiceCollection(); + services.AddRouting(); + services.AddSingleton(typeof(UpperCaseParameterTransform), new UpperCaseParameterTransform()); - var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); - serviceCollection.Configure(routeOptionsSetup.Configure); + var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); + services.Configure(routeOptionsSetup.Configure); + services.Configure(options => + { + options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); + }); - serviceProvider = serviceCollection.BuildServiceProvider(); + serviceProvider = services.BuildServiceProvider(); } var parameterPolicyFactory = serviceProvider.GetRequiredService(); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/EndpointRoutingTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/EndpointRoutingTest.cs index f82be33703..73cab5f469 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/EndpointRoutingTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/EndpointRoutingTest.cs @@ -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(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(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(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(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(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(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(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(body); + + Assert.Equal("EndpointRouting", result.Controller); + Assert.Equal("ParameterTransformer", result.Action); + + Assert.Equal("/", result.Link); + } + [Fact] public async override Task HasEndpointMatch() { @@ -123,5 +287,119 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests // Assert 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(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(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(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(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(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(body); + + Assert.Equal("ConventionalTransformer", result.Controller); + Assert.Equal("Index", result.Action); + Assert.Equal("/ConventionalTransformerRoute/_ConventionalTransformer_", result.Link); + } } } diff --git a/test/WebSites/RoutingWebSite/ControllerRouteTokenTransformerConvention.cs b/test/WebSites/RoutingWebSite/ControllerRouteTokenTransformerConvention.cs new file mode 100644 index 0000000000..37dca3f259 --- /dev/null +++ b/test/WebSites/RoutingWebSite/ControllerRouteTokenTransformerConvention.cs @@ -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; + } + } + } + } +} diff --git a/test/WebSites/RoutingWebSite/Controllers/ConventionalTransformerController.cs b/test/WebSites/RoutingWebSite/Controllers/ConventionalTransformerController.cs new file mode 100644 index 0000000000..b31b2ee8b9 --- /dev/null +++ b/test/WebSites/RoutingWebSite/Controllers/ConventionalTransformerController.cs @@ -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}"); + } + } +} diff --git a/test/WebSites/RoutingWebSite/Controllers/EndpointRoutingController.cs b/test/WebSites/RoutingWebSite/Controllers/EndpointRoutingController.cs new file mode 100644 index 0000000000..1163302277 --- /dev/null +++ b/test/WebSites/RoutingWebSite/Controllers/EndpointRoutingController.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RoutingWebSite/Controllers/OrderController.cs b/test/WebSites/RoutingWebSite/Controllers/OrderController.cs index 6f3f49ef2c..f0770ab539 100644 --- a/test/WebSites/RoutingWebSite/Controllers/OrderController.cs +++ b/test/WebSites/RoutingWebSite/Controllers/OrderController.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc; -namespace RoutingWebSite.Controllers +namespace RoutingWebSite { [Route("Order/[action]/{orderId?}", Name = "Order_[action]")] public class OrderController : Controller diff --git a/test/WebSites/RoutingWebSite/Controllers/ParameterTransformerController.cs b/test/WebSites/RoutingWebSite/Controllers/ParameterTransformerController.cs new file mode 100644 index 0000000000..69991c60b5 --- /dev/null +++ b/test/WebSites/RoutingWebSite/Controllers/ParameterTransformerController.cs @@ -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_"); + } + } +} diff --git a/test/WebSites/RoutingWebSite/Controllers/RouteDataController.cs b/test/WebSites/RoutingWebSite/Controllers/RouteDataController.cs index e02f385707..97f49e00c4 100644 --- a/test/WebSites/RoutingWebSite/Controllers/RouteDataController.cs +++ b/test/WebSites/RoutingWebSite/Controllers/RouteDataController.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; -namespace RoutingWebSite.Controllers +namespace RoutingWebSite { public class RouteDataController : Controller { diff --git a/test/WebSites/RoutingWebSite/RemoveControllerActionDescriptorProvider.cs b/test/WebSites/RoutingWebSite/RemoveControllerActionDescriptorProvider.cs new file mode 100644 index 0000000000..40d40aa355 --- /dev/null +++ b/test/WebSites/RoutingWebSite/RemoveControllerActionDescriptorProvider.cs @@ -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); + } + } + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/RoutingWebSite/Startup.cs b/test/WebSites/RoutingWebSite/Startup.cs index 08a67d963f..653aa4e81f 100644 --- a/test/WebSites/RoutingWebSite/Startup.cs +++ b/test/WebSites/RoutingWebSite/Startup.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; namespace RoutingWebSite @@ -14,8 +15,20 @@ namespace RoutingWebSite // Set up application 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); + services + .AddRouting(options => + { + options.ConstraintMap["test-transformer"] = typeof(TestParameterTransformer); + }); services.AddScoped(); services.AddSingleton(); @@ -32,6 +45,12 @@ namespace RoutingWebSite constraints: new { controller = "DataTokens" }, dataTokens: new { hasDataTokens = true }); + routes.MapRoute( + "ConventionalTransformerRoute", + "ConventionalTransformerRoute/{controller:test-transformer}/{action=Index}/{param:test-transformer?}", + defaults: null, + constraints: new { controller = "ConventionalTransformer" }); + routes.MapAreaRoute( "flightRoute", "adminRoute", diff --git a/test/WebSites/RoutingWebSite/StartupWith21Compat.cs b/test/WebSites/RoutingWebSite/StartupWith21Compat.cs index eb891a53c5..44a6abfc61 100644 --- a/test/WebSites/RoutingWebSite/StartupWith21Compat.cs +++ b/test/WebSites/RoutingWebSite/StartupWith21Compat.cs @@ -1,11 +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.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace RoutingWebSite { @@ -14,11 +19,17 @@ namespace RoutingWebSite // Set up application services public void ConfigureServices(IServiceCollection services) { - services.AddMvc() + services + .AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.AddScoped(); services.AddSingleton(); + + // EndpointRoutingController is not compatible with old routing + // Remove its action to avoid errors + var actionDescriptorProvider = new RemoveControllerActionDescriptorProvider(typeof(EndpointRoutingController)); + services.TryAddEnumerable(ServiceDescriptor.Singleton(actionDescriptorProvider)); } public void Configure(IApplicationBuilder app) diff --git a/test/WebSites/RoutingWebSite/TestParameterTransformer.cs b/test/WebSites/RoutingWebSite/TestParameterTransformer.cs new file mode 100644 index 0000000000..0af1f4ed7e --- /dev/null +++ b/test/WebSites/RoutingWebSite/TestParameterTransformer.cs @@ -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 + "_"; + } + } +} \ No newline at end of file