File: DependencyInjection\PageConventionCollectionExtensions.cs
Web Access
Project: src\src\Mvc\Mvc.RazorPages\src\Microsoft.AspNetCore.Mvc.RazorPages.csproj (Microsoft.AspNetCore.Mvc.RazorPages)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
 
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)
    {
        ArgumentNullException.ThrowIfNull(conventions);
        ArgumentNullException.ThrowIfNull(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)
    {
        ArgumentNullException.ThrowIfNull(conventions);
        ArgumentNullException.ThrowIfNull(filter);
 
        conventions.AddFolderApplicationModelConvention("/", model => model.Filters.Add(filter));
        return conventions;
    }
 
    /// <summary>
    /// Adds the specified <paramref name="convention"/> to <paramref name="conventions"/>.
    /// The added convention will apply to all handler properties and parameters on handler methods.
    /// </summary>
    /// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
    /// <param name="convention">The <see cref="IParameterModelBaseConvention"/> to apply.</param>
    /// <returns>The <see cref="PageConventionCollection"/>.</returns>
    public static PageConventionCollection Add(this PageConventionCollection conventions, IParameterModelBaseConvention convention)
    {
        ArgumentNullException.ThrowIfNull(conventions);
        ArgumentNullException.ThrowIfNull(convention);
 
        var adapter = new ParameterModelBaseConventionAdapter(convention);
        conventions.Add(adapter);
        return conventions;
    }
 
    /// <summary>
    /// Allows anonymous access 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)
    {
        ArgumentNullException.ThrowIfNull(conventions);
        ArgumentException.ThrowIfNullOrEmpty(pageName);
 
        conventions.AddPageApplicationModelConvention(pageName, model =>
        {
            if (conventions.MvcOptions.EnableEndpointRouting)
            {
                model.EndpointMetadata.Add(new AllowAnonymousAttribute());
            }
            else
            {
                model.Filters.Add(new AllowAnonymousFilter());
            }
        });
        return conventions;
    }
 
    /// <summary>
    /// Allows anonymous access 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)
    {
        ArgumentNullException.ThrowIfNull(conventions);
        ArgumentException.ThrowIfNullOrEmpty(areaName);
        ArgumentException.ThrowIfNullOrEmpty(pageName);
 
        conventions.AddAreaPageApplicationModelConvention(areaName, pageName, model =>
        {
            if (conventions.MvcOptions.EnableEndpointRouting)
            {
                model.EndpointMetadata.Add(new AllowAnonymousAttribute());
            }
            else
            {
                model.Filters.Add(new AllowAnonymousFilter());
            }
        });
        return conventions;
    }
 
    /// <summary>
    /// Allows anonymous access 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)
    {
        ArgumentNullException.ThrowIfNull(conventions);
        ArgumentException.ThrowIfNullOrEmpty(folderPath);
 
        conventions.AddFolderApplicationModelConvention(folderPath, model =>
        {
            if (conventions.MvcOptions.EnableEndpointRouting)
            {
                model.EndpointMetadata.Add(new AllowAnonymousAttribute());
            }
            else
            {
                model.Filters.Add(new AllowAnonymousFilter());
            }
        });
        return conventions;
    }
 
    /// <summary>
    /// Allows anonymous access 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)
    {
        ArgumentNullException.ThrowIfNull(conventions);
        ArgumentException.ThrowIfNullOrEmpty(areaName);
        ArgumentException.ThrowIfNullOrEmpty(folderPath);
 
        conventions.AddAreaFolderApplicationModelConvention(areaName, folderPath, model =>
        {
            if (conventions.MvcOptions.EnableEndpointRouting)
            {
                model.EndpointMetadata.Add(new AllowAnonymousAttribute());
            }
            else
            {
                model.Filters.Add(new AllowAnonymousFilter());
            }
        });
        return conventions;
    }
 
    /// <summary>
    /// Requires authorization with the specified policy for 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)
    {
        ArgumentNullException.ThrowIfNull(conventions);
        ArgumentException.ThrowIfNullOrEmpty(pageName);
 
        conventions.AddPageApplicationModelConvention(pageName, model =>
        {
            if (conventions.MvcOptions.EnableEndpointRouting)
            {
                model.EndpointMetadata.Add(new AuthorizeAttribute(policy));
            }
            else
            {
                model.Filters.Add(new AuthorizeFilter(policy));
            }
        });
        return conventions;
    }
 
    /// <summary>
    /// Requires authorization for the specified page.
    /// </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>
    /// Requires authorization for the specified area page.
    /// </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>
    /// Requires authorization for the specified area page with the specified policy.
    /// </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)
    {
        ArgumentNullException.ThrowIfNull(conventions);
        ArgumentException.ThrowIfNullOrEmpty(areaName);
        ArgumentException.ThrowIfNullOrEmpty(pageName);
 
        conventions.AddAreaPageApplicationModelConvention(areaName, pageName, model =>
        {
            if (conventions.MvcOptions.EnableEndpointRouting)
            {
                model.EndpointMetadata.Add(new AuthorizeAttribute(policy));
            }
            else
            {
                model.Filters.Add(new AuthorizeFilter(policy));
            }
        });
        return conventions;
    }
 
    /// <summary>
    /// Requires authorization for 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)
    {
        ArgumentNullException.ThrowIfNull(conventions);
        ArgumentException.ThrowIfNullOrEmpty(folderPath);
 
        conventions.AddFolderApplicationModelConvention(folderPath, model =>
        {
            if (conventions.MvcOptions.EnableEndpointRouting)
            {
                model.EndpointMetadata.Add(new AuthorizeAttribute(policy));
            }
            else
            {
                model.Filters.Add(new AuthorizeFilter(policy));
            }
        });
        return conventions;
    }
 
    /// <summary>
    /// Requires authorization for 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>
    /// Requires authorization with the default policy for 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>
    /// Requires authorization with the specified policy for 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)
    {
        ArgumentNullException.ThrowIfNull(conventions);
        ArgumentException.ThrowIfNullOrEmpty(areaName);
        ArgumentException.ThrowIfNullOrEmpty(folderPath);
 
        conventions.AddAreaFolderApplicationModelConvention(areaName, folderPath, model =>
        {
            if (conventions.MvcOptions.EnableEndpointRouting)
            {
                model.EndpointMetadata.Add(new AuthorizeAttribute(policy));
            }
            else
            {
                model.Filters.Add(new AuthorizeFilter(policy));
            }
        });
        return conventions;
    }
 
    /// <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, [StringSyntax("Route")] string route)
    {
        ArgumentNullException.ThrowIfNull(conventions);
        ArgumentException.ThrowIfNullOrEmpty(pageName);
        ArgumentNullException.ThrowIfNull(route);
 
        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,
        [StringSyntax("Route")] string route)
    {
        ArgumentNullException.ThrowIfNull(conventions);
        ArgumentException.ThrowIfNullOrEmpty(areaName);
        ArgumentException.ThrowIfNullOrEmpty(pageName);
        ArgumentNullException.ThrowIfNull(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)
            {
                selector.AttributeRouteModel!.SuppressLinkGeneration = true;
            }
 
            model.Selectors.Add(new SelectorModel
            {
                AttributeRouteModel = new AttributeRouteModel
                {
                    Template = route,
                }
            });
        };
    }
 
    private sealed class ParameterModelBaseConventionAdapter : IPageConvention, IParameterModelBaseConvention
    {
        private readonly IParameterModelBaseConvention _convention;
 
        public ParameterModelBaseConventionAdapter(IParameterModelBaseConvention convention)
        {
            _convention = convention;
        }
 
        public void Apply(ParameterModelBase parameter)
        {
            _convention.Apply(parameter);
        }
    }
}