Add PageConvention overloads for areas

Fixes #7246
This commit is contained in:
Pranav K 2018-01-17 11:57:57 -08:00
parent 6751e3b7ca
commit f0ae0ce528
14 changed files with 867 additions and 21 deletions

View File

@ -72,9 +72,22 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
/// <summary>
/// Gets the path relative to the base path for page discovery.
/// <para>
/// This value is the path of the file without extension, relative to the pages root directory.
/// e.g. the <see cref="ViewEnginePath"/> for the file /Pages/Catalog/Antiques.cshtml is <c>/Catalog/Antiques</c>
/// </para>
/// <para>
/// In an area, this value is the path of the file without extension, relative to the pages root directory for the specified area.
/// e.g. the <see cref="ViewEnginePath"/> for the file Areas/Identity/Pages/Manage/Accounts.cshtml, is <c>/Manage/Accounts</c>.
/// </para>
/// </summary>
public string ViewEnginePath => ActionDescriptor.ViewEnginePath;
/// <summary>
/// Gets the area name.
/// </summary>
public string AreaName => ActionDescriptor.AreaName;
/// <summary>
/// Gets the route template for the page.
/// </summary>

View File

@ -48,6 +48,40 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
return Add(new PageApplicationModelConvention(pageName, action));
}
/// <summary>
/// Creates and adds an <see cref="IPageApplicationModelConvention"/> that invokes an action on the
/// <see cref="PageApplicationModel"/> for the page with the specified name located in the specified area.
/// </summary>
/// <param name="areaName">The name of area.</param>
/// <param name="pageName">
/// The page name e.g. <c>/Users/List</c>
/// <para>
/// The page name is the path of the file without extension, relative to the pages root directory for the specified area.
/// e.g. the page name for the file Areas/Identity/Pages/Manage/Accounts.cshtml, is <c>/Manage/Accounts</c>.
/// </para>
/// </param>
/// <param name="action">The <see cref="Action"/>.</param>
/// <returns>The added <see cref="IPageApplicationModelConvention"/>.</returns>
public IPageApplicationModelConvention AddAreaPageApplicationModelConvention(
string areaName,
string pageName,
Action<PageApplicationModel> action)
{
if (string.IsNullOrEmpty(areaName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(areaName));
}
EnsureValidPageName(pageName);
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
return Add(new PageApplicationModelConvention(areaName, 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.
@ -67,6 +101,40 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
return Add(new FolderApplicationModelConvention(folderPath, action));
}
/// <summary>
/// Creates and adds an <see cref="IPageApplicationModelConvention"/> that invokes an action on
/// <see cref="PageApplicationModel"/> instances for all pages under the specified area folder.
/// </summary>
/// <param name="areaName">The name of area.</param>
/// <param name="folderPath">
/// The folder path e.g. <c>/Manage/</c>
/// <para>
/// The folder path is the path of the folder, relative to the pages root directory for the specified area.
/// e.g. the folder path for the file Areas/Identity/Pages/Manage/Accounts.cshtml, is <c>/Manage</c>.
/// </para>
/// </param>
/// <param name="action">The <see cref="Action"/>.</param>
/// <returns>The added <see cref="IPageApplicationModelConvention"/>.</returns>
public IPageApplicationModelConvention AddAreaFolderApplicationModelConvention(
string areaName,
string folderPath,
Action<PageApplicationModel> action)
{
if (string.IsNullOrEmpty(areaName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(areaName));
}
EnsureValidFolderPath(folderPath);
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
return Add(new FolderApplicationModelConvention(areaName, folderPath, action));
}
/// <summary>
/// Creates and adds an <see cref="IPageRouteModelConvention"/> that invokes an action on the
/// <see cref="PageRouteModel"/> for the page with the specified name.
@ -86,6 +154,37 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
return Add(new PageRouteModelConvention(pageName, action));
}
/// <summary>
/// Creates and adds an <see cref="IPageRouteModelConvention"/> that invokes an action on the
/// <see cref="PageRouteModel"/> for the page with the specified name located in the specified area.
/// </summary>
/// <param name="areaName">The area name.</param>
/// <param name="pageName">
/// The page name e.g. <c>/Users/List</c>
/// <para>
/// The page name is the path of the file without extension, relative to the pages root directory for the specified area.
/// e.g. the page name for the file Areas/Identity/Pages/Manage/Accounts.cshtml, is <c>/Manage/Accounts</c>.
/// </para>
/// </param>
/// <param name="action">The <see cref="Action"/>.</param>
/// <returns>The added <see cref="IPageRouteModelConvention"/>.</returns>
public IPageRouteModelConvention AddAreaPageRouteModelConvention(string areaName, string pageName, Action<PageRouteModel> action)
{
if (string.IsNullOrEmpty(areaName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(areaName));
}
EnsureValidPageName(pageName);
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
return Add(new PageRouteModelConvention(areaName, 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.
@ -105,6 +204,37 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
return Add(new FolderRouteModelConvention(folderPath, action));
}
/// <summary>
/// Creates and adds an <see cref="IPageRouteModelConvention"/> that invokes an action on
/// <see cref="PageRouteModel"/> instances for all page under the specified area folder.
/// </summary>
/// <param name="areaName">The area name.</param>
/// <param name="folderPath">
/// The folder path e.g. <c>/Manage/</c>
/// <para>
/// The folder path is the path of the folder, relative to the pages root directory for the specified area.
/// e.g. the folder path for the file Areas/Identity/Pages/Manage/Accounts.cshtml, is <c>/Manage</c>.
/// </para>
/// </param>
/// <param name="action">The <see cref="Action"/>.</param>
/// <returns>The added <see cref="IPageApplicationModelConvention"/>.</returns>
public IPageRouteModelConvention AddAreaFolderRouteModelConvention(string areaName, string folderPath, Action<PageRouteModel> action)
{
if (string.IsNullOrEmpty(areaName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(areaName));
}
EnsureValidFolderPath(folderPath);
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
return Add(new FolderRouteModelConvention(areaName, folderPath, action));
}
/// <summary>
/// Removes all <see cref="IPageConvention"/> instances of the specified type.
/// </summary>
@ -158,7 +288,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
}
}
private TConvention Add<TConvention>(TConvention convention) where TConvention: IPageConvention
private TConvention Add<TConvention>(TConvention convention) where TConvention : IPageConvention
{
base.Add(convention);
return convention;
@ -166,18 +296,26 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
private class PageRouteModelConvention : IPageRouteModelConvention
{
private readonly string _areaName;
private readonly string _path;
private readonly Action<PageRouteModel> _action;
public PageRouteModelConvention(string path, Action<PageRouteModel> action)
: this(null, path, action)
{
}
public PageRouteModelConvention(string areaName, string path, Action<PageRouteModel> action)
{
_areaName = areaName;
_path = path;
_action = action;
}
public void Apply(PageRouteModel model)
{
if (string.Equals(model.ViewEnginePath, _path, StringComparison.OrdinalIgnoreCase))
if (string.Equals(_areaName, model.AreaName, StringComparison.OrdinalIgnoreCase) &&
string.Equals(model.ViewEnginePath, _path, StringComparison.OrdinalIgnoreCase))
{
_action(model);
}
@ -186,20 +324,26 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
private class FolderRouteModelConvention : IPageRouteModelConvention
{
private readonly string _areaName;
private readonly string _folderPath;
private readonly Action<PageRouteModel> _action;
public FolderRouteModelConvention(string folderPath, Action<PageRouteModel> action)
: this(null, folderPath, action)
{
}
public FolderRouteModelConvention(string areaName, string folderPath, Action<PageRouteModel> action)
{
_areaName = areaName;
_folderPath = folderPath.TrimEnd('/');
_action = action;
}
public void Apply(PageRouteModel model)
{
var viewEnginePath = model.ViewEnginePath;
if (PathBelongsToFolder(_folderPath, viewEnginePath))
if (string.Equals(_areaName, model.AreaName, StringComparison.OrdinalIgnoreCase) &&
PathBelongsToFolder(_folderPath, model.ViewEnginePath))
{
_action(model);
}
@ -208,18 +352,26 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
private class PageApplicationModelConvention : IPageApplicationModelConvention
{
private readonly string _areaName;
private readonly string _path;
private readonly Action<PageApplicationModel> _action;
public PageApplicationModelConvention(string path, Action<PageApplicationModel> action)
: this(null, path, action)
{
}
public PageApplicationModelConvention(string areaName, string path, Action<PageApplicationModel> action)
{
_areaName = areaName;
_path = path;
_action = action;
}
public void Apply(PageApplicationModel model)
{
if (string.Equals(model.ViewEnginePath, _path, StringComparison.OrdinalIgnoreCase))
if (string.Equals(model.ViewEnginePath, _path, StringComparison.OrdinalIgnoreCase) &&
string.Equals(model.AreaName, _areaName, StringComparison.OrdinalIgnoreCase))
{
_action(model);
}
@ -228,20 +380,26 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
private class FolderApplicationModelConvention : IPageApplicationModelConvention
{
private readonly string _areaName;
private readonly string _folderPath;
private readonly Action<PageApplicationModel> _action;
public FolderApplicationModelConvention(string folderPath, Action<PageApplicationModel> action)
: this(null, folderPath, action)
{
}
public FolderApplicationModelConvention(string areaName, string folderPath, Action<PageApplicationModel> action)
{
_areaName = areaName;
_folderPath = folderPath.TrimEnd('/');
_action = action;
}
public void Apply(PageApplicationModel model)
{
var viewEnginePath = model.ViewEnginePath;
if (PathBelongsToFolder(_folderPath, viewEnginePath))
if (string.Equals(_areaName, model.AreaName, StringComparison.OrdinalIgnoreCase) &&
PathBelongsToFolder(_folderPath, model.ViewEnginePath))
{
_action(model);
}

View File

@ -21,9 +21,21 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
/// <param name="relativePath">The application relative path of the page.</param>
/// <param name="viewEnginePath">The path relative to the base path for page discovery.</param>
public PageRouteModel(string relativePath, string viewEnginePath)
: this(relativePath, viewEnginePath, areaName: null)
{
}
/// <summary>
/// Initializes a new instance of <see cref="PageRouteModel"/>.
/// </summary>
/// <param name="relativePath">The application relative path of the page.</param>
/// <param name="viewEnginePath">The path relative to the base path for page discovery.</param>
/// <param name="areaName">The area name.</param>
public PageRouteModel(string relativePath, string viewEnginePath, string areaName)
{
RelativePath = relativePath ?? throw new ArgumentNullException(nameof(relativePath));
ViewEnginePath = viewEnginePath ?? throw new ArgumentNullException(nameof(viewEnginePath));
AreaName = areaName;
Properties = new Dictionary<object, object>();
Selectors = new List<SelectorModel>();
@ -43,6 +55,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
RelativePath = other.RelativePath;
ViewEnginePath = other.ViewEnginePath;
AreaName = other.AreaName;
Properties = new Dictionary<object, object>(other.Properties);
Selectors = new List<SelectorModel>(other.Selectors.Select(m => new SelectorModel(m)));
@ -56,12 +69,22 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
/// <summary>
/// Gets the path relative to the base path for page discovery.
/// <para>
/// This value is the path of the file without extension, relative to the pages root directory.
/// e.g. the <see cref="ViewEnginePath"/> for the file /Pages/Catalog/Antiques.cshtml is <c>/Catalog/Antiques</c>
/// </para>
/// <para>
/// In an area, this value is the path of the file without extension, relative to the pages root directory for the specified area.
/// e.g. the <see cref="ViewEnginePath"/> for the file Areas/Identity/Pages/Manage/Accounts.cshtml, is <c>/Manage/Accounts</c>.
/// </para>
/// </summary>
/// <remarks>
/// For area pages, this path is calculated relative to the <see cref="RazorPagesOptions.RootDirectory"/> of the specific area.
/// </remarks>
public string ViewEnginePath { get; }
/// <summary>
/// Gets the area name. Will be <c>null</c> for non-area pages.
/// </summary>
public string AreaName { get; }
/// <summary>
/// Stores arbitrary metadata properties associated with the <see cref="PageRouteModel"/>.
/// </summary>
@ -79,7 +102,14 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
/// <remarks>
/// <para>
/// The value of <see cref="ViewEnginePath"/> is considered an implicit route value corresponding
/// to the key <c>page</c>. These entries will be implicitly added to <see cref="ActionDescriptor.RouteValues"/>
/// to the key <c>page</c>.
/// </para>
/// <para>
/// The value of <see cref="AreaName"/> is considered an implicit route value corresponding
/// to the key <c>area</c> when <see cref="AreaName"/> is not <c>null</c>.
/// </para>
/// <para>
/// These entries will be implicitly added to <see cref="ActionDescriptor.RouteValues"/>
/// when the action descriptor is created, but will not be visible in <see cref="RouteValues"/>.
/// </para>
/// </remarks>

View File

@ -82,6 +82,44 @@ namespace Microsoft.Extensions.DependencyInjection
return conventions;
}
/// <summary>
/// Adds a <see cref="AllowAnonymousFilter"/> to the page with the specified name located in the specified area.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="areaName">The area name.</param>
/// <param name="pageName">
/// The page name e.g. <c>/Users/List</c>
/// <para>
/// The page name is the path of the file without extension, relative to the pages root directory for the specified area.
/// e.g. the page name for the file Areas/Identity/Pages/Manage/Accounts.cshtml, is <c>/Manage/Accounts</c>.
/// </para>
/// </param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
public static PageConventionCollection AllowAnonymousToAreaPage(
this PageConventionCollection conventions,
string areaName,
string pageName)
{
if (conventions == null)
{
throw new ArgumentNullException(nameof(conventions));
}
if (string.IsNullOrEmpty(areaName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(areaName));
}
if (string.IsNullOrEmpty(pageName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName));
}
var anonymousFilter = new AllowAnonymousFilter();
conventions.AddAreaPageApplicationModelConvention(areaName, pageName, model => model.Filters.Add(anonymousFilter));
return conventions;
}
/// <summary>
/// Adds a <see cref="AllowAnonymousFilter"/> to all pages under the specified folder.
/// </summary>
@ -105,6 +143,44 @@ namespace Microsoft.Extensions.DependencyInjection
return conventions;
}
/// <summary>
/// Adds a <see cref="AllowAnonymousFilter"/> to all pages under the specified area folder.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="areaName">The area name.</param>
/// <param name="folderPath">
/// The folder path e.g. <c>/Manage/</c>
/// <para>
/// The folder path is the path of the folder, relative to the pages root directory for the specified area.
/// e.g. the folder path for the file Areas/Identity/Pages/Manage/Accounts.cshtml, is <c>/Manage</c>.
/// </para>
///.</param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
public static PageConventionCollection AllowAnonymousToAreaFolder(
this PageConventionCollection conventions,
string areaName,
string folderPath)
{
if (conventions == null)
{
throw new ArgumentNullException(nameof(conventions));
}
if (string.IsNullOrEmpty(areaName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(areaName));
}
if (string.IsNullOrEmpty(folderPath))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(folderPath));
}
var anonymousFilter = new AllowAnonymousFilter();
conventions.AddAreaFolderApplicationModelConvention(areaName, 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>
@ -138,6 +214,62 @@ namespace Microsoft.Extensions.DependencyInjection
public static PageConventionCollection AuthorizePage(this PageConventionCollection conventions, string pageName) =>
AuthorizePage(conventions, pageName, policy: string.Empty);
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> with default policy to the page with the specified name.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="areaName">The area name.</param>
/// <param name="pageName">
/// The page name e.g. <c>/Users/List</c>
/// <para>
/// The page name is the path of the file without extension, relative to the pages root directory for the specified area.
/// e.g. the page name for the file Areas/Identity/Pages/Manage/Accounts.cshtml, is <c>/Manage/Accounts</c>.
/// </para>
/// </param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
public static PageConventionCollection AuthorizeAreaPage(this PageConventionCollection conventions, string areaName, string pageName)
=> AuthorizeAreaPage(conventions, areaName, pageName, policy: string.Empty);
/// <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="areaName">The area name.</param>
/// <param name="pageName">
/// The page name e.g. <c>/Users/List</c>
/// <para>
/// The page name is the path of the file without extension, relative to the pages root directory for the specified area.
/// e.g. the page name for the file Areas/Identity/Pages/Manage/Accounts.cshtml, is <c>/Manage/Accounts</c>.
/// </para>
/// </param>
/// <param name="policy">The authorization policy.</param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
public static PageConventionCollection AuthorizeAreaPage(
this PageConventionCollection conventions,
string areaName,
string pageName,
string policy)
{
if (conventions == null)
{
throw new ArgumentNullException(nameof(conventions));
}
if (string.IsNullOrEmpty(areaName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(areaName));
}
if (string.IsNullOrEmpty(pageName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName));
}
var authorizeFilter = new AuthorizeFilter(policy);
conventions.AddAreaPageApplicationModelConvention(areaName, pageName, model => model.Filters.Add(authorizeFilter));
return conventions;
}
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> with the specified policy to all pages under the specified folder.
/// </summary>
@ -171,6 +303,62 @@ namespace Microsoft.Extensions.DependencyInjection
public static PageConventionCollection AuthorizeFolder(this PageConventionCollection conventions, string folderPath) =>
AuthorizeFolder(conventions, folderPath, policy: string.Empty);
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> with the default policy to all pages under the specified folder.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="areaName">The area name.</param>
/// <param name="folderPath">
/// The folder path e.g. <c>/Manage/</c>
/// <para>
/// The folder path is the path of the folder, relative to the pages root directory for the specified area.
/// e.g. the folder path for the file Areas/Identity/Pages/Manage/Accounts.cshtml, is <c>/Manage</c>.
/// </para>
/// </param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
public static PageConventionCollection AuthorizeAreaFolder(this PageConventionCollection conventions, string areaName, string folderPath)
=> AuthorizeAreaFolder(conventions, areaName, folderPath, 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="areaName">The area name.</param>
/// <param name="folderPath">
/// The folder path e.g. <c>/Manage/</c>
/// <para>
/// The folder path is the path of the folder, relative to the pages root directory for the specified area.
/// e.g. the folder path for the file Areas/Identity/Pages/Manage/Accounts.cshtml, is <c>/Manage</c>.
/// </para>
/// </param>
/// <param name="policy">The authorization policy.</param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
public static PageConventionCollection AuthorizeAreaFolder(
this PageConventionCollection conventions,
string areaName,
string folderPath,
string policy)
{
if (conventions == null)
{
throw new ArgumentNullException(nameof(conventions));
}
if (string.IsNullOrEmpty(areaName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(areaName));
}
if (string.IsNullOrEmpty(folderPath))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(folderPath));
}
var authorizeFilter = new AuthorizeFilter(policy);
conventions.AddAreaFolderApplicationModelConvention(areaName, folderPath, model => model.Filters.Add(authorizeFilter));
return conventions;
}
/// <summary>
/// Adds the specified <paramref name="route"/> to the page at the specified <paramref name="pageName"/>.
/// <para>
@ -199,7 +387,64 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentNullException(nameof(route));
}
conventions.AddPageRouteModelConvention(pageName, model =>
conventions.AddPageRouteModelConvention(pageName, AddPageRouteThunk(route));
return conventions;
}
/// <summary>
/// Adds the specified <paramref name="route"/> to the page at the specified <paramref name="pageName"/> located in the specified
/// area.
/// <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="areaName">The area name.</param>
/// <param name="pageName">
/// The page name e.g. <c>/Users/List</c>
/// <para>
/// The page name is the path of the file without extension, relative to the pages root directory for the specified area.
/// e.g. the page name for the file Areas/Identity/Pages/Manage/Accounts.cshtml, is <c>/Manage/Accounts</c>.
/// </para>
/// </param>
/// <param name="route">The route to associate with the page.</param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
public static PageConventionCollection AddAreaPageRoute(
this PageConventionCollection conventions,
string areaName,
string pageName,
string route)
{
if (conventions == null)
{
throw new ArgumentNullException(nameof(conventions));
}
if (string.IsNullOrEmpty(areaName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(areaName));
}
if (string.IsNullOrEmpty(pageName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
conventions.AddAreaPageRouteModelConvention(areaName, pageName, AddPageRouteThunk(route));
return conventions;
}
private static Action<PageRouteModel> AddPageRouteThunk(string route)
{
return model =>
{
// Use the route specified in MapPageRoute for outbound routing.
foreach (var selector in model.Selectors)
@ -214,9 +459,7 @@ namespace Microsoft.Extensions.DependencyInjection
Template = route,
}
});
});
return conventions;
};
}
}
}

View File

@ -73,9 +73,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
foreach (var selector in model.Selectors)
{
var descriptor = new PageActionDescriptor()
var descriptor = new PageActionDescriptor
{
AttributeRouteInfo = new AttributeRouteInfo()
AttributeRouteInfo = new AttributeRouteInfo
{
Name = selector.AttributeRouteModel.Name,
Order = selector.AttributeRouteModel.Order ?? 0,
@ -88,6 +88,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
Properties = new Dictionary<object, object>(model.Properties),
RelativePath = model.RelativePath,
ViewEnginePath = model.ViewEnginePath,
AreaName = model.AreaName,
};
foreach (var kvp in model.RouteValues)

View File

@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
return null;
}
var routeModel = new PageRouteModel(relativePath, areaResult.viewEnginePath);
var routeModel = new PageRouteModel(relativePath, areaResult.viewEnginePath, areaResult.areaName);
var routePrefix = CreateAreaRoute(areaResult.areaName, areaResult.viewEnginePath);
PopulateRouteModel(routeModel, routePrefix, routeTemplate);

View File

@ -23,8 +23,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
/// <param name="other">The <see cref="PageActionDescriptor"/> to copy from.</param>
public PageActionDescriptor(PageActionDescriptor other)
{
if (other == null)
{
throw new ArgumentNullException(nameof(other));
}
RelativePath = other.RelativePath;
ViewEnginePath = other.ViewEnginePath;
AreaName = other.AreaName;
}
/// <summary>
@ -34,9 +40,23 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
/// <summary>
/// Gets or sets the path relative to the base path for page discovery.
/// <para>
/// This value is the path of the file without extension, relative to the pages root directory.
/// e.g. the <see cref="ViewEnginePath"/> for the file /Pages/Catalog/Antiques.cshtml is <c>/Catalog/Antiques</c>
/// </para>
/// <para>
/// In an area, this value is the path of the file without extension, relative to the pages root directory for the specified area.
/// e.g. the <see cref="ViewEnginePath"/> for the file Areas/Identity/Pages/Manage/Accounts.cshtml, is <c>/Manage/Accounts</c>.
/// </para>
/// </summary>
public string ViewEnginePath { get; set; }
/// <summary>
/// Gets or sets the area name for this page.
/// This value will be <c>null</c> for non-area pages.
/// </summary>
public string AreaName { get; set; }
/// <inheritdoc />
public override string DisplayName
{

View File

@ -370,5 +370,26 @@ Hello from /Pages/Shared/";
// Assert
Assert.Equal(expected, response.Trim());
}
[Fact]
public async Task AuthorizeFolderConvention_CanBeAppliedToAreaPages()
{
// Act
var response = await Client.GetAsync("/Accounts/RequiresAuth");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/Login?ReturnUrl=%2FAccounts%2FRequiresAuth", response.Headers.Location.PathAndQuery);
}
[Fact]
public async Task AllowAnonymouseToPageConvention_CanBeAppliedToAreaPages()
{
// Act
var response = await Client.GetStringAsync("/Accounts/RequiresAuth/AllowAnonymous");
// Assert
Assert.Equal("Hello from AllowAnonymous", response.Trim());
}
}
}

View File

@ -72,6 +72,31 @@ namespace Microsoft.Extensions.DependencyInjection
});
}
[Fact]
public void AllowAnonymousToAreaPage_AddsAllowAnonymousFilterToSpecificPage()
{
// Arrange
var conventions = new PageConventionCollection();
var models = new[]
{
CreateApplicationModel("/Profile.cshtml", "/Profile"),
CreateApplicationModel("/Areas/Accounts/Pages/Profile.cshtml", "/Profile", "Accounts"),
};
// Act
conventions.AllowAnonymousToAreaPage("Accounts", "/Profile");
ApplyConventions(conventions, models);
// Assert
Assert.Collection(models,
model => Assert.Empty(model.Filters),
model =>
{
Assert.Equal("/Areas/Accounts/Pages/Profile.cshtml", model.RelativePath);
Assert.IsType<AllowAnonymousFilter>(Assert.Single(model.Filters));
});
}
[Theory]
[InlineData("/Users")]
[InlineData("/Users/")]
@ -112,6 +137,41 @@ namespace Microsoft.Extensions.DependencyInjection
});
}
[Fact]
public void AllowAnonymousToAreaFolder_AddsAllowAnonymousFilterToFolderInArea()
{
// Arrange
var conventions = new PageConventionCollection();
var models = new[]
{
CreateApplicationModel("/Profile.cshtml", "/Profile"),
CreateApplicationModel("/Mange/Profile.cshtml", "/Manage/Profile"),
CreateApplicationModel("/Areas/Accounts/Pages/Manage/Profile.cshtml", "/Manage/Profile", "Accounts"),
CreateApplicationModel("/Areas/Accounts/Pages/Manage/2FA.cshtml", "/Manage/2FA", "Accounts"),
CreateApplicationModel("/Areas/Accounts/Pages/View/OrderHistory.cshtml", "/View/OrderHistory", "Accounts"),
};
// Act
conventions.AllowAnonymousToAreaFolder("Accounts", "/Manage");
ApplyConventions(conventions, models);
// Assert
Assert.Collection(models,
model => Assert.Empty(model.Filters),
model => Assert.Empty(model.Filters),
model =>
{
Assert.Equal("/Areas/Accounts/Pages/Manage/Profile.cshtml", model.RelativePath);
Assert.IsType<AllowAnonymousFilter>(Assert.Single(model.Filters));
},
model =>
{
Assert.Equal("/Areas/Accounts/Pages/Manage/2FA.cshtml", model.RelativePath);
Assert.IsType<AllowAnonymousFilter>(Assert.Single(model.Filters));
},
model => Assert.Empty(model.Filters));
}
[Fact]
public void AuthorizePage_AddsAuthorizeFilterWithPolicyToSpecificPage()
{
@ -141,6 +201,60 @@ namespace Microsoft.Extensions.DependencyInjection
model => Assert.Empty(model.Filters));
}
[Fact]
public void AuthorizeAreaPage_AddsAuthorizeFilterWithDefaultPolicyToAreaPage()
{
// Arrange
var conventions = new PageConventionCollection();
var models = new[]
{
CreateApplicationModel("/Profile.cshtml", "/Profile"),
CreateApplicationModel("/Areas/Accounts/Pages/Profile.cshtml", "/Profile", "Accounts"),
};
// Act
conventions.AuthorizeAreaPage("Accounts", "/Profile");
ApplyConventions(conventions, models);
// Assert
Assert.Collection(models,
model => Assert.Empty(model.Filters),
model =>
{
Assert.Equal("/Areas/Accounts/Pages/Profile.cshtml", model.RelativePath);
var authFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeAttribute = Assert.Single(authFilter.AuthorizeData);
Assert.Empty(authorizeAttribute.Policy);
});
}
[Fact]
public void AuthorizeAreaPage_AddsAuthorizeFilterWithCustomPolicyToAreaPage()
{
// Arrange
var conventions = new PageConventionCollection();
var models = new[]
{
CreateApplicationModel("/Profile.cshtml", "/Profile"),
CreateApplicationModel("/Areas/Accounts/Pages/Profile.cshtml", "/Profile", "Accounts"),
};
// Act
conventions.AuthorizeAreaPage("Accounts", "/Profile", "custom");
ApplyConventions(conventions, models);
// Assert
Assert.Collection(models,
model => Assert.Empty(model.Filters),
model =>
{
Assert.Equal("/Areas/Accounts/Pages/Profile.cshtml", model.RelativePath);
var authFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeAttribute = Assert.Single(authFilter.AuthorizeData);
Assert.Equal("custom", authorizeAttribute.Policy);
});
}
[Fact]
public void AuthorizePage_AddsAuthorizeFilterWithoutPolicyToSpecificPage()
{
@ -244,6 +358,84 @@ namespace Microsoft.Extensions.DependencyInjection
});
}
[Fact]
public void AuthorizeAreaFolder_AddsAuthorizeFilterWithDefaultPolicyToAreaPagesInFolder()
{
// Arrange
var conventions = new PageConventionCollection();
var models = new[]
{
CreateApplicationModel("/Profile.cshtml", "/Profile"),
CreateApplicationModel("/Mange/Profile.cshtml", "/Manage/Profile"),
CreateApplicationModel("/Areas/Accounts/Pages/Manage/Profile.cshtml", "/Manage/Profile", "Accounts"),
CreateApplicationModel("/Areas/Accounts/Pages/Manage/2FA.cshtml", "/Manage/2FA", "Accounts"),
CreateApplicationModel("/Areas/Accounts/Pages/View/OrderHistory.cshtml", "/View/OrderHistory", "Accounts"),
};
// Act
conventions.AuthorizeAreaFolder("Accounts", "/Manage");
ApplyConventions(conventions, models);
// Assert
Assert.Collection(models,
model => Assert.Empty(model.Filters),
model => Assert.Empty(model.Filters),
model =>
{
Assert.Equal("/Areas/Accounts/Pages/Manage/Profile.cshtml", model.RelativePath);
var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData));
Assert.Empty(authorizeData.Policy);
},
model =>
{
Assert.Equal("/Areas/Accounts/Pages/Manage/2FA.cshtml", model.RelativePath);
var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData));
Assert.Empty(authorizeData.Policy);
},
model => Assert.Empty(model.Filters));
}
[Fact]
public void AuthorizeAreaFolder_AddsAuthorizeFilterWithCustomPolicyToAreaPagesInFolder()
{
// Arrange
var conventions = new PageConventionCollection();
var models = new[]
{
CreateApplicationModel("/Profile.cshtml", "/Profile"),
CreateApplicationModel("/Mange/Profile.cshtml", "/Manage/Profile"),
CreateApplicationModel("/Areas/Accounts/Pages/Manage/Profile.cshtml", "/Manage/Profile", "Accounts"),
CreateApplicationModel("/Areas/Accounts/Pages/Manage/2FA.cshtml", "/Manage/2FA", "Accounts"),
CreateApplicationModel("/Areas/Accounts/Pages/View/OrderHistory.cshtml", "/View/OrderHistory", "Accounts"),
};
// Act
conventions.AuthorizeAreaFolder("Accounts", "/Manage", "custom");
ApplyConventions(conventions, models);
// Assert
Assert.Collection(models,
model => Assert.Empty(model.Filters),
model => Assert.Empty(model.Filters),
model =>
{
Assert.Equal("/Areas/Accounts/Pages/Manage/Profile.cshtml", model.RelativePath);
var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData));
Assert.Equal("custom", authorizeData.Policy);
},
model =>
{
Assert.Equal("/Areas/Accounts/Pages/Manage/2FA.cshtml", model.RelativePath);
var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData));
Assert.Equal("custom", authorizeData.Policy);
},
model => Assert.Empty(model.Filters));
}
[Fact]
public void AddPageRoute_AddsRouteToSelector()
{
@ -306,6 +498,62 @@ namespace Microsoft.Extensions.DependencyInjection
});
}
[Fact]
public void AddAreaPageRoute_AddsRouteToSelector()
{
// Arrange
var conventions = new PageConventionCollection();
var models = new[]
{
new PageRouteModel("/Pages/Profile.cshtml", "/Profile")
{
Selectors =
{
CreateSelectorModel("Profile"),
}
},
new PageRouteModel("/Areas/Accounts/Pages/Profile.cshtml", "/Profile", "Accounts")
{
Selectors =
{
CreateSelectorModel("Accounts/Profile"),
}
}
};
// Act
conventions.AddAreaPageRoute("Accounts", "/Profile", "Different-Route");
ApplyConventions(conventions, models);
// Assert
Assert.Collection(models,
model =>
{
Assert.Equal("/Pages/Profile.cshtml", model.RelativePath);
Assert.Collection(model.Selectors,
selector =>
{
Assert.Equal("Profile", selector.AttributeRouteModel.Template);
Assert.False(selector.AttributeRouteModel.SuppressLinkGeneration);
});
},
model =>
{
Assert.Equal("/Areas/Accounts/Pages/Profile.cshtml", model.RelativePath);
Assert.Collection(model.Selectors,
selector =>
{
Assert.Equal("Accounts/Profile", selector.AttributeRouteModel.Template);
Assert.True (selector.AttributeRouteModel.SuppressLinkGeneration);
},
selector =>
{
Assert.Equal("Different-Route", selector.AttributeRouteModel.Template);
Assert.False(selector.AttributeRouteModel.SuppressLinkGeneration);
});
});
}
private static SelectorModel CreateSelectorModel(string template, bool suppressLinkGeneration = false)
{
return new SelectorModel
@ -339,12 +587,13 @@ namespace Microsoft.Extensions.DependencyInjection
}
}
private PageApplicationModel CreateApplicationModel(string relativePath, string viewEnginePath)
private PageApplicationModel CreateApplicationModel(string relativePath, string viewEnginePath, string areaName = null)
{
var descriptor = new PageActionDescriptor
{
ViewEnginePath = viewEnginePath,
RelativePath = relativePath,
AreaName = areaName,
};
return new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), new object[0]);

View File

@ -2,6 +2,7 @@
// 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.ApplicationModels;
using Microsoft.Extensions.Options;
@ -65,6 +66,107 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
Assert.Equal("/Test/{id:int?}", descriptor.AttributeRouteInfo.Template);
}
[Fact]
public void GetDescriptors_AddsDescriptorsForAreaPages()
{
// Arrange
var model = new PageRouteModel("/Test.cshtml", "/Test")
{
RouteValues =
{
{ "custom-key", "custom-value" },
},
Selectors =
{
new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Template = "/Test/{id:int?}",
}
}
}
};
var applicationModelProvider = new TestPageRouteModelProvider(model);
var provider = new PageActionDescriptorProvider(
new[] { applicationModelProvider },
GetAccessor<MvcOptions>(),
GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext();
// Act
provider.OnProvidersExecuting(context);
// Assert
var result = Assert.Single(context.Results);
var descriptor = Assert.IsType<PageActionDescriptor>(result);
Assert.Equal(model.RelativePath, descriptor.RelativePath);
Assert.Collection(
descriptor.RouteValues.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("custom-key", kvp.Key);
Assert.Equal("custom-value", kvp.Value);
},
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Test", kvp.Value);
});
}
[Fact]
public void GetDescriptors_CopiesRouteValuesFromModel()
{
// Arrange
var model = new PageRouteModel("/Areas/Accounts/Pages/Test.cshtml", "/Test", "Accounts")
{
RouteValues =
{
{ "page", "/Test" },
{ "area", "Accounts" },
},
Selectors =
{
new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Template = "Accounts/Test/{id:int?}",
}
}
}
};
var applicationModelProvider = new TestPageRouteModelProvider(model);
var provider = new PageActionDescriptorProvider(
new[] { applicationModelProvider },
GetAccessor<MvcOptions>(),
GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext();
// Act
provider.OnProvidersExecuting(context);
// Assert
var result = Assert.Single(context.Results);
var descriptor = Assert.IsType<PageActionDescriptor>(result);
Assert.Equal(model.RelativePath, descriptor.RelativePath);
Assert.Collection(
descriptor.RouteValues.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("Accounts", kvp.Value);
},
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Test", kvp.Value);
});
Assert.Equal("Accounts", descriptor.AreaName);
Assert.Equal("Accounts/Test/{id:int?}", descriptor.AttributeRouteInfo.Template);
}
[Fact]
public void GetDescriptors_AddsActionDescriptorForEachSelector()
{

View File

@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
// Assert
Assert.Equal(relativePath, routeModel.RelativePath);
Assert.Equal("/Users/Profile", routeModel.ViewEnginePath);
Assert.Null(routeModel.AreaName);
Assert.Collection(
routeModel.Selectors,
@ -109,6 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
// Assert
Assert.Equal(relativePath, routeModel.RelativePath);
Assert.Equal("/Users/Profile", routeModel.ViewEnginePath);
Assert.Equal("TestArea", routeModel.AreaName);
Assert.Collection(
routeModel.Selectors,
@ -142,6 +144,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
// Assert
Assert.Equal(relativePath, routeModel.RelativePath);
Assert.Equal("/Users/Profile/Index", routeModel.ViewEnginePath);
Assert.Equal("TestArea", routeModel.AreaName);
Assert.Collection(
routeModel.Selectors,

View File

@ -0,0 +1,2 @@
@page
Hello from AllowAnonymous

View File

@ -0,0 +1,2 @@
@page
@{ throw new Exception("This should not be rendered."); }

View File

@ -20,6 +20,8 @@ namespace RazorPagesWebSite
options.AllowAreas = true;
options.Conventions.AuthorizePage("/Conventions/Auth");
options.Conventions.AuthorizeFolder("/Conventions/AuthFolder");
options.Conventions.AuthorizeAreaFolder("Accounts", "/RequiresAuth");
options.Conventions.AllowAnonymousToAreaPage("Accounts", "/RequiresAuth/AllowAnonymous");
});
}