File: ApiExplorer\ApiConventionMatcher.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.Reflection;
 
namespace Microsoft.AspNetCore.Mvc.ApiExplorer;
 
internal static class ApiConventionMatcher
{
    internal static bool IsMatch(MethodInfo methodInfo, MethodInfo conventionMethod)
    {
        return MethodMatches() && ParametersMatch();
 
        bool MethodMatches()
        {
            var methodNameMatchBehavior = GetNameMatchBehavior(conventionMethod);
            return IsNameMatch(methodInfo.Name, conventionMethod.Name, methodNameMatchBehavior);
        }
 
        bool ParametersMatch()
        {
            var methodParameters = methodInfo.GetParameters();
            var conventionMethodParameters = conventionMethod.GetParameters();
 
            for (var i = 0; i < conventionMethodParameters.Length; i++)
            {
                var conventionParameter = conventionMethodParameters[i];
                if (conventionParameter.IsDefined(typeof(ParamArrayAttribute)))
                {
                    return true;
                }
 
                if (methodParameters.Length <= i)
                {
                    return false;
                }
 
                var nameMatchBehavior = GetNameMatchBehavior(conventionParameter);
                var typeMatchBehavior = GetTypeMatchBehavior(conventionParameter);
 
                if (!IsTypeMatch(methodParameters[i].ParameterType, conventionParameter.ParameterType, typeMatchBehavior) ||
                    !IsNameMatch(methodParameters[i].Name, conventionParameter.Name, nameMatchBehavior))
                {
                    return false;
                }
            }
 
            // Ensure convention has at least as many parameters as the method. params convention argument are handled
            // inside the for loop.
            return methodParameters.Length == conventionMethodParameters.Length;
        }
    }
 
    internal static ApiConventionNameMatchBehavior GetNameMatchBehavior(ICustomAttributeProvider attributeProvider)
    {
        var attribute = GetCustomAttribute<ApiConventionNameMatchAttribute>(attributeProvider);
        return attribute?.MatchBehavior ?? ApiConventionNameMatchBehavior.Exact;
    }
 
    internal static ApiConventionTypeMatchBehavior GetTypeMatchBehavior(ICustomAttributeProvider attributeProvider)
    {
        var attribute = GetCustomAttribute<ApiConventionTypeMatchAttribute>(attributeProvider);
        return attribute?.MatchBehavior ?? ApiConventionTypeMatchBehavior.AssignableFrom;
    }
 
    private static TAttribute? GetCustomAttribute<TAttribute>(ICustomAttributeProvider attributeProvider)
    {
        var attributes = attributeProvider.GetCustomAttributes(inherit: false);
        for (var i = 0; i < attributes.Length; i++)
        {
            if (attributes[i] is TAttribute attribute)
            {
                return attribute;
            }
        }
 
        return default;
    }
 
    internal static bool IsNameMatch(string? name, string? conventionName, ApiConventionNameMatchBehavior nameMatchBehavior)
    {
        switch (nameMatchBehavior)
        {
            case ApiConventionNameMatchBehavior.Any:
                return true;
 
            case ApiConventionNameMatchBehavior.Exact:
                return string.Equals(name, conventionName, StringComparison.Ordinal);
 
            case ApiConventionNameMatchBehavior.Prefix:
                return IsNameMatchPrefix();
 
            case ApiConventionNameMatchBehavior.Suffix:
                return IsNameMatchSuffix();
 
            default:
                return false;
        }
 
        bool IsNameMatchPrefix()
        {
            if (name is null || conventionName is null)
            {
                return false;
            }
 
            if (name.Length < conventionName.Length)
            {
                return false;
            }
 
            if (name.Length == conventionName.Length)
            {
                // name = "Post", conventionName = "Post"
                return string.Equals(name, conventionName, StringComparison.Ordinal);
            }
 
            if (!name.StartsWith(conventionName, StringComparison.Ordinal))
            {
                // name = "GetPerson", conventionName = "Post"
                return false;
            }
 
            // Check for name = "PostPerson", conventionName = "Post"
            // Verify the first letter after the convention name is upper case. In this case 'P' from "Person"
            return char.IsUpper(name[conventionName.Length]);
        }
 
        bool IsNameMatchSuffix()
        {
            if (name is null || conventionName is null)
            {
                return false;
            }
 
            if (name.Length < conventionName.Length)
            {
                // name = "person", conventionName = "personName"
                return false;
            }
 
            if (name.Length == conventionName.Length)
            {
                // name = id, conventionName = id
                return string.Equals(name, conventionName, StringComparison.Ordinal);
            }
 
            // Check for name = personName, conventionName = name
            var index = name.Length - conventionName.Length - 1;
            if (!char.IsLower(name[index]))
            {
                // Verify letter before "name" is lowercase. In this case the letter 'n' at the end of "person"
                return false;
            }
 
            index++;
            if (name[index] != char.ToUpperInvariant(conventionName[0]))
            {
                // Verify the first letter from convention is upper case. In this case 'n' from "name"
                return false;
            }
 
            // Match the remaining letters with exact case. i.e. match "ame" from "personName", "name"
            index++;
            return string.Compare(name, index, conventionName, 1, conventionName.Length - 1, StringComparison.Ordinal) == 0;
        }
    }
 
    internal static bool IsTypeMatch(Type type, Type conventionType, ApiConventionTypeMatchBehavior typeMatchBehavior)
    {
        switch (typeMatchBehavior)
        {
            case ApiConventionTypeMatchBehavior.Any:
                return true;
 
            case ApiConventionTypeMatchBehavior.AssignableFrom:
                return conventionType.IsAssignableFrom(type);
 
            default:
                return false;
        }
    }
}