Move convention extensions from RazorPagesOptions to PageConventionCollectionsExtensions

Fixes #6462
This commit is contained in:
Pranav K 2017-06-29 16:36:26 -07:00
parent 660b7df0e2
commit bc86ea4e47
18 changed files with 660 additions and 429 deletions

View File

@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
/// <summary> /// <summary>
/// Allows customization of the of the <see cref="PageApplicationModel"/>. /// Allows customization of the of the <see cref="PageApplicationModel"/>.
/// </summary> /// </summary>
public interface IPageApplicationModelConvention public interface IPageApplicationModelConvention : IPageConvention
{ {
/// <summary> /// <summary>
/// Called to apply the convention to the <see cref="PageApplicationModel"/>. /// Called to apply the convention to the <see cref="PageApplicationModel"/>.

View File

@ -0,0 +1,12 @@
// 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.ApplicationModels
{
/// <summary>
/// Common interface for route and application model conventions that apply to Razor Pages.
/// </summary>
public interface IPageConvention
{
}
}

View File

@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
/// <summary> /// <summary>
/// Allows customization of the of the <see cref="PageRouteModel"/>. /// Allows customization of the of the <see cref="PageRouteModel"/>.
/// </summary> /// </summary>
public interface IPageRouteModelConvention public interface IPageRouteModelConvention : IPageConvention
{ {
/// <summary> /// <summary>
/// Called to apply the convention to the <see cref="PageRouteModel"/>. /// Called to apply the convention to the <see cref="PageRouteModel"/>.

View File

@ -0,0 +1,222 @@
// 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.ObjectModel;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
public class PageConventionCollection : Collection<IPageConvention>
{
/// <summary>
/// Creates and adds an <see cref="IPageApplicationModelConvention"/> that invokes an action on the
/// <see cref="PageApplicationModel"/> for the page with the speciifed name.
/// </summary>
/// <param name="pageName">The name of the page e.g. <c>/Users/List</c></param>
/// <param name="action">The <see cref="Action"/>.</param>
/// <returns>The added <see cref="IPageApplicationModelConvention"/>.</returns>
public IPageApplicationModelConvention AddPageApplicationModelConvention(
string pageName,
Action<PageApplicationModel> action)
{
EnsureValidPageName(pageName);
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
return Add(new PageApplicationModelConvention(pageName, action));
}
/// <summary>
/// Creates and adds an <see cref="IPageApplicationModelConvention"/> that invokes an action on
/// <see cref="PageApplicationModel"/> instances for all page under the specified folder.
/// </summary>
/// <param name="folderPath">The path of the folder relative to the Razor Pages root. e.g. <c>/Users/</c></param>
/// <param name="action">The <see cref="Action"/>.</param>
/// <returns>The added <see cref="IPageApplicationModelConvention"/>.</returns>
public IPageApplicationModelConvention AddFolderApplicationModelConvention(string folderPath, Action<PageApplicationModel> action)
{
EnsureValidFolderPath(folderPath);
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
return Add(new FolderApplicationModelConvention(folderPath, action));
}
/// <summary>
/// Creates and adds an <see cref="IPageRouteModelConvention"/> that invokes an action on the
/// <see cref="PageRouteModel"/> for the page with the speciifed name.
/// </summary>
/// <param name="pageName">The name of the page e.g. <c>/Users/List</c></param>
/// <param name="action">The <see cref="Action"/>.</param>
/// <returns>The added <see cref="IPageRouteModelConvention"/>.</returns>
public IPageRouteModelConvention AddPageRouteModelConvention(string pageName, Action<PageRouteModel> action)
{
EnsureValidPageName(pageName);
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
return Add(new PageRouteModelConvention(pageName, action));
}
/// <summary>
/// Creates and adds an <see cref="IPageRouteModelConvention"/> that invokes an action on
/// <see cref="PageRouteModel"/> instances for all page under the specified folder.
/// </summary>
/// <param name="folderPath">The path of the folder relative to the Razor Pages root. e.g. <c>/Users/</c></param>
/// <param name="action">The <see cref="Action"/>.</param>
/// <returns>The added <see cref="IPageApplicationModelConvention"/>.</returns>
public IPageRouteModelConvention AddFolderRouteModelConvention(string folderPath, Action<PageRouteModel> action)
{
EnsureValidFolderPath(folderPath);
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
return Add(new FolderRouteModelConvention(folderPath, action));
}
// Internal for unit testing
internal static void EnsureValidPageName(string pageName)
{
if (string.IsNullOrEmpty(pageName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName));
}
if (pageName[0] != '/' || pageName.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException(Resources.FormatInvalidValidPageName(pageName), nameof(pageName));
}
}
// Internal for unit testing
internal static void EnsureValidFolderPath(string folderPath)
{
if (string.IsNullOrEmpty(folderPath))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(folderPath));
}
if (folderPath[0] != '/')
{
throw new ArgumentException(Resources.PathMustBeRootRelativePath, nameof(folderPath));
}
}
private TConvention Add<TConvention>(TConvention convention) where TConvention: IPageConvention
{
base.Add(convention);
return convention;
}
private class PageRouteModelConvention : IPageRouteModelConvention
{
private readonly string _path;
private readonly Action<PageRouteModel> _action;
public PageRouteModelConvention(string path, Action<PageRouteModel> action)
{
_path = path;
_action = action;
}
public void Apply(PageRouteModel model)
{
if (string.Equals(model.ViewEnginePath, _path, StringComparison.OrdinalIgnoreCase))
{
_action(model);
}
}
}
private class FolderRouteModelConvention : IPageRouteModelConvention
{
private readonly string _folderPath;
private readonly Action<PageRouteModel> _action;
public FolderRouteModelConvention(string folderPath, Action<PageRouteModel> action)
{
_folderPath = folderPath.TrimEnd('/');
_action = action;
}
public void Apply(PageRouteModel model)
{
var viewEnginePath = model.ViewEnginePath;
if (PathBelongsToFolder(_folderPath, viewEnginePath))
{
_action(model);
}
}
}
private class PageApplicationModelConvention : IPageApplicationModelConvention
{
private readonly string _path;
private readonly Action<PageApplicationModel> _action;
public PageApplicationModelConvention(string path, Action<PageApplicationModel> action)
{
_path = path;
_action = action;
}
public void Apply(PageApplicationModel model)
{
if (string.Equals(model.ViewEnginePath, _path, StringComparison.OrdinalIgnoreCase))
{
_action(model);
}
}
}
private class FolderApplicationModelConvention : IPageApplicationModelConvention
{
private readonly string _folderPath;
private readonly Action<PageApplicationModel> _action;
public FolderApplicationModelConvention(string folderPath, Action<PageApplicationModel> action)
{
_folderPath = folderPath.TrimEnd('/');
_action = action;
}
public void Apply(PageApplicationModel model)
{
var viewEnginePath = model.ViewEnginePath;
if (PathBelongsToFolder(_folderPath, viewEnginePath))
{
_action(model);
}
}
}
// Internal for unit testing
internal static bool PathBelongsToFolder(string folderPath, string viewEnginePath)
{
if (folderPath == "/")
{
// Root directory covers everything.
return true;
}
return viewEnginePath.Length > folderPath.Length &&
viewEnginePath.StartsWith(folderPath, StringComparison.OrdinalIgnoreCase) &&
viewEnginePath[folderPath.Length] == '/';
}
}
}

View File

@ -57,7 +57,7 @@ namespace Microsoft.Extensions.DependencyInjection
if (rootDirectory[0] != '/') if (rootDirectory[0] != '/')
{ {
throw new ArgumentException(Resources.PathMustBeAnAppRelativePath, nameof(rootDirectory)); throw new ArgumentException(Resources.PathMustBeRootRelativePath, nameof(rootDirectory));
} }
builder.Services.Configure<RazorPagesOptions>(options => options.RootDirectory = rootDirectory); builder.Services.Configure<RazorPagesOptions>(options => options.RootDirectory = rootDirectory);

View File

@ -0,0 +1,222 @@
// 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.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extensions for <see cref="PageConventionCollection"/>.
/// </summary>
public static class PageConventionCollectionExtensions
{
/// <summary>
/// Configures the specified <paramref name="factory"/> to apply filters to all Razor Pages.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="factory">The factory to create filters.</param>
/// <returns></returns>
public static IPageApplicationModelConvention ConfigureFilter(
this PageConventionCollection conventions,
Func<PageApplicationModel, IFilterMetadata> factory)
{
if (conventions == null)
{
throw new ArgumentNullException(nameof(conventions));
}
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return conventions.AddFolderApplicationModelConvention("/", model => model.Filters.Add(factory(model)));
}
/// <summary>
/// Configures the specified <paramref name="filter"/> to apply to all Razor Pages.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="filter">The <see cref="IFilterMetadata"/> to add.</param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
public static PageConventionCollection ConfigureFilter(this PageConventionCollection conventions, IFilterMetadata filter)
{
if (conventions == null)
{
throw new ArgumentNullException(nameof(conventions));
}
if (filter == null)
{
throw new ArgumentNullException(nameof(filter));
}
conventions.AddFolderApplicationModelConvention("/", model => model.Filters.Add(filter));
return conventions;
}
/// <summary>
/// Adds a <see cref="AllowAnonymousFilter"/> to the page with the specified name.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="pageName">The page name.</param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
public static PageConventionCollection AllowAnonymousToPage(this PageConventionCollection conventions, string pageName)
{
if (conventions == null)
{
throw new ArgumentNullException(nameof(conventions));
}
if (string.IsNullOrEmpty(pageName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName));
}
var anonymousFilter = new AllowAnonymousFilter();
conventions.AddPageApplicationModelConvention(pageName, model => model.Filters.Add(anonymousFilter));
return conventions;
}
/// <summary>
/// Adds a <see cref="AllowAnonymousFilter"/> to all pages under the specified folder.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="folderPath">The folder path.</param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
public static PageConventionCollection AllowAnonymousToFolder(this PageConventionCollection conventions, string folderPath)
{
if (conventions == null)
{
throw new ArgumentNullException(nameof(conventions));
}
if (string.IsNullOrEmpty(folderPath))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(folderPath));
}
var anonymousFilter = new AllowAnonymousFilter();
conventions.AddFolderApplicationModelConvention(folderPath, model => model.Filters.Add(anonymousFilter));
return conventions;
}
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> with the specified policy to the page with the specified name.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="pageName">The page name.</param>
/// <param name="policy">The authorization policy.</param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
public static PageConventionCollection AuthorizePage(this PageConventionCollection conventions, string pageName, string policy)
{
if (conventions == null)
{
throw new ArgumentNullException(nameof(conventions));
}
if (string.IsNullOrEmpty(pageName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName));
}
var authorizeFilter = new AuthorizeFilter(policy);
conventions.AddPageApplicationModelConvention(pageName, model => model.Filters.Add(authorizeFilter));
return conventions;
}
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> to the page with the specified name.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="pageName">The page name.</param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
public static PageConventionCollection AuthorizePage(this PageConventionCollection conventions, string pageName) =>
AuthorizePage(conventions, pageName, policy: string.Empty);
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> with the specified policy to all pages under the specified folder.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="folderPath">The folder path.</param>
/// <param name="policy">The authorization policy.</param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
public static PageConventionCollection AuthorizeFolder(this PageConventionCollection conventions, string folderPath, string policy)
{
if (conventions == null)
{
throw new ArgumentNullException(nameof(conventions));
}
if (string.IsNullOrEmpty(folderPath))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(folderPath));
}
var authorizeFilter = new AuthorizeFilter(policy);
conventions.AddFolderApplicationModelConvention(folderPath, model => model.Filters.Add(authorizeFilter));
return conventions;
}
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> to all pages under the specified folder.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="folderPath">The folder path.</param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
public static PageConventionCollection AuthorizeFolder(this PageConventionCollection conventions, string folderPath) =>
AuthorizeFolder(conventions, folderPath, policy: string.Empty);
/// <summary>
/// Adds the specified <paramref name="route"/> to the page at the specified <paramref name="pageName"/>.
/// <para>
/// The page can be routed via <paramref name="route"/> in addition to the default set of path based routes.
/// All links generated for this page will use the specified route.
/// </para>
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/>.</param>
/// <param name="pageName">The page name.</param>
/// <param name="route">The route to associate with the page.</param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
public static PageConventionCollection AddPageRoute(this PageConventionCollection conventions, string pageName, string route)
{
if (conventions == null)
{
throw new ArgumentNullException(nameof(conventions));
}
if (string.IsNullOrEmpty(pageName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
conventions.AddPageRouteModelConvention(pageName, model =>
{
// Use the route specified in MapPageRoute for outbound routing.
foreach (var selector in model.Selectors)
{
selector.AttributeRouteModel.SuppressLinkGeneration = true;
}
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Template = route,
}
});
});
return conventions;
}
}
}

View File

@ -1,318 +0,0 @@
// 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.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extensions for <see cref="RazorPagesOptions"/>.
/// </summary>
public static class RazorPagesOptionsExtensions
{
/// <summary>
/// Configures the specified <paramref name="factory"/> to apply filters to all Razor Pages.
/// </summary>
/// <param name="options">The <see cref="RazorPagesOptions"/> to configure.</param>
/// <param name="factory">The factory to create filters.</param>
/// <returns></returns>
public static RazorPagesOptions ConfigureFilter(this RazorPagesOptions options, Func<PageApplicationModel, IFilterMetadata> factory)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
options.ApplicationModelConventions.Add(new FolderApplicationModelConvention("/", model => model.Filters.Add(factory(model))));
return options;
}
/// <summary>
/// Configures the specified <paramref name="filter"/> to apply to all Razor Pages.
/// </summary>
/// <param name="options">The <see cref="RazorPagesOptions"/> to configure.</param>
/// <param name="filter">The <see cref="IFilterMetadata"/> to add.</param>
/// <returns>The <see cref="RazorPagesOptions"/>.</returns>
public static RazorPagesOptions ConfigureFilter(this RazorPagesOptions options, IFilterMetadata filter)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (filter == null)
{
throw new ArgumentNullException(nameof(filter));
}
options.ApplicationModelConventions.Add(new FolderApplicationModelConvention("/", model => model.Filters.Add(filter)));
return options;
}
/// <summary>
/// Adds a <see cref="AllowAnonymousFilter"/> to the page with the specified name.
/// </summary>
/// <param name="options">The <see cref="RazorPagesOptions"/> to configure.</param>
/// <param name="pageName">The page name.</param>
/// <returns>The <see cref="RazorPagesOptions"/>.</returns>
public static RazorPagesOptions AllowAnonymousToPage(this RazorPagesOptions options, string pageName)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (string.IsNullOrEmpty(pageName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName));
}
var anonymousFilter = new AllowAnonymousFilter();
options.ApplicationModelConventions.Add(new PageApplicationModelConvention(pageName, model => model.Filters.Add(anonymousFilter)));
return options;
}
/// <summary>
/// Adds a <see cref="AllowAnonymousFilter"/> to all pages under the specified folder.
/// </summary>
/// <param name="options">The <see cref="RazorPagesOptions"/> to configure.</param>
/// <param name="folderPath">The folder path.</param>
/// <returns>The <see cref="RazorPagesOptions"/>.</returns>
public static RazorPagesOptions AllowAnonymousToFolder(this RazorPagesOptions options, string folderPath)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (string.IsNullOrEmpty(folderPath))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(folderPath));
}
var anonymousFilter = new AllowAnonymousFilter();
options.ApplicationModelConventions.Add(new FolderApplicationModelConvention(folderPath, model => model.Filters.Add(anonymousFilter)));
return options;
}
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> with the specified policy to the page with the specified name.
/// </summary>
/// <param name="options">The <see cref="RazorPagesOptions"/> to configure.</param>
/// <param name="pageName">The page name.</param>
/// <param name="policy">The authorization policy.</param>
/// <returns>The <see cref="RazorPagesOptions"/>.</returns>
public static RazorPagesOptions AuthorizePage(this RazorPagesOptions options, string pageName, string policy)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (string.IsNullOrEmpty(pageName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName));
}
var authorizeFilter = new AuthorizeFilter(policy);
options.ApplicationModelConventions.Add(new PageApplicationModelConvention(pageName, model => model.Filters.Add(authorizeFilter)));
return options;
}
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> to the page with the specified name.
/// </summary>
/// <param name="options">The <see cref="RazorPagesOptions"/> to configure.</param>
/// <param name="pageName">The page name.</param>
/// <returns>The <see cref="RazorPagesOptions"/>.</returns>
public static RazorPagesOptions AuthorizePage(this RazorPagesOptions options, string pageName) =>
AuthorizePage(options, pageName, policy: string.Empty);
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> with the specified policy to all pages under the specified folder.
/// </summary>
/// <param name="options">The <see cref="RazorPagesOptions"/> to configure.</param>
/// <param name="folderPath">The folder path.</param>
/// <param name="policy">The authorization policy.</param>
/// <returns>The <see cref="RazorPagesOptions"/>.</returns>
public static RazorPagesOptions AuthorizeFolder(this RazorPagesOptions options, string folderPath, string policy)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (string.IsNullOrEmpty(folderPath))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(folderPath));
}
var authorizeFilter = new AuthorizeFilter(policy);
options.ApplicationModelConventions.Add(new FolderApplicationModelConvention(folderPath, model => model.Filters.Add(authorizeFilter)));
return options;
}
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> to all pages under the specified folder.
/// </summary>
/// <param name="options">The <see cref="RazorPagesOptions"/> to configure.</param>
/// <param name="folderPath">The folder path.</param>
/// <returns>The <see cref="RazorPagesOptions"/>.</returns>
public static RazorPagesOptions AuthorizeFolder(this RazorPagesOptions options, string folderPath) =>
AuthorizeFolder(options, folderPath, policy: string.Empty);
/// <summary>
/// Adds the specified <paramref name="route"/> to the page at the specified <paramref name="pageName"/>.
/// <para>
/// The page can be routed via <paramref name="route"/> in addition to the default set of path based routes.
/// All links generated for this page will use the specified route.
/// </para>
/// </summary>
/// <param name="options">The <see cref="RazorPagesOptions"/>.</param>
/// <param name="pageName">The page name.</param>
/// <param name="route">The route to associate with the page.</param>
/// <returns>The <see cref="RazorPagesOptions"/>.</returns>
public static RazorPagesOptions AddPageRoute(this RazorPagesOptions options, string pageName, string route)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (string.IsNullOrEmpty(pageName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
options.RouteModelConventions.Add(new PageRouteModelConvention(pageName, model =>
{
// Use the route specified in MapPageRoute for outbound routing.
foreach (var selector in model.Selectors)
{
selector.AttributeRouteModel.SuppressLinkGeneration = true;
}
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Template = route,
}
});
}));
return options;
}
private class PageRouteModelConvention : IPageRouteModelConvention
{
private readonly string _path;
private readonly Action<PageRouteModel> _action;
public PageRouteModelConvention(string path, Action<PageRouteModel> action)
{
_path = path;
_action = action;
}
public void Apply(PageRouteModel model)
{
if (string.Equals(model.ViewEnginePath, _path, StringComparison.OrdinalIgnoreCase))
{
_action(model);
}
}
}
private class FolderRouteModelConvention : IPageRouteModelConvention
{
private readonly string _folderPath;
private readonly Action<PageRouteModel> _action;
public FolderRouteModelConvention(string folderPath, Action<PageRouteModel> action)
{
_folderPath = folderPath.TrimEnd('/');
_action = action;
}
public void Apply(PageRouteModel model)
{
var viewEnginePath = model.ViewEnginePath;
if (PathBelongsToFolder(_folderPath, viewEnginePath))
{
_action(model);
}
}
}
private class PageApplicationModelConvention : IPageApplicationModelConvention
{
private readonly string _path;
private readonly Action<PageApplicationModel> _action;
public PageApplicationModelConvention(string path, Action<PageApplicationModel> action)
{
_path = path;
_action = action;
}
public void Apply(PageApplicationModel model)
{
if (string.Equals(model.ViewEnginePath, _path, StringComparison.OrdinalIgnoreCase))
{
_action(model);
}
}
}
private class FolderApplicationModelConvention : IPageApplicationModelConvention
{
private readonly string _folderPath;
private readonly Action<PageApplicationModel> _action;
public FolderApplicationModelConvention(string folderPath, Action<PageApplicationModel> action)
{
_folderPath = folderPath.TrimEnd('/');
_action = action;
}
public void Apply(PageApplicationModel model)
{
var viewEnginePath = model.ViewEnginePath;
if (PathBelongsToFolder(_folderPath, viewEnginePath))
{
_action(model);
}
}
}
private static bool PathBelongsToFolder(string folderPath, string viewEnginePath)
{
if (folderPath == "/")
{
// Root directory covers everything.
return true;
}
return viewEnginePath.Length > folderPath.Length &&
viewEnginePath.StartsWith(folderPath, StringComparison.OrdinalIgnoreCase) &&
viewEnginePath[folderPath.Length] == '/';
}
}
}

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{ {
private readonly IPageRouteModelProvider[] _routeModelProviders; private readonly IPageRouteModelProvider[] _routeModelProviders;
private readonly MvcOptions _mvcOptions; private readonly MvcOptions _mvcOptions;
private readonly RazorPagesOptions _pagesOptions; private readonly IPageRouteModelConvention[] _conventions;
public PageActionDescriptorProvider( public PageActionDescriptorProvider(
IEnumerable<IPageRouteModelProvider> pageRouteModelProviders, IEnumerable<IPageRouteModelProvider> pageRouteModelProviders,
@ -25,7 +25,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{ {
_routeModelProviders = pageRouteModelProviders.OrderBy(p => p.Order).ToArray(); _routeModelProviders = pageRouteModelProviders.OrderBy(p => p.Order).ToArray();
_mvcOptions = mvcOptionsAccessor.Value; _mvcOptions = mvcOptionsAccessor.Value;
_pagesOptions = pagesOptionsAccessor.Value;
_conventions = pagesOptionsAccessor.Value.Conventions
.OfType<IPageRouteModelConvention>()
.ToArray();
} }
public int Order { get; set; } = -900; // Run after the default MVC provider, but before others. public int Order { get; set; } = -900; // Run after the default MVC provider, but before others.
@ -63,9 +66,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
private void AddActionDescriptors(IList<ActionDescriptor> actions, PageRouteModel model) private void AddActionDescriptors(IList<ActionDescriptor> actions, PageRouteModel model)
{ {
for (var i = 0; i < _pagesOptions.RouteModelConventions.Count; i++) for (var i = 0; i < _conventions.Length; i++)
{ {
_pagesOptions.RouteModelConventions[i].Apply(model); _conventions[i].Apply(model);
} }
foreach (var selector in model.Selectors) foreach (var selector in model.Selectors)

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{ {
private readonly IPageApplicationModelProvider[] _applicationModelProviders; private readonly IPageApplicationModelProvider[] _applicationModelProviders;
private readonly IViewCompilerProvider _viewCompilerProvider; private readonly IViewCompilerProvider _viewCompilerProvider;
private readonly RazorPagesOptions _options; private readonly IPageApplicationModelConvention[] _conventions;
public DefaultPageLoader( public DefaultPageLoader(
IEnumerable<IPageApplicationModelProvider> applicationModelProviders, IEnumerable<IPageApplicationModelProvider> applicationModelProviders,
@ -25,7 +25,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{ {
_applicationModelProviders = applicationModelProviders.ToArray(); _applicationModelProviders = applicationModelProviders.ToArray();
_viewCompilerProvider = viewCompilerProvider; _viewCompilerProvider = viewCompilerProvider;
_options = pageOptions.Value; _conventions = pageOptions.Value.Conventions
.OfType<IPageApplicationModelConvention>()
.ToArray();
} }
private IViewCompiler Compiler => _viewCompilerProvider.GetCompiler(); private IViewCompiler Compiler => _viewCompilerProvider.GetCompiler();
@ -52,9 +54,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
_applicationModelProviders[i].OnProvidersExecuted(context); _applicationModelProviders[i].OnProvidersExecuted(context);
} }
for (var i = 0; i < _options.ApplicationModelConventions.Count; i++) for (var i = 0; i < _conventions.Length; i++)
{ {
_options.ApplicationModelConventions[i].Apply(context.PageApplicationModel); _conventions[i].Apply(context.PageApplicationModel);
} }
return CompiledPageActionDescriptorBuilder.Build(context.PageApplicationModel); return CompiledPageActionDescriptorBuilder.Build(context.PageApplicationModel);

View File

@ -109,18 +109,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
=> string.Format(CultureInfo.CurrentCulture, GetString("AmbiguousHandler"), p0, p1); => string.Format(CultureInfo.CurrentCulture, GetString("AmbiguousHandler"), p0, p1);
/// <summary> /// <summary>
/// Path must be an application relative path that starts with a forward slash '/'. /// Path must be a root relative path that starts with a forward slash '/'.
/// </summary> /// </summary>
internal static string PathMustBeAnAppRelativePath internal static string PathMustBeRootRelativePath
{ {
get => GetString("PathMustBeAnAppRelativePath"); get => GetString("PathMustBeRootRelativePath");
} }
/// <summary> /// <summary>
/// Path must be an application relative path that starts with a forward slash '/'. /// Path must be a root relative path that starts with a forward slash '/'.
/// </summary> /// </summary>
internal static string FormatPathMustBeAnAppRelativePath() internal static string FormatPathMustBeRootRelativePath()
=> GetString("PathMustBeAnAppRelativePath"); => GetString("PathMustBeRootRelativePath");
/// <summary> /// <summary>
/// If an {0} provides a result value by setting the {1} property of {2} to a non-null value, then it cannot call the next filter by invoking {3}. /// If an {0} provides a result value by setting the {1} property of {2} to a non-null value, then it cannot call the next filter by invoking {3}.
@ -164,6 +164,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
internal static string FormatInvalidPageType_NoModelProperty(object p0, object p1) internal static string FormatInvalidPageType_NoModelProperty(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidPageType_NoModelProperty"), p0, p1); => string.Format(CultureInfo.CurrentCulture, GetString("InvalidPageType_NoModelProperty"), p0, p1);
/// <summary>
/// '{0}' is not a valid page name. A page name is path relative to the Razor Pages root directory that starts with a leading forward slash ('/') and does not contain the file extension e.g "/Users/Edit".
/// </summary>
internal static string InvalidValidPageName
{
get => GetString("InvalidValidPageName");
}
/// <summary>
/// '{0}' is not a valid page name. A page name is path relative to the Razor Pages root directory that starts with a leading forward slash ('/') and does not contain the file extension e.g "/Users/Edit".
/// </summary>
internal static string FormatInvalidValidPageName(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidValidPageName"), p0);
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -4,8 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
namespace Microsoft.AspNetCore.Mvc.RazorPages namespace Microsoft.AspNetCore.Mvc.RazorPages
{ {
@ -17,16 +15,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
private string _root = "/Pages"; private string _root = "/Pages";
/// <summary> /// <summary>
/// Gets a list of <see cref="IPageRouteModelConvention"/> instances that will be applied to /// Gets a collection of <see cref="IPageConvention"/> instances that are applied during
/// the <see cref="PageModel"/> when discovering Razor Pages. /// route and page model construction.
/// </summary> /// </summary>
public IList<IPageRouteModelConvention> RouteModelConventions { get; } = new List<IPageRouteModelConvention>(); public PageConventionCollection Conventions { get; } = new PageConventionCollection();
/// <summary>
/// Gets a list of <see cref="IPageRouteModelConvention"/> instances that will be applied to
/// the <see cref="PageModel"/> when discovering Razor Pages.
/// </summary>
public IList<IPageApplicationModelConvention> ApplicationModelConventions { get; } = new List<IPageApplicationModelConvention>();
/// <summary> /// <summary>
/// Application relative path used as the root of discovery for Razor Page files. /// Application relative path used as the root of discovery for Razor Page files.
@ -44,7 +36,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
if (value[0] != '/') if (value[0] != '/')
{ {
throw new ArgumentException(Resources.PathMustBeAnAppRelativePath, nameof(value)); throw new ArgumentException(Resources.PathMustBeRootRelativePath, nameof(value));
} }
_root = value; _root = value;

View File

@ -138,8 +138,8 @@
<data name="AmbiguousHandler" xml:space="preserve"> <data name="AmbiguousHandler" xml:space="preserve">
<value>Multiple handlers matched. The following handlers matched route data and had all constraints satisfied:{0}{0}{1}</value> <value>Multiple handlers matched. The following handlers matched route data and had all constraints satisfied:{0}{0}{1}</value>
</data> </data>
<data name="PathMustBeAnAppRelativePath" xml:space="preserve"> <data name="PathMustBeRootRelativePath" xml:space="preserve">
<value>Path must be an application relative path that starts with a forward slash '/'.</value> <value>Path must be a root relative path that starts with a forward slash '/'.</value>
</data> </data>
<data name="AsyncPageFilter_InvalidShortCircuit" xml:space="preserve"> <data name="AsyncPageFilter_InvalidShortCircuit" xml:space="preserve">
<value>If an {0} provides a result value by setting the {1} property of {2} to a non-null value, then it cannot call the next filter by invoking {3}.</value> <value>If an {0} provides a result value by setting the {1} property of {2} to a non-null value, then it cannot call the next filter by invoking {3}.</value>
@ -150,4 +150,7 @@
<data name="InvalidPageType_NoModelProperty" xml:space="preserve"> <data name="InvalidPageType_NoModelProperty" xml:space="preserve">
<value>The type '{0}' is not a valid page. A page must define a public, non-static '{1}' property.</value> <value>The type '{0}' is not a valid page. A page must define a public, non-static '{1}' property.</value>
</data> </data>
<data name="InvalidValidPageName" xml:space="preserve">
<value>'{0}' is not a valid page name. A page name is path relative to the Razor Pages root directory that starts with a leading forward slash ('/') and does not contain the file extension e.g "/Users/Edit".</value>
</data>
</root> </root>

View File

@ -0,0 +1,76 @@
// 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.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
public class PageConventionCollectionTest
{
[Theory]
[InlineData(null)]
[InlineData("")]
public void EnsureValidPageName_ThrowsIfPageNameIsNullOrEmpty(string pageName)
{
// Act & Assert
var ex = ExceptionAssert.ThrowsArgument(
() => PageConventionCollection.EnsureValidPageName(pageName),
"pageName",
"Value cannot be null or empty.");
}
[Theory]
[InlineData("path-without-slash")]
[InlineData(@"c:\myapp\path-without-slash")]
public void EnsureValidPageName_ThrowsIfPageNameDoesNotStartWithLeadingSlash(string pageName)
{
// Arrange
var expected = $"'{pageName}' is not a valid page name. A page name is path relative to the Razor Pages root directory that starts with a leading forward slash ('/') and does not contain the file extension e.g \"/Users/Edit\".";
// Act & Assert
var ex = ExceptionAssert.ThrowsArgument(
() => PageConventionCollection.EnsureValidPageName(pageName),
"pageName",
expected);
}
[Fact]
public void EnsureValidPageName_ThrowsIfPageNameHasExtension()
{
// Arrange
var pageName = "/Page.cshtml";
var expected = $"'{pageName}' is not a valid page name. A page name is path relative to the Razor Pages root directory that starts with a leading forward slash ('/') and does not contain the file extension e.g \"/Users/Edit\".";
// Act & Assert
var ex = ExceptionAssert.ThrowsArgument(
() => PageConventionCollection.EnsureValidPageName(pageName),
"pageName",
expected);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void EnsureValidFolderPath_ThrowsIfPathIsNullOrEmpty(string folderPath)
{
// Arrange
// Act & Assert
var ex = ExceptionAssert.ThrowsArgument(
() => PageConventionCollection.EnsureValidFolderPath(folderPath),
"folderPath",
"Value cannot be null or empty.");
}
[Theory]
[InlineData("path-without-slash")]
[InlineData(@"c:\myapp\path-without-slash")]
public void EnsureValidFolderPath_ThrowsIfPageNameDoesNotStartWithLeadingSlash(string folderPath)
{
// Arrange
// Act & Assert
var ex = ExceptionAssert.ThrowsArgument(
() => PageConventionCollection.EnsureValidFolderPath(folderPath),
"folderPath",
"Path must be a root relative path that starts with a forward slash '/'.");
}
}
}

View File

@ -14,7 +14,7 @@ namespace Microsoft.Extensions.DependencyInjection
public class MvcRazorPagesMvcBuilderExtensionsTest public class MvcRazorPagesMvcBuilderExtensionsTest
{ {
[Fact] [Fact]
public void AddRazorPagesOptions_AddsApplicationModelConventions() public void AddRazorPagesOptions_AddsConventions()
{ {
// Arrange // Arrange
var services = new ServiceCollection().AddOptions(); var services = new ServiceCollection().AddOptions();
@ -23,18 +23,20 @@ namespace Microsoft.Extensions.DependencyInjection
var builder = new MvcBuilder(services, new ApplicationPartManager()); var builder = new MvcBuilder(services, new ApplicationPartManager());
builder.AddRazorPagesOptions(options => builder.AddRazorPagesOptions(options =>
{ {
options.ApplicationModelConventions.Add(applicationModelConvention); options.Conventions.Add(applicationModelConvention);
options.RouteModelConventions.Add(routeModelConvention); options.Conventions.Add(routeModelConvention);
}); });
var serviceProvider = services.BuildServiceProvider(); var serviceProvider = services.BuildServiceProvider();
var accessor = serviceProvider.GetRequiredService<IOptions<RazorPagesOptions>>(); var accessor = serviceProvider.GetRequiredService<IOptions<RazorPagesOptions>>();
// Act & Assert // Act & Assert
var conventions = accessor.Value.ApplicationModelConventions; var conventions = accessor.Value.Conventions;
// Assert // Assert
Assert.Collection(conventions, Assert.Collection(
convention => Assert.Same(applicationModelConvention, convention)); conventions,
convention => Assert.Same(applicationModelConvention, convention),
convention => Assert.Same(routeModelConvention, convention));
} }
} }
} }

View File

@ -1,6 +1,7 @@
// 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.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels;
@ -12,24 +13,24 @@ using Xunit;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
{ {
public class RazorPagesOptionsExtensionsTest public class PageConventionCollectionExtensionsTest
{ {
[Fact] [Fact]
public void AddFilter_AddsFiltersToAllPages() public void AddFilter_AddsFiltersToAllPages()
{ {
// Arrange // Arrange
var filter = Mock.Of<IFilterMetadata>(); var filter = Mock.Of<IFilterMetadata>();
var options = new RazorPagesOptions(); var conventions = new PageConventionCollection();
var models = new[] var models = new[]
{ {
CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), CreateApplicationModel("/Pages/Index.cshtml", "/Index"),
CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account"),
CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact"),
}; };
// Act // Act
options.ConfigureFilter(filter); conventions.ConfigureFilter(filter);
ApplyConventions(options, models); ApplyConventions(conventions, models);
// Assert // Assert
Assert.Collection(models, Assert.Collection(models,
@ -42,30 +43,30 @@ namespace Microsoft.Extensions.DependencyInjection
public void AuthorizePage_AddsAllowAnonymousFilterToSpecificPage() public void AuthorizePage_AddsAllowAnonymousFilterToSpecificPage()
{ {
// Arrange // Arrange
var options = new RazorPagesOptions(); var conventions = new PageConventionCollection();
var models = new[] var models = new[]
{ {
CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), CreateApplicationModel("/Pages/Index.cshtml", "/Index"),
CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account"),
CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact"),
}; };
// Act // Act
options.AuthorizeFolder("/Users"); conventions.AuthorizeFolder("/Users");
options.AllowAnonymousToPage("/Users/Contact.cshtml"); conventions.AllowAnonymousToPage("/Users/Contact");
ApplyConventions(options, models); ApplyConventions(conventions, models);
// Assert // Assert
Assert.Collection(models, Assert.Collection(models,
model => Assert.Empty(model.Filters), model => Assert.Empty(model.Filters),
model => model =>
{ {
Assert.Equal("/Users/Account.cshtml", model.ViewEnginePath); Assert.Equal("/Users/Account", model.ViewEnginePath);
Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters)); Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
}, },
model => model =>
{ {
Assert.Equal("/Users/Contact.cshtml", model.ViewEnginePath); Assert.Equal("/Users/Contact", model.ViewEnginePath);
Assert.IsType<AuthorizeFilter>(model.Filters[0]); Assert.IsType<AuthorizeFilter>(model.Filters[0]);
Assert.IsType<AllowAnonymousFilter>(model.Filters[1]); Assert.IsType<AllowAnonymousFilter>(model.Filters[1]);
}); });
@ -77,35 +78,35 @@ namespace Microsoft.Extensions.DependencyInjection
public void AuthorizePage_AddsAllowAnonymousFilterToPagesUnderFolder(string folderName) public void AuthorizePage_AddsAllowAnonymousFilterToPagesUnderFolder(string folderName)
{ {
// Arrange // Arrange
var options = new RazorPagesOptions(); var conventions = new PageConventionCollection();
var models = new[] var models = new[]
{ {
CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), CreateApplicationModel("/Pages/Index.cshtml", "/Index"),
CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account"),
CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact"),
}; };
// Act // Act
options.AuthorizeFolder("/"); conventions.AuthorizeFolder("/");
options.AllowAnonymousToFolder("/Users"); conventions.AllowAnonymousToFolder("/Users");
ApplyConventions(options, models); ApplyConventions(conventions, models);
// Assert // Assert
Assert.Collection(models, Assert.Collection(models,
model => model =>
{ {
Assert.Equal("/Index.cshtml", model.ViewEnginePath); Assert.Equal("/Index", model.ViewEnginePath);
Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters)); Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
}, },
model => model =>
{ {
Assert.Equal("/Users/Account.cshtml", model.ViewEnginePath); Assert.Equal("/Users/Account", model.ViewEnginePath);
Assert.IsType<AuthorizeFilter>(model.Filters[0]); Assert.IsType<AuthorizeFilter>(model.Filters[0]);
Assert.IsType<AllowAnonymousFilter>(model.Filters[1]); Assert.IsType<AllowAnonymousFilter>(model.Filters[1]);
}, },
model => model =>
{ {
Assert.Equal("/Users/Contact.cshtml", model.ViewEnginePath); Assert.Equal("/Users/Contact", model.ViewEnginePath);
Assert.IsType<AuthorizeFilter>(model.Filters[0]); Assert.IsType<AuthorizeFilter>(model.Filters[0]);
Assert.IsType<AllowAnonymousFilter>(model.Filters[1]); Assert.IsType<AllowAnonymousFilter>(model.Filters[1]);
}); });
@ -115,24 +116,24 @@ namespace Microsoft.Extensions.DependencyInjection
public void AuthorizePage_AddsAuthorizeFilterWithPolicyToSpecificPage() public void AuthorizePage_AddsAuthorizeFilterWithPolicyToSpecificPage()
{ {
// Arrange // Arrange
var options = new RazorPagesOptions(); var conventions = new PageConventionCollection();
var models = new[] var models = new[]
{ {
CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), CreateApplicationModel("/Pages/Index.cshtml", "/Index"),
CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account"),
CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact"),
}; };
// Act // Act
options.AuthorizePage("/Users/Account.cshtml", "Manage-Accounts"); conventions.AuthorizePage("/Users/Account", "Manage-Accounts");
ApplyConventions(options, models); ApplyConventions(conventions, models);
// Assert // Assert
Assert.Collection(models, Assert.Collection(models,
model => Assert.Empty(model.Filters), model => Assert.Empty(model.Filters),
model => model =>
{ {
Assert.Equal("/Users/Account.cshtml", model.ViewEnginePath); Assert.Equal("/Users/Account", model.ViewEnginePath);
var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters)); var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData)); var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData));
Assert.Equal("Manage-Accounts", authorizeData.Policy); Assert.Equal("Manage-Accounts", authorizeData.Policy);
@ -144,24 +145,24 @@ namespace Microsoft.Extensions.DependencyInjection
public void AuthorizePage_AddsAuthorizeFilterWithoutPolicyToSpecificPage() public void AuthorizePage_AddsAuthorizeFilterWithoutPolicyToSpecificPage()
{ {
// Arrange // Arrange
var options = new RazorPagesOptions(); var conventions = new PageConventionCollection();
var models = new[] var models = new[]
{ {
CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), CreateApplicationModel("/Pages/Index.cshtml", "/Index"),
CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account"),
CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact"),
}; };
// Act // Act
options.AuthorizePage("/Users/Account.cshtml"); conventions.AuthorizePage("/Users/Account");
ApplyConventions(options, models); ApplyConventions(conventions, models);
// Assert // Assert
Assert.Collection(models, Assert.Collection(models,
model => Assert.Empty(model.Filters), model => Assert.Empty(model.Filters),
model => model =>
{ {
Assert.Equal("/Users/Account.cshtml", model.ViewEnginePath); Assert.Equal("/Users/Account", model.ViewEnginePath);
var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters)); var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData)); var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData));
Assert.Equal(string.Empty, authorizeData.Policy); Assert.Equal(string.Empty, authorizeData.Policy);
@ -175,31 +176,31 @@ namespace Microsoft.Extensions.DependencyInjection
public void AuthorizePage_AddsAuthorizeFilterWithPolicyToPagesUnderFolder(string folderName) public void AuthorizePage_AddsAuthorizeFilterWithPolicyToPagesUnderFolder(string folderName)
{ {
// Arrange // Arrange
var options = new RazorPagesOptions(); var conventions = new PageConventionCollection();
var models = new[] var models = new[]
{ {
CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), CreateApplicationModel("/Pages/Index.cshtml", "/Index"),
CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account"),
CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact"),
}; };
// Act // Act
options.AuthorizeFolder(folderName, "Manage-Accounts"); conventions.AuthorizeFolder(folderName, "Manage-Accounts");
ApplyConventions(options, models); ApplyConventions(conventions, models);
// Assert // Assert
Assert.Collection(models, Assert.Collection(models,
model => Assert.Empty(model.Filters), model => Assert.Empty(model.Filters),
model => model =>
{ {
Assert.Equal("/Users/Account.cshtml", model.ViewEnginePath); Assert.Equal("/Users/Account", model.ViewEnginePath);
var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters)); var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData)); var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData));
Assert.Equal("Manage-Accounts", authorizeData.Policy); Assert.Equal("Manage-Accounts", authorizeData.Policy);
}, },
model => model =>
{ {
Assert.Equal("/Users/Contact.cshtml", model.ViewEnginePath); Assert.Equal("/Users/Contact", model.ViewEnginePath);
var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters)); var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData)); var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData));
Assert.Equal("Manage-Accounts", authorizeData.Policy); Assert.Equal("Manage-Accounts", authorizeData.Policy);
@ -212,31 +213,31 @@ namespace Microsoft.Extensions.DependencyInjection
public void AuthorizePage_AddsAuthorizeFilterWithoutPolicyToPagesUnderFolder(string folderName) public void AuthorizePage_AddsAuthorizeFilterWithoutPolicyToPagesUnderFolder(string folderName)
{ {
// Arrange // Arrange
var options = new RazorPagesOptions(); var conventions = new PageConventionCollection();
var models = new[] var models = new[]
{ {
CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"),
CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account"),
CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact"),
}; };
// Act // Act
options.AuthorizeFolder(folderName); conventions.AuthorizeFolder(folderName);
ApplyConventions(options, models); ApplyConventions(conventions, models);
// Assert // Assert
Assert.Collection(models, Assert.Collection(models,
model => Assert.Empty(model.Filters), model => Assert.Empty(model.Filters),
model => model =>
{ {
Assert.Equal("/Users/Account.cshtml", model.ViewEnginePath); Assert.Equal("/Users/Account", model.ViewEnginePath);
var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters)); var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData)); var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData));
Assert.Equal(string.Empty, authorizeData.Policy); Assert.Equal(string.Empty, authorizeData.Policy);
}, },
model => model =>
{ {
Assert.Equal("/Users/Contact.cshtml", model.ViewEnginePath); Assert.Equal("/Users/Contact", model.ViewEnginePath);
var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters)); var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData)); var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData));
Assert.Equal(string.Empty, authorizeData.Policy); Assert.Equal(string.Empty, authorizeData.Policy);
@ -247,10 +248,10 @@ namespace Microsoft.Extensions.DependencyInjection
public void AddPageRoute_AddsRouteToSelector() public void AddPageRoute_AddsRouteToSelector()
{ {
// Arrange // Arrange
var options = new RazorPagesOptions(); var conventions = new PageConventionCollection();
var models = new[] var models = new[]
{ {
new PageRouteModel("/Pages/Index.cshtml", "/Index.cshtml") new PageRouteModel("/Pages/Index.cshtml", "/Index")
{ {
Selectors = Selectors =
{ {
@ -258,7 +259,7 @@ namespace Microsoft.Extensions.DependencyInjection
CreateSelectorModel(""), CreateSelectorModel(""),
} }
}, },
new PageRouteModel("/Pages/About.cshtml", "/About.cshtml") new PageRouteModel("/Pages/About.cshtml", "/About")
{ {
Selectors = Selectors =
{ {
@ -268,14 +269,14 @@ namespace Microsoft.Extensions.DependencyInjection
}; };
// Act // Act
options.AddPageRoute("/Index.cshtml", "Different-Route"); conventions.AddPageRoute("/Index", "Different-Route");
ApplyConventions(options, models); ApplyConventions(conventions, models);
// Assert // Assert
Assert.Collection(models, Assert.Collection(models,
model => model =>
{ {
Assert.Equal("/Index.cshtml", model.ViewEnginePath); Assert.Equal("/Index", model.ViewEnginePath);
Assert.Collection(model.Selectors, Assert.Collection(model.Selectors,
selector => selector =>
{ {
@ -295,7 +296,7 @@ namespace Microsoft.Extensions.DependencyInjection
}, },
model => model =>
{ {
Assert.Equal("/About.cshtml", model.ViewEnginePath); Assert.Equal("/About", model.ViewEnginePath);
Assert.Collection(model.Selectors, Assert.Collection(model.Selectors,
selector => selector =>
{ {
@ -317,9 +318,9 @@ namespace Microsoft.Extensions.DependencyInjection
}; };
} }
private static void ApplyConventions(RazorPagesOptions options, PageRouteModel[] models) private static void ApplyConventions(PageConventionCollection conventions, PageRouteModel[] models)
{ {
foreach (var convention in options.RouteModelConventions) foreach (var convention in conventions.OfType<IPageRouteModelConvention>())
{ {
foreach (var model in models) foreach (var model in models)
{ {
@ -327,9 +328,9 @@ namespace Microsoft.Extensions.DependencyInjection
} }
} }
} }
private static void ApplyConventions(RazorPagesOptions options, PageApplicationModel[] models) private static void ApplyConventions(PageConventionCollection conventions, PageApplicationModel[] models)
{ {
foreach (var convention in options.ApplicationModelConventions) foreach (var convention in conventions.OfType<IPageApplicationModelConvention>())
{ {
foreach (var model in models) foreach (var model in models)
{ {

View File

@ -105,7 +105,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{ {
Assert.Same(model, m); Assert.Same(model, m);
}); });
options.Value.ApplicationModelConventions.Add(convention.Object); options.Value.Conventions.Add(convention.Object);
var loader = new DefaultPageLoader( var loader = new DefaultPageLoader(
providers, providers,

View File

@ -16,11 +16,11 @@ namespace RazorPagesWebSite
.AddCookieTempDataProvider() .AddCookieTempDataProvider()
.AddRazorPagesOptions(options => .AddRazorPagesOptions(options =>
{ {
options.AuthorizePage("/HelloWorldWithAuth"); options.Conventions.AuthorizePage("/HelloWorldWithAuth");
options.AuthorizeFolder("/Pages/Admin"); options.Conventions.AuthorizeFolder("/Pages/Admin");
options.AllowAnonymousToPage("/Pages/Admin/Login"); options.Conventions.AllowAnonymousToPage("/Pages/Admin/Login");
options.AddPageRoute("/HelloWorldWithRoute", "Different-Route/{text}"); options.Conventions.AddPageRoute("/HelloWorldWithRoute", "Different-Route/{text}");
options.AddPageRoute("/Pages/NotTheRoot", string.Empty); options.Conventions.AddPageRoute("/Pages/NotTheRoot", string.Empty);
}) })
.WithRazorPagesAtContentRoot(); .WithRazorPagesAtContentRoot();
} }

View File

@ -16,8 +16,8 @@ namespace RazorPagesWebSite
.AddCookieTempDataProvider() .AddCookieTempDataProvider()
.AddRazorPagesOptions(options => .AddRazorPagesOptions(options =>
{ {
options.AuthorizePage("/Conventions/Auth"); options.Conventions.AuthorizePage("/Conventions/Auth");
options.AuthorizeFolder("/Conventions/AuthFolder"); options.Conventions.AuthorizeFolder("/Conventions/AuthFolder");
}); });
} }