diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/RouteTokenTransformerConvention.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/RouteTokenTransformerConvention.cs
index c83ba6cc47..004e4a22c3 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/RouteTokenTransformerConvention.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/RouteTokenTransformerConvention.cs
@@ -8,7 +8,8 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
///
/// An that sets attribute routing token replacement
- /// to use the specified on selectors.
+ /// to use the specified on .
+ /// This convention does not effect Razor page routes.
///
public class RouteTokenTransformerConvention : IActionModelConvention
{
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteTransformerConvention.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteTransformerConvention.cs
new file mode 100644
index 0000000000..df47d3b7c2
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteTransformerConvention.cs
@@ -0,0 +1,42 @@
+// 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 page route resolution
+ /// to use the specified on .
+ /// This convention does not effect controller action routes.
+ ///
+ public class PageRouteTransformerConvention : IPageRouteModelConvention
+ {
+ private IOutboundParameterTransformer _parameterTransformer;
+
+ ///
+ /// Creates a new instance of with the specified .
+ ///
+ /// The to use resolve page routes.
+ public PageRouteTransformerConvention(IOutboundParameterTransformer parameterTransformer)
+ {
+ if (parameterTransformer == null)
+ {
+ throw new ArgumentNullException(nameof(parameterTransformer));
+ }
+
+ _parameterTransformer = parameterTransformer;
+ }
+
+ public void Apply(PageRouteModel model)
+ {
+ if (ShouldApply(model))
+ {
+ model.Properties[typeof(IOutboundParameterTransformer)] = _parameterTransformer;
+ }
+ }
+
+ protected virtual bool ShouldApply(PageRouteModel action) => true;
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs
index 06bc6f52eb..71a826c857 100644
--- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs
@@ -4,10 +4,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.Mvc.Internal;
+using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
using Microsoft.AspNetCore.Mvc.Routing;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
@@ -81,7 +86,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
Name = selector.AttributeRouteModel.Name,
Order = selector.AttributeRouteModel.Order ?? 0,
- Template = selector.AttributeRouteModel.Template,
+ Template = TransformPageRoute(model, selector),
SuppressLinkGeneration = selector.AttributeRouteModel.SuppressLinkGeneration,
SuppressPathMatching = selector.AttributeRouteModel.SuppressPathMatching,
},
@@ -109,5 +114,35 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
actions.Add(descriptor);
}
}
+
+ private static string TransformPageRoute(PageRouteModel model, SelectorModel selectorModel)
+ {
+ model.Properties.TryGetValue(typeof(IOutboundParameterTransformer), out var transformer);
+ var pageRouteTransformer = transformer as IOutboundParameterTransformer;
+
+ // Transformer not set on page route
+ if (pageRouteTransformer == null)
+ {
+ return selectorModel.AttributeRouteModel.Template;
+ }
+
+ var pageRouteMetadata = selectorModel.EndpointMetadata.OfType().SingleOrDefault();
+ if (pageRouteMetadata == null)
+ {
+ // Selector does not have expected metadata. Should never reach here
+ throw new InvalidOperationException("Page selector did not have page route metadata.");
+ }
+
+ var segments = pageRouteMetadata.PageRoute.Split('/');
+ for (var i = 0; i < segments.Length; i++)
+ {
+ segments[i] = pageRouteTransformer.TransformOutbound(segments[i]);
+ }
+
+ var transformedPageRoute = string.Join("/", segments);
+
+ // Combine transformed page route with template
+ return AttributeRouteModel.CombineTemplates(transformedPageRoute, pageRouteMetadata.RouteTemplate);
+ }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteMetadata.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteMetadata.cs
new file mode 100644
index 0000000000..ec5390ea98
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteMetadata.cs
@@ -0,0 +1,18 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
+{
+ // This is used to store the uncombined parts of the final page route
+ internal class PageRouteMetadata
+ {
+ public PageRouteMetadata(string pageRoute, string routeTemplate)
+ {
+ PageRoute = pageRoute;
+ RouteTemplate = routeTemplate;
+ }
+
+ public string PageRoute { get; }
+ public string RouteTemplate { get; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs
index e3d2d6289a..63f06ded68 100644
--- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs
@@ -159,6 +159,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
AttributeRouteModel = new AttributeRouteModel
{
Template = AttributeRouteModel.CombineTemplates(prefix, routeTemplate),
+ },
+ EndpointMetadata =
+ {
+ new PageRouteMetadata(prefix, routeTemplate)
}
};
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/RouteTokenTransformerConventionTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/RouteTokenTransformerConventionTest.cs
index 807bb4d41a..52214026ef 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/RouteTokenTransformerConventionTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/RouteTokenTransformerConventionTest.cs
@@ -11,25 +11,6 @@ namespace Microsoft.AspNetCore.Mvc.Test.ApplicationModels
{
public class RouteTokenTransformerConventionTest
{
- [Fact]
- public void Apply_NullAttributeRouteModel_NoOp()
- {
- // Arrange
- var convention = new RouteTokenTransformerConvention(new TestParameterTransformer());
-
- var model = new ActionModel(GetMethodInfo(), Array.Empty