Add PageRouteTransformerConvention (#8541)

This commit is contained in:
James Newton-King 2018-10-04 17:34:26 +13:00 committed by GitHub
parent 7854d65c11
commit 94101a9cde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 258 additions and 21 deletions

View File

@ -8,7 +8,8 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
/// <summary>
/// An <see cref="IActionModelConvention"/> that sets attribute routing token replacement
/// to use the specified <see cref="IOutboundParameterTransformer"/> on <see cref="ActionModel"/> selectors.
/// to use the specified <see cref="IOutboundParameterTransformer"/> on <see cref="ActionModel"/>.
/// This convention does not effect Razor page routes.
/// </summary>
public class RouteTokenTransformerConvention : IActionModelConvention
{

View File

@ -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
{
/// <summary>
/// An <see cref="IPageRouteModelConvention"/> that sets page route resolution
/// to use the specified <see cref="IOutboundParameterTransformer"/> on <see cref="PageRouteModel"/>.
/// This convention does not effect controller action routes.
/// </summary>
public class PageRouteTransformerConvention : IPageRouteModelConvention
{
private IOutboundParameterTransformer _parameterTransformer;
/// <summary>
/// Creates a new instance of <see cref="PageRouteTransformerConvention"/> with the specified <see cref="IOutboundParameterTransformer"/>.
/// </summary>
/// <param name="parameterTransformer">The <see cref="IOutboundParameterTransformer"/> to use resolve page routes.</param>
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;
}
}

View File

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

View File

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

View File

@ -159,6 +159,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
AttributeRouteModel = new AttributeRouteModel
{
Template = AttributeRouteModel.CombineTemplates(prefix, routeTemplate),
},
EndpointMetadata =
{
new PageRouteMetadata(prefix, routeTemplate)
}
};
}

View File

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

View File

@ -158,6 +158,18 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("/LGPage?another-value=4", responseContent);
}
[Fact]
public async Task GetPathByPage_CanGeneratePathToPage_PathTransformed()
{
// Act
var response = await Client.GetAsync("LG1/LinkToPageWithTransformedPath?id=HelloWorld");
var responseContent = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("/page-route-transformer/test-page/ExtraPath/HelloWorld", responseContent);
}
[Fact]
public async Task GetPathByPage_CanGeneratePathToPageInArea()
{

View File

@ -115,6 +115,38 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Empty(result);
}
[Fact]
public async Task Page_PageRouteTransformer()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/page-route-transformer/index");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task Page_PageRouteTransformer_WithoutIndex()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/page-route-transformer");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task Page_PageRouteTransformer_RouteParameter()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/page-route-transformer/test-page/ExtraPath/World");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Equal("Hello from World", body);
}
[Fact]
public virtual async Task ConventionalRoutedController_ActionIsReachable()
{

View File

@ -0,0 +1,67 @@
// 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.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Routing;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Test.ApplicationModels
{
public class PageRouteTransformerConventionTest
{
[Fact]
public void Apply_SetTransformer()
{
// Arrange
var transformer = new TestParameterTransformer();
var convention = new PageRouteTransformerConvention(transformer);
var model = new PageRouteModel(string.Empty, string.Empty);
// Act
convention.Apply(model);
// Assert
Assert.True(model.Properties.TryGetValue(typeof(IOutboundParameterTransformer), out var routeTokenTransformer));
Assert.Equal(transformer, routeTokenTransformer);
}
[Fact]
public void Apply_ShouldApplyFalse_NoOp()
{
// Arrange
var transformer = new TestParameterTransformer();
var convention = new CustomPageRouteTransformerConvention(transformer);
var model = new PageRouteModel(string.Empty, string.Empty);
// Act
convention.Apply(model);
// Assert
Assert.False(model.Properties.TryGetValue(typeof(IOutboundParameterTransformer), out _));
}
private class TestParameterTransformer : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
{
return value?.ToString();
}
}
private class CustomPageRouteTransformerConvention : PageRouteTransformerConvention
{
public CustomPageRouteTransformerConvention(IOutboundParameterTransformer parameterTransformer) : base(parameterTransformer)
{
}
protected override bool ShouldApply(PageRouteModel action)
{
return false;
}
}
}
}

View File

@ -59,6 +59,14 @@ namespace RoutingWebSite
values: QueryToRouteValues(HttpContext.Request.Query));
}
public string LinkToPageWithTransformedPath()
{
return _linkGenerator.GetPathByPage(
HttpContext,
page: "/PageRouteTransformer/TestPage",
values: QueryToRouteValues(HttpContext.Request.Query));
}
public string LinkToPageInArea()
{
var values = QueryToRouteValues(HttpContext.Request.Query);

View File

@ -0,0 +1,3 @@
@page
@{
}

View File

@ -0,0 +1,4 @@
@page "ExtraPath/{id?}"
@{
}
Hello from @ViewContext.RouteData.Values["id"]

View File

@ -4,6 +4,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
@ -15,6 +16,8 @@ namespace RoutingWebSite
// Set up application services
public void ConfigureServices(IServiceCollection services)
{
var pageRouteTransformerConvention = new PageRouteTransformerConvention(new SlugifyParameterTransformer());
services
.AddMvc(options =>
{
@ -23,6 +26,13 @@ namespace RoutingWebSite
typeof(ParameterTransformerController),
new SlugifyParameterTransformer()));
})
.AddRazorPagesOptions(options =>
{
options.Conventions.AddFolderRouteModelConvention("/PageRouteTransformer", model =>
{
pageRouteTransformerConvention.Apply(model);
});
})
.SetCompatibilityVersion(CompatibilityVersion.Latest);
services
.AddRouting(options =>

View File

@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
@ -13,8 +14,17 @@ namespace RoutingWebSite
{
public void ConfigureServices(IServiceCollection services)
{
var pageRouteTransformerConvention = new PageRouteTransformerConvention(new SlugifyParameterTransformer());
services
.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AddFolderRouteModelConvention("/PageRouteTransformer", model =>
{
pageRouteTransformerConvention.Apply(model);
});
})
.SetCompatibilityVersion(CompatibilityVersion.Latest);
services
.AddRouting(options =>

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
@ -19,8 +20,17 @@ namespace RoutingWebSite
// Set up application services
public void ConfigureServices(IServiceCollection services)
{
var pageRouteTransformerConvention = new PageRouteTransformerConvention(new SlugifyParameterTransformer());
services
.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AddFolderRouteModelConvention("/PageRouteTransformer", model =>
{
pageRouteTransformerConvention.Apply(model);
});
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddScoped<TestResponseGenerator>();