File: ApplicationModels\AttributeRouteModel.cs
Web Access
Project: src\src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj (Microsoft.AspNetCore.Mvc.Core)
// 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 System.Linq;
using System.Text;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
 
namespace Microsoft.AspNetCore.Mvc.ApplicationModels;
 
/// <summary>
/// A model for attribute routes.
/// </summary>
public class AttributeRouteModel
{
    private static readonly AttributeRouteModel _default = new AttributeRouteModel();
 
    /// <summary>
    /// Initializes a new instance of <see cref="AttributeRoute"/>.
    /// </summary>
    public AttributeRouteModel()
    {
    }
 
    /// <summary>
    /// Initializes a new instance of <see cref="AttributeRoute"/> using the specified <paramref name="templateProvider"/>.
    /// </summary>
    /// <param name="templateProvider">The <see cref="IRouteTemplateProvider"/>.</param>
    public AttributeRouteModel(IRouteTemplateProvider templateProvider)
    {
        ArgumentNullException.ThrowIfNull(templateProvider);
 
        Attribute = templateProvider;
        Template = templateProvider.Template;
        Order = templateProvider.Order;
        Name = templateProvider.Name;
    }
 
    /// <summary>
    /// Copy constructor for <see cref="AttributeRoute"/>.
    /// </summary>
    /// <param name="other">The <see cref="AttributeRouteModel"/> to copy.</param>
    public AttributeRouteModel(AttributeRouteModel other)
    {
        ArgumentNullException.ThrowIfNull(other);
 
        Attribute = other.Attribute;
        Name = other.Name;
        Order = other.Order;
        Template = other.Template;
        SuppressLinkGeneration = other.SuppressLinkGeneration;
        SuppressPathMatching = other.SuppressPathMatching;
    }
 
    /// <summary>
    /// Gets the <see cref="IRouteTemplateProvider"/>.
    /// </summary>
    public IRouteTemplateProvider? Attribute { get; }
 
    /// <summary>
    /// Gets or sets the attribute route template.
    /// </summary>
    [StringSyntax("Route")]
    public string? Template { get; set; }
 
    /// <summary>
    /// Gets or sets the route order.
    /// </summary>
    public int? Order { get; set; }
 
    /// <summary>
    /// Gets or sets the route name.
    /// </summary>
    public string? Name { get; set; }
 
    /// <summary>
    /// Gets or sets a value that determines if this model participates in link generation.
    /// </summary>
    public bool SuppressLinkGeneration { get; set; }
 
    /// <summary>
    /// Gets or sets a value that determines if this model participates in path matching (inbound routing).
    /// </summary>
    public bool SuppressPathMatching { get; set; }
 
    /// <summary>
    /// Gets or sets a value that determines if this route template for this model overrides the route template at the parent scope.
    /// </summary>
    public bool IsAbsoluteTemplate => Template != null && IsOverridePattern(Template);
 
    /// <summary>
    /// Combines two <see cref="AttributeRouteModel"/> instances and returns
    /// a new <see cref="AttributeRouteModel"/> instance with the result.
    /// </summary>
    /// <param name="left">The left <see cref="AttributeRouteModel"/>.</param>
    /// <param name="right">The right <see cref="AttributeRouteModel"/>.</param>
    /// <returns>A new instance of <see cref="AttributeRouteModel"/> that represents the
    /// combination of the two <see cref="AttributeRouteModel"/> instances or <c>null</c> if both
    /// parameters are <c>null</c>.</returns>
    public static AttributeRouteModel? CombineAttributeRouteModel(
        AttributeRouteModel? left,
        AttributeRouteModel? right)
    {
        right = right ?? _default;
 
        // If the right template is an override template (starts with / or ~/)
        // we ignore the values from left.
        if (left == null || IsOverridePattern(right.Template))
        {
            left = _default;
        }
 
        var combinedTemplate = CombineTemplates(left.Template, right.Template);
 
        // The action is not attribute routed.
        if (combinedTemplate == null)
        {
            return null;
        }
 
        return new AttributeRouteModel()
        {
            Template = combinedTemplate,
            Order = right.Order ?? left.Order,
            Name = ChooseName(left, right),
            SuppressLinkGeneration = left.SuppressLinkGeneration || right.SuppressLinkGeneration,
            SuppressPathMatching = left.SuppressPathMatching || right.SuppressPathMatching,
        };
    }
 
    /// <summary>
    /// Combines the prefix and route template for an attribute route.
    /// </summary>
    /// <param name="prefix">The prefix.</param>
    /// <param name="template">The route template.</param>
    /// <returns>The combined pattern.</returns>
    public static string? CombineTemplates([StringSyntax("Route")] string? prefix, [StringSyntax("Route")] string? template)
    {
        var result = CombineCore(prefix, template);
        return CleanTemplate(result);
    }
 
    /// <summary>
    /// Determines if a template pattern can be used to override a prefix.
    /// </summary>
    /// <param name="template">The template.</param>
    /// <returns><c>true</c> if this is an overriding template, <c>false</c> otherwise.</returns>
    /// <remarks>
    /// Route templates starting with "~/" or "/" can be used to override the prefix.
    /// </remarks>
    public static bool IsOverridePattern([StringSyntax("Route")] string? template)
    {
        return template != null &&
            (template.StartsWith("~/", StringComparison.Ordinal) ||
            template.StartsWith('/'));
    }
 
    private static string? ChooseName(
        AttributeRouteModel left,
        AttributeRouteModel right)
    {
        if (right.Name == null && string.IsNullOrEmpty(right.Template))
        {
            return left.Name;
        }
        else
        {
            return right.Name;
        }
    }
 
    private static string? CombineCore(string? left, string? right)
    {
        if (left == null && right == null)
        {
            return null;
        }
        else if (right == null)
        {
            return left;
        }
        else if (IsEmptyLeftSegment(left) || IsOverridePattern(right))
        {
            return right;
        }
 
        if (left!.EndsWith('/'))
        {
            return left + right;
        }
 
        // Both templates contain some text.
        return left + "/" + right;
    }
 
    private static bool IsEmptyLeftSegment(string? template)
    {
        return template == null ||
            template.Equals(string.Empty, StringComparison.Ordinal) ||
            template.Equals("~/", StringComparison.Ordinal) ||
            template.Equals("/", StringComparison.Ordinal);
    }
 
    private static string? CleanTemplate(string? result)
    {
        if (result == null)
        {
            return null;
        }
 
        // This is an invalid combined template, so we don't want to
        // accidentally clean it and produce a valid template. For that
        // reason we ignore the clean up process for it.
        if (result.Equals("//", StringComparison.Ordinal))
        {
            return result;
        }
 
        var startIndex = 0;
        if (result.StartsWith('/'))
        {
            startIndex = 1;
        }
        else if (result.StartsWith("~/", StringComparison.Ordinal))
        {
            startIndex = 2;
        }
 
        // We are in the case where the string is "/" or "~/"
        if (startIndex == result.Length)
        {
            return string.Empty;
        }
 
        var subStringLength = result.Length - startIndex;
        if (result.EndsWith('/'))
        {
            subStringLength--;
        }
 
        return result.Substring(startIndex, subStringLength);
    }
 
    /// <summary>
    /// Replaces the tokens in the template with the provided values.
    /// </summary>
    /// <param name="template">The template.</param>
    /// <param name="values">The token values to use.</param>
    /// <returns>A new string with the replaced values.</returns>
    public static string ReplaceTokens([StringSyntax("Route")] string template, IDictionary<string, string?> values)
    {
        return ReplaceTokens(template, values, routeTokenTransformer: null);
    }
 
    /// <summary>
    /// Replaces the tokens in the template with the provided values and route token transformer.
    /// </summary>
    /// <param name="template">The template.</param>
    /// <param name="values">The token values to use.</param>
    /// <param name="routeTokenTransformer">The route token transformer.</param>
    /// <returns>A new string with the replaced values.</returns>
    public static string ReplaceTokens([StringSyntax("Route")] string template, IDictionary<string, string?> values, IOutboundParameterTransformer? routeTokenTransformer)
    {
        var builder = new StringBuilder();
        var state = TemplateParserState.Plaintext;
 
        int? tokenStart = null;
        var scope = 0;
 
        // We'll run the loop one extra time with 'null' to detect the end of the string.
        for (var i = 0; i <= template.Length; i++)
        {
            var c = i < template.Length ? (char?)template[i] : null;
            switch (state)
            {
                case TemplateParserState.Plaintext:
                    if (c == '[')
                    {
                        scope++;
                        state = TemplateParserState.SeenLeft;
                        break;
                    }
                    else if (c == ']')
                    {
                        state = TemplateParserState.SeenRight;
                        break;
                    }
                    else if (c == null)
                    {
                        // We're at the end of the string, nothing left to do.
                        break;
                    }
                    else
                    {
                        builder.Append(c);
                        break;
                    }
                case TemplateParserState.SeenLeft:
                    if (c == '[')
                    {
                        // This is an escaped left-bracket
                        builder.Append(c);
                        state = TemplateParserState.Plaintext;
                        break;
                    }
                    else if (c == ']')
                    {
                        // This is zero-width parameter - not allowed.
                        var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
                            template,
                            Resources.AttributeRoute_TokenReplacement_EmptyTokenNotAllowed);
                        throw new InvalidOperationException(message);
                    }
                    else if (c == null)
                    {
                        // This is a left-bracket at the end of the string.
                        var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
                            template,
                            Resources.AttributeRoute_TokenReplacement_UnclosedToken);
                        throw new InvalidOperationException(message);
                    }
                    else
                    {
                        tokenStart = i;
                        state = TemplateParserState.InsideToken;
                        break;
                    }
                case TemplateParserState.SeenRight:
                    if (c == ']')
                    {
                        // This is an escaped right-bracket
                        builder.Append(c);
                        state = TemplateParserState.Plaintext;
                        break;
                    }
                    else if (c == null)
                    {
                        // This is an imbalanced right-bracket at the end of the string.
                        var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
                            template,
                            Resources.AttributeRoute_TokenReplacement_ImbalancedSquareBrackets);
                        throw new InvalidOperationException(message);
                    }
                    else
                    {
                        // This is an imbalanced right-bracket.
                        var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
                            template,
                            Resources.AttributeRoute_TokenReplacement_ImbalancedSquareBrackets);
                        throw new InvalidOperationException(message);
                    }
                case TemplateParserState.InsideToken:
                    if (c == '[')
                    {
                        state = TemplateParserState.InsideToken | TemplateParserState.SeenLeft;
                        break;
                    }
                    else if (c == ']')
                    {
                        --scope;
                        state = TemplateParserState.InsideToken | TemplateParserState.SeenRight;
                        break;
                    }
                    else if (c == null)
                    {
                        // This is an unclosed replacement token
                        var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
                            template,
                            Resources.AttributeRoute_TokenReplacement_UnclosedToken);
                        throw new InvalidOperationException(message);
                    }
                    else
                    {
                        // This is a just part of the parameter
                        break;
                    }
                case TemplateParserState.InsideToken | TemplateParserState.SeenLeft:
                    if (c == '[')
                    {
                        // This is an escaped left-bracket
                        state = TemplateParserState.InsideToken;
                        break;
                    }
                    else
                    {
                        // Unescaped left-bracket is not allowed inside a token.
                        var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
                            template,
                            Resources.AttributeRoute_TokenReplacement_UnescapedBraceInToken);
                        throw new InvalidOperationException(message);
                    }
                case TemplateParserState.InsideToken | TemplateParserState.SeenRight:
                    if (c == ']' && scope == 0)
                    {
                        // This is an escaped right-bracket
                        state = TemplateParserState.InsideToken;
                        break;
                    }
                    else
                    {
                        // This is the end of a replacement token.
                        var token = template
                            .Substring(tokenStart!.Value, i - tokenStart.Value - 1)
                            .Replace("[[", "[")
                            .Replace("]]", "]");
 
                        if (!values.TryGetValue(token, out var value))
                        {
                            // Value not found
                            var message = Resources.FormatAttributeRoute_TokenReplacement_ReplacementValueNotFound(
                                template,
                                token,
                                string.Join(", ", values.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase)));
                            throw new InvalidOperationException(message);
                        }
 
                        if (routeTokenTransformer != null)
                        {
                            value = routeTokenTransformer.TransformOutbound(value);
                        }
 
                        builder.Append(value);
 
                        if (c == '[')
                        {
                            state = TemplateParserState.SeenLeft;
                        }
                        else if (c == ']')
                        {
                            state = TemplateParserState.SeenRight;
                        }
                        else if (c == null)
                        {
                            state = TemplateParserState.Plaintext;
                        }
                        else
                        {
                            builder.Append(c);
                            state = TemplateParserState.Plaintext;
                        }
 
                        scope = 0;
                        tokenStart = null;
                        break;
                    }
            }
        }
 
        return builder.ToString();
    }
 
    [Flags]
    private enum TemplateParserState : uint
    {
        // default state - allow non-special characters to pass through to the
        // buffer.
        Plaintext = 0,
 
        // We're inside a replacement token, may be combined with other states to detect
        // a possible escaped bracket inside the token.
        InsideToken = 1,
 
        // We've seen a left brace, need to see the next character to find out if it's escaped
        // or not.
        SeenLeft = 2,
 
        // We've seen a right brace, need to see the next character to find out if it's escaped
        // or not.
        SeenRight = 4,
    }
}