File: Infrastructure\PageActionDescriptorProvider.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.Linq;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.Options;
 
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
 
/// <summary>
/// A <see cref="IActionDescriptorProvider"/> for PageActions
/// </summary>
public class PageActionDescriptorProvider : IActionDescriptorProvider
{
    private readonly IPageRouteModelProvider[] _routeModelProviders;
    private readonly MvcOptions _mvcOptions;
    private readonly IPageRouteModelConvention[] _conventions;
 
    /// <summary>
    /// Instantiates a new instance of <see cref="PageActionDescriptorProvider"/>.
    /// </summary>
    /// <param name="pageRouteModelProviders">The <see cref="IPageRouteModelProvider"/>s to use.</param>
    /// <param name="mvcOptionsAccessor">The <see cref="MvcOptions"/>.</param>
    /// <param name="pagesOptionsAccessor">The <see cref="RazorPagesOptions"/>.</param>
    public PageActionDescriptorProvider(
        IEnumerable<IPageRouteModelProvider> pageRouteModelProviders,
        IOptions<MvcOptions> mvcOptionsAccessor,
        IOptions<RazorPagesOptions> pagesOptionsAccessor)
    {
        _routeModelProviders = pageRouteModelProviders.OrderBy(p => p.Order).ToArray();
        _mvcOptions = mvcOptionsAccessor.Value;
 
        _conventions = pagesOptionsAccessor.Value.Conventions
            .OfType<IPageRouteModelConvention>()
            .ToArray();
    }
 
    /// <inheritdoc/>
    public int Order { get; set; } = -900; // Run after the default MVC provider, but before others.
 
    /// <inheritdoc/>
    public void OnProvidersExecuting(ActionDescriptorProviderContext context)
    {
        var pageRouteModels = BuildModel();
 
        for (var i = 0; i < pageRouteModels.Count; i++)
        {
            AddActionDescriptors(context.Results, pageRouteModels[i]);
        }
    }
 
    /// <summary>
    /// Build the model.
    /// </summary>
    /// <returns>The list of <see cref="PageRouteModel"/>.</returns>
    protected IList<PageRouteModel> BuildModel()
    {
        var context = new PageRouteModelProviderContext();
 
        for (var i = 0; i < _routeModelProviders.Length; i++)
        {
            _routeModelProviders[i].OnProvidersExecuting(context);
        }
 
        for (var i = _routeModelProviders.Length - 1; i >= 0; i--)
        {
            _routeModelProviders[i].OnProvidersExecuted(context);
        }
 
        return context.RouteModels;
    }
 
    /// <inheritdoc/>
    public void OnProvidersExecuted(ActionDescriptorProviderContext context)
    {
    }
 
    private void AddActionDescriptors(IList<ActionDescriptor> actions, PageRouteModel model)
    {
        for (var i = 0; i < _conventions.Length; i++)
        {
            _conventions[i].Apply(model);
        }
 
        foreach (var selector in model.Selectors)
        {
            var descriptor = new PageActionDescriptor
            {
                ActionConstraints = selector.ActionConstraints.ToList(),
                AreaName = model.AreaName,
                AttributeRouteInfo = new AttributeRouteInfo
                {
                    Name = selector.AttributeRouteModel!.Name,
                    Order = selector.AttributeRouteModel.Order ?? 0,
                    Template = TransformPageRoute(model, selector),
                    SuppressLinkGeneration = selector.AttributeRouteModel.SuppressLinkGeneration,
                    SuppressPathMatching = selector.AttributeRouteModel.SuppressPathMatching,
                },
                DisplayName = $"Page: {model.ViewEnginePath}",
                EndpointMetadata = selector.EndpointMetadata.ToList(),
                FilterDescriptors = Array.Empty<FilterDescriptor>(),
                Properties = new Dictionary<object, object?>(model.Properties),
                RelativePath = model.RelativePath,
                ViewEnginePath = model.ViewEnginePath,
            };
 
            foreach (var kvp in model.RouteValues)
            {
                if (!descriptor.RouteValues.ContainsKey(kvp.Key))
                {
                    descriptor.RouteValues.Add(kvp.Key, kvp.Value);
                }
            }
 
            if (!descriptor.RouteValues.ContainsKey("page"))
            {
                descriptor.RouteValues.Add("page", model.ViewEnginePath);
            }
 
            actions.Add(descriptor);
        }
    }
 
    private static string? TransformPageRoute(PageRouteModel model, SelectorModel selectorModel)
    {
        // Transformer not set on page route
        if (model.RouteParameterTransformer == null)
        {
            return selectorModel.AttributeRouteModel!.Template;
        }
 
        var pageRouteMetadata = selectorModel.EndpointMetadata.OfType<PageRouteMetadata>().SingleOrDefault();
        if (pageRouteMetadata == null)
        {
            // Selector does not have expected metadata
            // This selector was likely configured by AddPageRouteModelConvention
            // Use the existing explicitly configured template
            return selectorModel.AttributeRouteModel!.Template;
        }
 
        var segments = (string?[])pageRouteMetadata.PageRoute.Split('/');
        for (var i = 0; i < segments.Length; i++)
        {
            segments[i] = model.RouteParameterTransformer.TransformOutbound(segments[i]);
        }
 
        var transformedPageRoute = string.Join("/", segments);
 
        // Combine transformed page route with template
        return AttributeRouteModel.CombineTemplates(transformedPageRoute, pageRouteMetadata.RouteTemplate);
    }
}