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(
actionDescriptorCollectionProvider,
new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())));
new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
new MockParameterPolicyFactory());
return dataSource;
}

View File

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

View File

@ -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
{
/// <summary>

View File

@ -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<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 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 == '[')

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.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)

View File

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

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.Routing;
using Xunit;
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}.{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<RouteOptions>(routeOptionsSetup.Configure);
services.Configure<RouteOptions>(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<IActionInvokerProvider>())));
mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
serviceProvider.GetRequiredService<ParameterPolicyFactory>());
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<RouteOptions>(routeOptionsSetup.Configure);
var routeOptionsSetup = new MvcCoreRouteOptionsSetup();
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>();

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]
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<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;
namespace RoutingWebSite.Controllers
namespace RoutingWebSite
{
[Route("Order/[action]/{orderId?}", Name = "Order_[action]")]
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.Filters;
namespace RoutingWebSite.Controllers
namespace RoutingWebSite
{
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.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<TestResponseGenerator>();
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
@ -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",

View File

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

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 + "_";
}
}
}