| File: ApplicationModels\AttributeRouteModel.cs | Web Access |
| Project: src\aspnetcore\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, } }