Quellcode durchsuchen

Harden handling for generic type arguments in source generator (#61145)

* Harden handling for generic type arguments in source generator

* Address feedback

* Switch to documentation IDs for cache keys

* Remove Debugger and LINQ, format ints

* Add array tests, remove unneeded formatting, rename _cache

* Commit updated snapshots

* Quarantine OpenApiReference tests
Safia Abdalla vor 11 Monaten
Ursprung
Commit
9c67617a43
13 geänderte Dateien mit 1693 neuen und 1024 gelöschten Zeilen
  1. 189 119
      src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
  2. 4 14
      src/OpenApi/gen/XmlCommentGenerator.Parser.cs
  3. 0 143
      src/OpenApi/gen/XmlComments/MemberKey.cs
  4. 94 0
      src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/CompletenessTests.cs
  5. 135 0
      src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/OperationTests.MinimalApis.cs
  6. 185 103
      src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs
  7. 216 134
      src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.verified.cs
  8. 251 153
      src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
  9. 189 107
      src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs
  10. 208 116
      src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs
  11. 217 135
      src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
  12. 3 0
      src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddFileTests.cs
  13. 2 0
      src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs

+ 189 - 119
src/OpenApi/gen/XmlCommentGenerator.Emitter.cs

@@ -8,7 +8,6 @@ using Microsoft.CodeAnalysis;
 using Microsoft.CodeAnalysis.CSharp;
 using Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
 using System.Threading;
-using System.Linq;
 
 namespace Microsoft.AspNetCore.OpenApi.SourceGenerators;
 
@@ -48,8 +47,10 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
     using System;
     using System.Collections.Generic;
     using System.Diagnostics.CodeAnalysis;
+    using System.Globalization;
     using System.Linq;
     using System.Reflection;
+    using System.Text;
     using System.Text.Json;
     using System.Text.Json.Nodes;
     using System.Threading;
@@ -80,148 +81,228 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
     file record XmlResponseComment(string Code, string? Description, string? Example);
 
     {{GeneratedCodeAttribute}}
-    file sealed record MemberKey(
-        Type? DeclaringType,
-        MemberType MemberKind,
-        string? Name,
-        Type? ReturnType,
-        Type[]? Parameters) : IEquatable<MemberKey>
+    file static class XmlCommentCache
     {
-        public bool Equals(MemberKey? other)
+        private static Dictionary<string, XmlComment>? _cache;
+        public static Dictionary<string, XmlComment> Cache => _cache ??= GenerateCacheEntries();
+
+        private static Dictionary<string, XmlComment> GenerateCacheEntries()
         {
-            if (other is null) return false;
+            var cache = new Dictionary<string, XmlComment>();
+{{commentsFromXmlFile}}
+{{commentsFromCompilation}}
+            return cache;
+        }
+    }
 
-            // Check member kind
-            if (MemberKind != other.MemberKind) return false;
+    file static class DocumentationCommentIdHelper
+    {
+        /// <summary>
+        /// Generates a documentation comment ID for a type.
+        /// Example: T:Namespace.Outer+Inner`1 becomes T:Namespace.Outer.Inner`1
+        /// </summary>
+        public static string CreateDocumentationId(this Type type)
+        {
+            if (type == null)
+            {
+                throw new ArgumentNullException(nameof(type));
+            }
 
-            // Check declaring type, handling generic types
-            if (!TypesEqual(DeclaringType, other.DeclaringType)) return false;
+            return "T:" + GetTypeDocId(type, includeGenericArguments: false, omitGenericArity: false);
+        }
 
-            // Check name
-            if (Name != other.Name) return false;
+        /// <summary>
+        /// Generates a documentation comment ID for a property.
+        /// Example: P:Namespace.ContainingType.PropertyName or for an indexer P:Namespace.ContainingType.Item(System.Int32)
+        /// </summary>
+        public static string CreateDocumentationId(this PropertyInfo property)
+        {
+            if (property == null)
+            {
+                throw new ArgumentNullException(nameof(property));
+            }
+
+            var sb = new StringBuilder();
+            sb.Append("P:");
 
-            // For methods, check return type and parameters
-            if (MemberKind == MemberType.Method)
+            if (property.DeclaringType != null)
             {
-                if (!TypesEqual(ReturnType, other.ReturnType)) return false;
-                if (Parameters is null || other.Parameters is null) return false;
-                if (Parameters.Length != other.Parameters.Length) return false;
+                sb.Append(GetTypeDocId(property.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
+            }
+
+            sb.Append('.');
+            sb.Append(property.Name);
 
-                for (int i = 0; i < Parameters.Length; i++)
+            // For indexers, include the parameter list.
+            var indexParams = property.GetIndexParameters();
+            if (indexParams.Length > 0)
+            {
+                sb.Append('(');
+                for (int i = 0; i < indexParams.Length; i++)
                 {
-                    if (!TypesEqual(Parameters[i], other.Parameters[i])) return false;
+                    if (i > 0)
+                    {
+                        sb.Append(',');
+                    }
+
+                    sb.Append(GetTypeDocId(indexParams[i].ParameterType, includeGenericArguments: true, omitGenericArity: false));
                 }
+                sb.Append(')');
             }
 
-            return true;
+            return sb.ToString();
         }
 
-        private static bool TypesEqual(Type? type1, Type? type2)
+        /// <summary>
+        /// Generates a documentation comment ID for a method (or constructor).
+        /// For example:
+        ///   M:Namespace.ContainingType.MethodName(ParamType1,ParamType2)~ReturnType
+        ///   M:Namespace.ContainingType.#ctor(ParamType)
+        /// </summary>
+        public static string CreateDocumentationId(this MethodInfo method)
         {
-            if (type1 == type2) return true;
-            if (type1 == null || type2 == null) return false;
+            if (method == null)
+            {
+                throw new ArgumentNullException(nameof(method));
+            }
 
-            if (type1.IsGenericType && type2.IsGenericType)
+            var sb = new StringBuilder();
+            sb.Append("M:");
+
+            // Append the fully qualified name of the declaring type.
+            if (method.DeclaringType != null)
             {
-                return type1.GetGenericTypeDefinition() == type2.GetGenericTypeDefinition();
+                sb.Append(GetTypeDocId(method.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
             }
 
-            return type1 == type2;
-        }
+            sb.Append('.');
 
-        public override int GetHashCode()
-        {
-            var hash = new HashCode();
-            hash.Add(GetTypeHashCode(DeclaringType));
-            hash.Add(MemberKind);
-            hash.Add(Name);
+            // Append the method name, handling constructors specially.
+            if (method.IsConstructor)
+            {
+                sb.Append(method.IsStatic ? "#cctor" : "#ctor");
+            }
+            else
+            {
+                sb.Append(method.Name);
+                if (method.IsGenericMethod)
+                {
+                    sb.Append("``");
+                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", method.GetGenericArguments().Length);
+                }
+            }
 
-            if (MemberKind == MemberType.Method)
+            // Append the parameter list, if any.
+            var parameters = method.GetParameters();
+            if (parameters.Length > 0)
             {
-                hash.Add(GetTypeHashCode(ReturnType));
-                if (Parameters is not null)
+                sb.Append('(');
+                for (int i = 0; i < parameters.Length; i++)
                 {
-                    foreach (var param in Parameters)
+                    if (i > 0)
                     {
-                        hash.Add(GetTypeHashCode(param));
+                        sb.Append(',');
                     }
+
+                    // Omit the generic arity for the parameter type.
+                    sb.Append(GetTypeDocId(parameters[i].ParameterType, includeGenericArguments: true, omitGenericArity: true));
                 }
+                sb.Append(')');
             }
 
-            return hash.ToHashCode();
-        }
+            // Append the return type after a '~' (if the method returns a value).
+            if (method.ReturnType != typeof(void))
+            {
+                sb.Append('~');
+                // Omit the generic arity for the return type.
+                sb.Append(GetTypeDocId(method.ReturnType, includeGenericArguments: true, omitGenericArity: true));
+            }
 
-        private static int GetTypeHashCode(Type? type)
-        {
-            if (type == null) return 0;
-            return type.IsGenericType ? type.GetGenericTypeDefinition().GetHashCode() : type.GetHashCode();
+            return sb.ToString();
         }
 
-        public static MemberKey FromMethodInfo(MethodInfo method)
+        /// <summary>
+        /// Generates a documentation ID string for a type.
+        /// This method handles nested types (replacing '+' with '.'),
+        /// generic types, arrays, pointers, by-ref types, and generic parameters.
+        /// The <paramref name="includeGenericArguments"/> flag controls whether
+        /// constructed generic type arguments are emitted, while <paramref name="omitGenericArity"/>
+        /// controls whether the generic arity marker (e.g. "`1") is appended.
+        /// </summary>
+        private static string GetTypeDocId(Type type, bool includeGenericArguments, bool omitGenericArity)
         {
-            return new MemberKey(
-                method.DeclaringType,
-                MemberType.Method,
-                method.Name,
-                method.ReturnType.IsGenericParameter ? typeof(object) : method.ReturnType,
-                method.GetParameters().Select(p => p.ParameterType.IsGenericParameter ? typeof(object) : p.ParameterType).ToArray());
-        }
+            if (type.IsGenericParameter)
+            {
+                // Use `` for method-level generic parameters and ` for type-level.
+                if (type.DeclaringMethod != null)
+                {
+                    return "``" + type.GenericParameterPosition;
+                }
+                else if (type.DeclaringType != null)
+                {
+                    return "`" + type.GenericParameterPosition;
+                }
+                else
+                {
+                    return type.Name;
+                }
+            }
 
-        public static MemberKey FromPropertyInfo(PropertyInfo property)
-        {
-            return new MemberKey(
-                property.DeclaringType,
-                MemberType.Property,
-                property.Name,
-                null,
-                null);
-        }
+            if (type.IsGenericType)
+            {
+                Type genericDef = type.GetGenericTypeDefinition();
+                string fullName = genericDef.FullName ?? genericDef.Name;
 
-        public static MemberKey FromTypeInfo(Type type)
-        {
-            return new MemberKey(
-                type,
-                MemberType.Type,
-                null,
-                null,
-                null);
-        }
-    }
+                var sb = new StringBuilder(fullName.Length);
 
-    file enum MemberType
-    {
-        Type,
-        Property,
-        Method
-    }
+                // Replace '+' with '.' for nested types
+                for (var i = 0; i < fullName.Length; i++)
+                {
+                    char c = fullName[i];
+                    if (c == '+')
+                    {
+                        sb.Append('.');
+                    }
+                    else if (c == '`')
+                    {
+                        break;
+                    }
+                    else
+                    {
+                        sb.Append(c);
+                    }
+                }
 
-    {{GeneratedCodeAttribute}}
-    file static class XmlCommentCache
-    {
-        private static Dictionary<MemberKey, XmlComment>? _cache;
-        public static Dictionary<MemberKey, XmlComment> Cache => _cache ??= GenerateCacheEntries();
+                if (!omitGenericArity)
+                {
+                    int arity = genericDef.GetGenericArguments().Length;
+                    sb.Append('`');
+                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", arity);
+                }
 
-        private static Dictionary<MemberKey, XmlComment> GenerateCacheEntries()
-        {
-            var _cache = new Dictionary<MemberKey, XmlComment>();
-{{commentsFromXmlFile}}
-{{commentsFromCompilation}}
-            return _cache;
-        }
+                if (includeGenericArguments && !type.IsGenericTypeDefinition)
+                {
+                    var typeArgs = type.GetGenericArguments();
+                    sb.Append('{');
 
-        internal static bool TryGetXmlComment(Type? type, MethodInfo? methodInfo, [NotNullWhen(true)] out XmlComment? xmlComment)
-        {
-            if (methodInfo is null)
-            {
-                return Cache.TryGetValue(new MemberKey(type, MemberType.Property, null, null, null), out xmlComment);
-            }
+                    for (int i = 0; i < typeArgs.Length; i++)
+                    {
+                        if (i > 0)
+                        {
+                            sb.Append(',');
+                        }
 
-            return Cache.TryGetValue(MemberKey.FromMethodInfo(methodInfo), out xmlComment);
-        }
+                        sb.Append(GetTypeDocId(typeArgs[i], includeGenericArguments, omitGenericArity));
+                    }
 
-        internal static bool TryGetXmlComment(Type? type, string? memberName, [NotNullWhen(true)] out XmlComment? xmlComment)
-        {
-            return Cache.TryGetValue(new MemberKey(type, memberName is null ? MemberType.Type : MemberType.Property, memberName, null, null), out xmlComment);
+                    sb.Append('}');
+                }
+
+                return sb.ToString();
+            }
+
+            // For non-generic types, use FullName (if available) and replace nested type separators.
+            return (type.FullName ?? type.Name).Replace('+', '.');
         }
     }
 
@@ -238,7 +319,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
             {
                 return Task.CompletedTask;
             }
-            if (XmlCommentCache.TryGetXmlComment(methodInfo.DeclaringType, methodInfo, out var methodComment))
+            if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
             {
                 if (methodComment.Summary is { } summary)
                 {
@@ -311,7 +392,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
         {
             if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
             {
-                if (XmlCommentCache.TryGetXmlComment(propertyInfo.DeclaringType, propertyInfo.Name, out var propertyComment))
+                if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
                 {
                     schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
                     if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -320,7 +401,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
                     }
                 }
             }
-            if (XmlCommentCache.TryGetXmlComment(context.JsonTypeInfo.Type, (string?)null, out var typeComment))
+            if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
             {
                 schema.Description = typeComment.Summary;
                 if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -434,7 +515,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
         return writer.ToString();
     }
 
-    internal static string EmitCommentsCache(IEnumerable<(MemberKey MemberKey, XmlComment? Comment)> comments, CancellationToken cancellationToken)
+    internal static string EmitCommentsCache(IEnumerable<(string MemberKey, XmlComment? Comment)> comments, CancellationToken cancellationToken)
     {
         var writer = new StringWriter();
         var codeWriter = new CodeWriter(writer, baseIndent: 3);
@@ -442,21 +523,10 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
         {
             if (comment is not null)
             {
-                codeWriter.WriteLine($"_cache.Add(new MemberKey(" +
-                    $"{FormatLiteralOrNull(memberKey.DeclaringType)}, " +
-                    $"MemberType.{memberKey.MemberKind}, " +
-                    $"{FormatLiteralOrNull(memberKey.Name, true)}, " +
-                    $"{FormatLiteralOrNull(memberKey.ReturnType)}, " +
-                    $"[{(memberKey.Parameters != null ? string.Join(", ", memberKey.Parameters.Select(p => SymbolDisplay.FormatLiteral(p, false))) : "")}]), " +
-                    $"{EmitSourceGeneratedXmlComment(comment)});");
+                codeWriter.WriteLine($"cache.Add({FormatStringForCode(memberKey)}, {EmitSourceGeneratedXmlComment(comment)});");
             }
         }
         return writer.ToString();
-
-        static string FormatLiteralOrNull(string? input, bool quote = false)
-        {
-            return input == null ? "null" : SymbolDisplay.FormatLiteral(input, quote);
-        }
     }
 
     private static string FormatStringForCode(string? input)

+ 4 - 14
src/OpenApi/gen/XmlCommentGenerator.Parser.cs

@@ -83,18 +83,18 @@ public sealed partial class XmlCommentGenerator
         return comments;
     }
 
-    internal static IEnumerable<(MemberKey, XmlComment?)> ParseComments(
+    internal static IEnumerable<(string, XmlComment?)> ParseComments(
         (List<(string, string)> RawComments, Compilation Compilation) input,
         CancellationToken cancellationToken)
     {
         var compilation = input.Compilation;
-        var comments = new List<(MemberKey, XmlComment?)>();
+        var comments = new List<(string, XmlComment?)>();
         foreach (var (name, value) in input.RawComments)
         {
             if (DocumentationCommentId.GetFirstSymbolForDeclarationId(name, compilation) is ISymbol symbol &&
                 // Only include symbols that are declared in the application assembly or are
                 // accessible from the application assembly.
-                (SymbolEqualityComparer.Default.Equals(symbol.ContainingAssembly, input.Compilation.Assembly) || symbol.IsAccessibleType()) &&
+                (SymbolEqualityComparer.Default.Equals(symbol.ContainingAssembly, compilation.Assembly) || symbol.IsAccessibleType()) &&
                 // Skip static classes that are just containers for members with annotations
                 // since they cannot be instantiated.
                 symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class, IsStatic: true })
@@ -102,17 +102,7 @@ public sealed partial class XmlCommentGenerator
                 var parsedComment = XmlComment.Parse(symbol, compilation, value, cancellationToken);
                 if (parsedComment is not null)
                 {
-                    var memberKey = symbol switch
-                    {
-                        IMethodSymbol methodSymbol => MemberKey.FromMethodSymbol(methodSymbol, input.Compilation),
-                        IPropertySymbol propertySymbol => MemberKey.FromPropertySymbol(propertySymbol),
-                        INamedTypeSymbol typeSymbol => MemberKey.FromTypeSymbol(typeSymbol),
-                        _ => null
-                    };
-                    if (memberKey is not null)
-                    {
-                        comments.Add((memberKey, parsedComment));
-                    }
+                    comments.Add((name, parsedComment));
                 }
             }
         }

+ 0 - 143
src/OpenApi/gen/XmlComments/MemberKey.cs

@@ -1,143 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using Microsoft.CodeAnalysis;
-
-namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
-
-internal sealed record MemberKey(
-    string? DeclaringType,
-    MemberType MemberKind,
-    string? Name,
-    string? ReturnType,
-    string[]? Parameters) : IEquatable<MemberKey>
-{
-    private static readonly SymbolDisplayFormat _typeKeyFormat = new(
-        globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included,
-        typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
-        genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters);
-
-    public static MemberKey FromMethodSymbol(IMethodSymbol method, Compilation compilation)
-    {
-        string returnType;
-        if (method.ReturnsVoid)
-        {
-            returnType = "typeof(void)";
-        }
-        else
-        {
-            // Handle Task/ValueTask for async methods
-            var actualReturnType = method.ReturnType;
-            if (method.IsAsync && actualReturnType is INamedTypeSymbol namedType)
-            {
-                if (namedType.TypeArguments.Length > 0)
-                {
-                    actualReturnType = namedType.TypeArguments[0];
-                }
-                else
-                {
-                    actualReturnType = compilation.GetSpecialType(SpecialType.System_Void);
-                }
-            }
-
-            returnType = actualReturnType.TypeKind == TypeKind.TypeParameter
-                ? "typeof(object)"
-                : $"typeof({ReplaceGenericArguments(actualReturnType.ToDisplayString(_typeKeyFormat))})";
-        }
-
-        // Handle extension methods by skipping the 'this' parameter
-        var parameters = method.Parameters
-            .Where(p => !p.IsThis)
-            .Select(p =>
-            {
-                if (p.Type.TypeKind == TypeKind.TypeParameter)
-                {
-                    return "typeof(object)";
-                }
-
-                // For params arrays, use the array type
-                if (p.IsParams && p.Type is IArrayTypeSymbol arrayType)
-                {
-                    return $"typeof({ReplaceGenericArguments(arrayType.ToDisplayString(_typeKeyFormat))})";
-                }
-
-                return $"typeof({ReplaceGenericArguments(p.Type.ToDisplayString(_typeKeyFormat))})";
-            })
-            .ToArray();
-
-        // For generic methods, use the containing type with generic parameters
-        var declaringType = method.ContainingType;
-        var typeDisplay = declaringType.ToDisplayString(_typeKeyFormat);
-
-        // If the method is in a generic type, we need to handle the type parameters
-        if (declaringType.IsGenericType)
-        {
-            typeDisplay = ReplaceGenericArguments(typeDisplay);
-        }
-
-        return new MemberKey(
-            $"typeof({typeDisplay})",
-            MemberType.Method,
-            method.MetadataName,  // Use MetadataName to match runtime MethodInfo.Name
-            returnType,
-            parameters);
-    }
-
-    public static MemberKey FromPropertySymbol(IPropertySymbol property)
-    {
-        return new MemberKey(
-            $"typeof({ReplaceGenericArguments(property.ContainingType.ToDisplayString(_typeKeyFormat))})",
-            MemberType.Property,
-            property.Name,
-            null,
-            null);
-    }
-
-    public static MemberKey FromTypeSymbol(INamedTypeSymbol type)
-    {
-        return new MemberKey(
-            $"typeof({ReplaceGenericArguments(type.ToDisplayString(_typeKeyFormat))})",
-            MemberType.Type,
-            null,
-            null,
-            null);
-    }
-
-    /// Supports replacing generic type arguments to support use of open
-    /// generics in `typeof` expressions for the declaring type.
-    private static string ReplaceGenericArguments(string typeName)
-    {
-        var stack = new Stack<int>();
-        var result = new StringBuilder(typeName);
-        for (var i = 0; i < result.Length; i++)
-        {
-            if (result[i] == '<')
-            {
-                stack.Push(i);
-            }
-            else if (result[i] == '>' && stack.Count > 0)
-            {
-                var start = stack.Pop();
-                // Replace everything between < and > with empty strings separated by commas
-                var segment = result.ToString(start + 1, i - start - 1);
-                var commaCount = segment.Count(c => c == ',');
-                var replacement = new string(',', commaCount);
-                result.Remove(start + 1, i - start - 1);
-                result.Insert(start + 1, replacement);
-                i = start + replacement.Length + 1;
-            }
-        }
-        return result.ToString();
-    }
-}
-
-internal enum MemberType
-{
-    Type,
-    Property,
-    Method
-}

+ 94 - 0
src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/CompletenessTests.cs

@@ -17,6 +17,7 @@ public class CompletenessTests
     {
         var source = """
 using System;
+using System.Collections.Generic;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.Extensions.DependencyInjection;
@@ -36,6 +37,7 @@ app.MapPost("/implementing-class", (ImplementingClass impl) => { });
 app.MapPost("/inherit-only-returns", (InheritOnlyReturns returns) => { });
 app.MapPost("/inherit-all-but-remarks", (InheritAllButRemarks remarks) => { });
 app.MapPost("/generic-class", (GenericClass<string> generic) => { });
+app.MapPost("/generic-parent", (GenericParent parent) => { });
 app.MapPost("/params-and-param-refs", (ParamsAndParamRefs refs) => { });
 
 
@@ -335,6 +337,89 @@ public class GenericClass<T>
     // Fields and members.
 }
 
+/// <summary>
+/// This class validates the behavior for mapping
+/// generic types to open generics for use in
+/// typeof expressions.
+/// </summary>
+public class GenericParent
+{
+    /// <summary>
+    /// This property is a nullable value type.
+    /// </summary>
+    public int? Id { get; set; }
+
+    /// <summary>
+    /// This property is a nullable reference type.
+    /// </summary>
+    public string? Name { get; set; }
+
+    /// <summary>
+    /// This property is a generic type containing a tuple.
+    /// </summary>
+    public Task<(int, string)> TaskOfTupleProp { get; set; }
+
+    /// <summary>
+    /// This property is a tuple with a generic type inside.
+    /// </summary>
+    public (int, Dictionary<int, string>) TupleWithGenericProp { get; set; }
+
+    /// <summary>
+    /// This property is a tuple with a nested generic type inside.
+    /// </summary>
+    public (int, Dictionary<int, Dictionary<string, int>>) TupleWithNestedGenericProp { get; set; }
+
+    /// <summary>
+    /// This method returns a generic type containing a tuple.
+    /// </summary>
+    public static Task<(int, string)> GetTaskOfTuple()
+    {
+        return Task.FromResult((1, "test"));
+    }
+
+    /// <summary>
+    /// This method returns a tuple with a generic type inside.
+    /// </summary>
+    public static (int, Dictionary<int, string>) GetTupleOfTask()
+    {
+        return (1, new Dictionary<int, string>());
+    }
+
+    /// <summary>
+    /// This method return a tuple with a generic type containing a
+    /// type parameter inside.
+    /// </summary>
+    public static (int, Dictionary<int, T>) GetTupleOfTask1<T>()
+    {
+        return (1, new Dictionary<int, T>());
+    }
+
+    /// <summary>
+    /// This method return a tuple with a generic type containing a
+    /// type parameter inside.
+    /// </summary>
+    public static (T, Dictionary<int, string>) GetTupleOfTask2<T>()
+    {
+        return (default, new Dictionary<int, string>());
+    }
+
+    /// <summary>
+    /// This method returns a nested generic with all types resolved.
+    /// </summary>
+    public static Dictionary<int, Dictionary<int, string>> GetNestedGeneric()
+    {
+        return new Dictionary<int, Dictionary<int, string>>();
+    }
+
+    /// <summary>
+    /// This method returns a nested generic with a type parameter.
+    /// </summary>
+    public static Dictionary<int, Dictionary<int, T>> GetNestedGeneric1<T>()
+    {
+        return new Dictionary<int, Dictionary<int, T>>();
+    }
+}
+
 /// <summary>
 /// This shows examples of typeparamref and typeparam tags
 /// </summary>
@@ -394,6 +479,15 @@ public class ParamsAndParamRefs
             var genericClass = path.RequestBody.Content["application/json"].Schema;
             Assert.Equal("This is a generic class.", genericClass.Description);
 
+            path = document.Paths["/generic-parent"].Operations[OperationType.Post];
+            var genericParent = path.RequestBody.Content["application/json"].Schema;
+            Assert.Equal("This class validates the behavior for mapping\ngeneric types to open generics for use in\ntypeof expressions.", genericParent.Description, ignoreLineEndingDifferences: true);
+            Assert.Equal("This property is a nullable value type.", genericParent.Properties["id"].Description);
+            Assert.Equal("This property is a nullable reference type.", genericParent.Properties["name"].Description);
+            Assert.Equal("This property is a generic type containing a tuple.", genericParent.Properties["taskOfTupleProp"].Description);
+            Assert.Equal("This property is a tuple with a generic type inside.", genericParent.Properties["tupleWithGenericProp"].Description);
+            Assert.Equal("This property is a tuple with a nested generic type inside.", genericParent.Properties["tupleWithNestedGenericProp"].Description);
+
             path = document.Paths["/params-and-param-refs"].Operations[OperationType.Post];
             var paramsAndParamRefs = path.RequestBody.Content["application/json"].Schema;
             Assert.Equal("This shows examples of typeparamref and typeparam tags", paramsAndParamRefs.Description);

+ 135 - 0
src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/OperationTests.MinimalApis.cs

@@ -14,6 +14,8 @@ public partial class OperationTests
     {
         var source = """
 using System;
+using System.Threading.Tasks;
+using System.Collections.Generic;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.AspNetCore.Http.HttpResults;
@@ -32,6 +34,16 @@ app.MapGet("/4", RouteHandlerExtensionMethods.Get4);
 app.MapGet("/5", RouteHandlerExtensionMethods.Get5);
 app.MapPost("/6", RouteHandlerExtensionMethods.Post6);
 app.MapPut("/7", RouteHandlerExtensionMethods.Put7);
+app.MapGet("/8", RouteHandlerExtensionMethods.Get8);
+app.MapGet("/9", RouteHandlerExtensionMethods.Get9);
+app.MapGet("/10", RouteHandlerExtensionMethods.Get10);
+app.MapGet("/11", RouteHandlerExtensionMethods.Get11);
+app.MapGet("/12", RouteHandlerExtensionMethods.Get12);
+app.MapGet("/13", RouteHandlerExtensionMethods.Get13);
+app.MapGet("/14", RouteHandlerExtensionMethods.Get14);
+app.MapGet("/15", RouteHandlerExtensionMethods.Get15);
+app.MapPost("/16", RouteHandlerExtensionMethods.Post16);
+app.MapGet("/17", RouteHandlerExtensionMethods.Get17);
 
 app.Run();
 
@@ -114,6 +126,83 @@ public static class RouteHandlerExtensionMethods
     {
         return TypedResults.NoContent();
     }
+
+    /// <summary>
+    /// A summary of Get8.
+    /// </summary>
+    public static async Task Get8()
+    {
+        await Task.Delay(1000);
+        return;
+    }
+    /// <summary>
+    /// A summary of Get9.
+    /// </summary>
+    public static async ValueTask Get9()
+    {
+        await Task.Delay(1000);
+        return;
+    }
+    /// <summary>
+    /// A summary of Get10.
+    /// </summary>
+    public static Task Get10()
+    {
+        return Task.CompletedTask;
+    }
+    /// <summary>
+    /// A summary of Get11.
+    /// </summary>
+    public static ValueTask Get11()
+    {
+        return ValueTask.CompletedTask;
+    }
+    /// <summary>
+    /// A summary of Get12.
+    /// </summary>
+    public static Task<string> Get12()
+    {
+        return Task.FromResult("Hello, World!");
+    }
+    /// <summary>
+    /// A summary of Get13.
+    /// </summary>
+    public static ValueTask<string> Get13()
+    {
+        return new ValueTask<string>("Hello, World!");
+    }
+    /// <summary>
+    /// A summary of Get14.
+    /// </summary>
+    public static async Task<Holder<string>> Get14()
+    {
+        await Task.Delay(1000);
+        return new Holder<string> { Value = "Hello, World!" };
+    }
+    /// <summary>
+    /// A summary of Get15.
+    /// </summary>
+    public static Task<Holder<string>> Get15()
+    {
+        return Task.FromResult(new Holder<string> { Value = "Hello, World!" });
+    }
+
+    /// <summary>
+    /// A summary of Post16.
+    /// </summary>
+    public static void Post16(Example example)
+    {
+        return;
+    }
+
+    /// <summary>
+    /// A summary of Get17.
+    /// </summary>
+    public static int[][] Get17(int[] args)
+    {
+        return [[1, 2, 3], [4, 5, 6], [7, 8, 9], args];
+
+    }
 }
 
 public class User
@@ -121,6 +210,22 @@ public class User
     public string Username { get; set; } = string.Empty;
     public string Email { get; set; } = string.Empty;
 }
+
+public class Holder<T>
+{
+    public T Value { get; set; } = default!;
+}
+
+public class Example : Task<int>
+{
+    public Example(Func<int> function) : base(function)
+    {
+    }
+
+    public Example(Func<object?, int> function, object? state) : base(function, state)
+    {
+    }
+}
 """;
         var generator = new XmlCommentGenerator();
         await SnapshotTestHelper.Verify(source, generator, out var compilation);
@@ -159,6 +264,36 @@ public class User
             var idParam = path7.Parameters.First(p => p.Name == "id");
             Assert.True(idParam.Deprecated);
             Assert.Equal("Legacy ID parameter - use uuid instead.", idParam.Description);
+
+            var path8 = document.Paths["/8"].Operations[OperationType.Get];
+            Assert.Equal("A summary of Get8.", path8.Summary);
+
+            var path9 = document.Paths["/9"].Operations[OperationType.Get];
+            Assert.Equal("A summary of Get9.", path9.Summary);
+
+            var path10 = document.Paths["/10"].Operations[OperationType.Get];
+            Assert.Equal("A summary of Get10.", path10.Summary);
+
+            var path11 = document.Paths["/11"].Operations[OperationType.Get];
+            Assert.Equal("A summary of Get11.", path11.Summary);
+
+            var path12 = document.Paths["/12"].Operations[OperationType.Get];
+            Assert.Equal("A summary of Get12.", path12.Summary);
+
+            var path13 = document.Paths["/13"].Operations[OperationType.Get];
+            Assert.Equal("A summary of Get13.", path13.Summary);
+
+            var path14 = document.Paths["/14"].Operations[OperationType.Get];
+            Assert.Equal("A summary of Get14.", path14.Summary);
+
+            var path15 = document.Paths["/15"].Operations[OperationType.Get];
+            Assert.Equal("A summary of Get15.", path15.Summary);
+
+            var path16 = document.Paths["/16"].Operations[OperationType.Post];
+            Assert.Equal("A summary of Post16.", path16.Summary);
+
+            var path17 = document.Paths["/17"].Operations[OperationType.Get];
+            Assert.Equal("A summary of Get17.", path17.Summary);
         });
     }
 }

+ 185 - 103
src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs

@@ -29,8 +29,10 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
     using System;
     using System.Collections.Generic;
     using System.Diagnostics.CodeAnalysis;
+    using System.Globalization;
     using System.Linq;
     using System.Reflection;
+    using System.Text;
     using System.Text.Json;
     using System.Text.Json.Nodes;
     using System.Threading;
@@ -61,148 +63,228 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
     file record XmlResponseComment(string Code, string? Description, string? Example);
 
     [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
-    file sealed record MemberKey(
-        Type? DeclaringType,
-        MemberType MemberKind,
-        string? Name,
-        Type? ReturnType,
-        Type[]? Parameters) : IEquatable<MemberKey>
+    file static class XmlCommentCache
     {
-        public bool Equals(MemberKey? other)
+        private static Dictionary<string, XmlComment>? _cache;
+        public static Dictionary<string, XmlComment> Cache => _cache ??= GenerateCacheEntries();
+
+        private static Dictionary<string, XmlComment> GenerateCacheEntries()
         {
-            if (other is null) return false;
+            var cache = new Dictionary<string, XmlComment>();
 
-            // Check member kind
-            if (MemberKind != other.MemberKind) return false;
 
-            // Check declaring type, handling generic types
-            if (!TypesEqual(DeclaringType, other.DeclaringType)) return false;
+            return cache;
+        }
+    }
 
-            // Check name
-            if (Name != other.Name) return false;
+    file static class DocumentationCommentIdHelper
+    {
+        /// <summary>
+        /// Generates a documentation comment ID for a type.
+        /// Example: T:Namespace.Outer+Inner`1 becomes T:Namespace.Outer.Inner`1
+        /// </summary>
+        public static string CreateDocumentationId(this Type type)
+        {
+            if (type == null)
+            {
+                throw new ArgumentNullException(nameof(type));
+            }
 
-            // For methods, check return type and parameters
-            if (MemberKind == MemberType.Method)
+            return "T:" + GetTypeDocId(type, includeGenericArguments: false, omitGenericArity: false);
+        }
+
+        /// <summary>
+        /// Generates a documentation comment ID for a property.
+        /// Example: P:Namespace.ContainingType.PropertyName or for an indexer P:Namespace.ContainingType.Item(System.Int32)
+        /// </summary>
+        public static string CreateDocumentationId(this PropertyInfo property)
+        {
+            if (property == null)
             {
-                if (!TypesEqual(ReturnType, other.ReturnType)) return false;
-                if (Parameters is null || other.Parameters is null) return false;
-                if (Parameters.Length != other.Parameters.Length) return false;
+                throw new ArgumentNullException(nameof(property));
+            }
+
+            var sb = new StringBuilder();
+            sb.Append("P:");
+
+            if (property.DeclaringType != null)
+            {
+                sb.Append(GetTypeDocId(property.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
+            }
 
-                for (int i = 0; i < Parameters.Length; i++)
+            sb.Append('.');
+            sb.Append(property.Name);
+
+            // For indexers, include the parameter list.
+            var indexParams = property.GetIndexParameters();
+            if (indexParams.Length > 0)
+            {
+                sb.Append('(');
+                for (int i = 0; i < indexParams.Length; i++)
                 {
-                    if (!TypesEqual(Parameters[i], other.Parameters[i])) return false;
+                    if (i > 0)
+                    {
+                        sb.Append(',');
+                    }
+
+                    sb.Append(GetTypeDocId(indexParams[i].ParameterType, includeGenericArguments: true, omitGenericArity: false));
                 }
+                sb.Append(')');
             }
 
-            return true;
+            return sb.ToString();
         }
 
-        private static bool TypesEqual(Type? type1, Type? type2)
+        /// <summary>
+        /// Generates a documentation comment ID for a method (or constructor).
+        /// For example:
+        ///   M:Namespace.ContainingType.MethodName(ParamType1,ParamType2)~ReturnType
+        ///   M:Namespace.ContainingType.#ctor(ParamType)
+        /// </summary>
+        public static string CreateDocumentationId(this MethodInfo method)
         {
-            if (type1 == type2) return true;
-            if (type1 == null || type2 == null) return false;
+            if (method == null)
+            {
+                throw new ArgumentNullException(nameof(method));
+            }
+
+            var sb = new StringBuilder();
+            sb.Append("M:");
 
-            if (type1.IsGenericType && type2.IsGenericType)
+            // Append the fully qualified name of the declaring type.
+            if (method.DeclaringType != null)
             {
-                return type1.GetGenericTypeDefinition() == type2.GetGenericTypeDefinition();
+                sb.Append(GetTypeDocId(method.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
             }
 
-            return type1 == type2;
-        }
+            sb.Append('.');
 
-        public override int GetHashCode()
-        {
-            var hash = new HashCode();
-            hash.Add(GetTypeHashCode(DeclaringType));
-            hash.Add(MemberKind);
-            hash.Add(Name);
+            // Append the method name, handling constructors specially.
+            if (method.IsConstructor)
+            {
+                sb.Append(method.IsStatic ? "#cctor" : "#ctor");
+            }
+            else
+            {
+                sb.Append(method.Name);
+                if (method.IsGenericMethod)
+                {
+                    sb.Append("``");
+                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", method.GetGenericArguments().Length);
+                }
+            }
 
-            if (MemberKind == MemberType.Method)
+            // Append the parameter list, if any.
+            var parameters = method.GetParameters();
+            if (parameters.Length > 0)
             {
-                hash.Add(GetTypeHashCode(ReturnType));
-                if (Parameters is not null)
+                sb.Append('(');
+                for (int i = 0; i < parameters.Length; i++)
                 {
-                    foreach (var param in Parameters)
+                    if (i > 0)
                     {
-                        hash.Add(GetTypeHashCode(param));
+                        sb.Append(',');
                     }
+
+                    // Omit the generic arity for the parameter type.
+                    sb.Append(GetTypeDocId(parameters[i].ParameterType, includeGenericArguments: true, omitGenericArity: true));
                 }
+                sb.Append(')');
             }
 
-            return hash.ToHashCode();
-        }
+            // Append the return type after a '~' (if the method returns a value).
+            if (method.ReturnType != typeof(void))
+            {
+                sb.Append('~');
+                // Omit the generic arity for the return type.
+                sb.Append(GetTypeDocId(method.ReturnType, includeGenericArguments: true, omitGenericArity: true));
+            }
 
-        private static int GetTypeHashCode(Type? type)
-        {
-            if (type == null) return 0;
-            return type.IsGenericType ? type.GetGenericTypeDefinition().GetHashCode() : type.GetHashCode();
+            return sb.ToString();
         }
 
-        public static MemberKey FromMethodInfo(MethodInfo method)
+        /// <summary>
+        /// Generates a documentation ID string for a type.
+        /// This method handles nested types (replacing '+' with '.'),
+        /// generic types, arrays, pointers, by-ref types, and generic parameters.
+        /// The <paramref name="includeGenericArguments"/> flag controls whether
+        /// constructed generic type arguments are emitted, while <paramref name="omitGenericArity"/>
+        /// controls whether the generic arity marker (e.g. "`1") is appended.
+        /// </summary>
+        private static string GetTypeDocId(Type type, bool includeGenericArguments, bool omitGenericArity)
         {
-            return new MemberKey(
-                method.DeclaringType,
-                MemberType.Method,
-                method.Name,
-                method.ReturnType.IsGenericParameter ? typeof(object) : method.ReturnType,
-                method.GetParameters().Select(p => p.ParameterType.IsGenericParameter ? typeof(object) : p.ParameterType).ToArray());
-        }
+            if (type.IsGenericParameter)
+            {
+                // Use `` for method-level generic parameters and ` for type-level.
+                if (type.DeclaringMethod != null)
+                {
+                    return "``" + type.GenericParameterPosition;
+                }
+                else if (type.DeclaringType != null)
+                {
+                    return "`" + type.GenericParameterPosition;
+                }
+                else
+                {
+                    return type.Name;
+                }
+            }
 
-        public static MemberKey FromPropertyInfo(PropertyInfo property)
-        {
-            return new MemberKey(
-                property.DeclaringType,
-                MemberType.Property,
-                property.Name,
-                null,
-                null);
-        }
+            if (type.IsGenericType)
+            {
+                Type genericDef = type.GetGenericTypeDefinition();
+                string fullName = genericDef.FullName ?? genericDef.Name;
 
-        public static MemberKey FromTypeInfo(Type type)
-        {
-            return new MemberKey(
-                type,
-                MemberType.Type,
-                null,
-                null,
-                null);
-        }
-    }
+                var sb = new StringBuilder(fullName.Length);
 
-    file enum MemberType
-    {
-        Type,
-        Property,
-        Method
-    }
+                // Replace '+' with '.' for nested types
+                for (var i = 0; i < fullName.Length; i++)
+                {
+                    char c = fullName[i];
+                    if (c == '+')
+                    {
+                        sb.Append('.');
+                    }
+                    else if (c == '`')
+                    {
+                        break;
+                    }
+                    else
+                    {
+                        sb.Append(c);
+                    }
+                }
 
-    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
-    file static class XmlCommentCache
-    {
-        private static Dictionary<MemberKey, XmlComment>? _cache;
-        public static Dictionary<MemberKey, XmlComment> Cache => _cache ??= GenerateCacheEntries();
+                if (!omitGenericArity)
+                {
+                    int arity = genericDef.GetGenericArguments().Length;
+                    sb.Append('`');
+                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", arity);
+                }
 
-        private static Dictionary<MemberKey, XmlComment> GenerateCacheEntries()
-        {
-            var _cache = new Dictionary<MemberKey, XmlComment>();
+                if (includeGenericArguments && !type.IsGenericTypeDefinition)
+                {
+                    var typeArgs = type.GetGenericArguments();
+                    sb.Append('{');
+
+                    for (int i = 0; i < typeArgs.Length; i++)
+                    {
+                        if (i > 0)
+                        {
+                            sb.Append(',');
+                        }
 
+                        sb.Append(GetTypeDocId(typeArgs[i], includeGenericArguments, omitGenericArity));
+                    }
 
-            return _cache;
-        }
+                    sb.Append('}');
+                }
 
-        internal static bool TryGetXmlComment(Type? type, MethodInfo? methodInfo, [NotNullWhen(true)] out XmlComment? xmlComment)
-        {
-            if (methodInfo is null)
-            {
-                return Cache.TryGetValue(new MemberKey(type, MemberType.Property, null, null, null), out xmlComment);
+                return sb.ToString();
             }
 
-            return Cache.TryGetValue(MemberKey.FromMethodInfo(methodInfo), out xmlComment);
-        }
-
-        internal static bool TryGetXmlComment(Type? type, string? memberName, [NotNullWhen(true)] out XmlComment? xmlComment)
-        {
-            return Cache.TryGetValue(new MemberKey(type, memberName is null ? MemberType.Type : MemberType.Property, memberName, null, null), out xmlComment);
+            // For non-generic types, use FullName (if available) and replace nested type separators.
+            return (type.FullName ?? type.Name).Replace('+', '.');
         }
     }
 
@@ -219,7 +301,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
             {
                 return Task.CompletedTask;
             }
-            if (XmlCommentCache.TryGetXmlComment(methodInfo.DeclaringType, methodInfo, out var methodComment))
+            if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
             {
                 if (methodComment.Summary is { } summary)
                 {
@@ -292,7 +374,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
         {
             if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
             {
-                if (XmlCommentCache.TryGetXmlComment(propertyInfo.DeclaringType, propertyInfo.Name, out var propertyComment))
+                if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
                 {
                     schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
                     if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -301,7 +383,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
                     }
                 }
             }
-            if (XmlCommentCache.TryGetXmlComment(context.JsonTypeInfo.Type, (string?)null, out var typeComment))
+            if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
             {
                 schema.Description = typeComment.Summary;
                 if (typeComment.Examples?.FirstOrDefault() is { } jsonString)

+ 216 - 134
src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.verified.cs

@@ -29,8 +29,10 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
     using System;
     using System.Collections.Generic;
     using System.Diagnostics.CodeAnalysis;
+    using System.Globalization;
     using System.Linq;
     using System.Reflection;
+    using System.Text;
     using System.Text.Json;
     using System.Text.Json.Nodes;
     using System.Threading;
@@ -61,177 +63,257 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
     file record XmlResponseComment(string Code, string? Description, string? Example);
 
     [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
-    file sealed record MemberKey(
-        Type? DeclaringType,
-        MemberType MemberKind,
-        string? Name,
-        Type? ReturnType,
-        Type[]? Parameters) : IEquatable<MemberKey>
+    file static class XmlCommentCache
     {
-        public bool Equals(MemberKey? other)
+        private static Dictionary<string, XmlComment>? _cache;
+        public static Dictionary<string, XmlComment> Cache => _cache ??= GenerateCacheEntries();
+
+        private static Dictionary<string, XmlComment> GenerateCacheEntries()
         {
-            if (other is null) return false;
+            var cache = new Dictionary<string, XmlComment>();
+            cache.Add(@"T:ClassLibrary.Todo", new XmlComment(@"This is a todo item.", null, null, null, null, false, null, null, null));
+            cache.Add(@"T:ClassLibrary.Project", new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:ClassLibrary.Project.#ctor(System.String,System.String)", new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, null, null));
+            cache.Add(@"T:ClassLibrary.ProjectBoard.BoardItem", new XmlComment(@"An item on the board.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:ClassLibrary.ProjectBoard.BoardItem.Name", new XmlComment(@"The identifier of the board item. Defaults to ""name"".", null, null, null, null, false, null, null, null));
+            cache.Add(@"T:ClassLibrary.ProjectRecord", new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, [new XmlParameterComment(@"Name", @"The name of the project.", null, false), new XmlParameterComment(@"Description", @"The description of the project.", null, false)], null));
+            cache.Add(@"M:ClassLibrary.ProjectRecord.#ctor(System.String,System.String)", new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, [new XmlParameterComment(@"Name", @"The name of the project.", null, false), new XmlParameterComment(@"Description", @"The description of the project.", null, false)], null));
+            cache.Add(@"P:ClassLibrary.ProjectRecord.Name", new XmlComment(@"The name of the project.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:ClassLibrary.ProjectRecord.Description", new XmlComment(@"The description of the project.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:ClassLibrary.TodoWithDescription.Id", new XmlComment(@"The identifier of the todo.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:ClassLibrary.TodoWithDescription.Name", new XmlComment(null, null, null, null, @"The name of the todo.", false, null, null, null));
+            cache.Add(@"P:ClassLibrary.TodoWithDescription.Description", new XmlComment(@"A description of the todo.", null, null, null, @"Another description of the todo.", false, null, null, null));
+            cache.Add(@"P:ClassLibrary.TypeWithExamples.BooleanType", new XmlComment(null, null, null, null, null, false, [@"true"], null, null));
+            cache.Add(@"P:ClassLibrary.TypeWithExamples.IntegerType", new XmlComment(null, null, null, null, null, false, [@"42"], null, null));
+            cache.Add(@"P:ClassLibrary.TypeWithExamples.LongType", new XmlComment(null, null, null, null, null, false, [@"1234567890123456789"], null, null));
+            cache.Add(@"P:ClassLibrary.TypeWithExamples.DoubleType", new XmlComment(null, null, null, null, null, false, [@"3.14"], null, null));
+            cache.Add(@"P:ClassLibrary.TypeWithExamples.FloatType", new XmlComment(null, null, null, null, null, false, [@"3.14"], null, null));
+            cache.Add(@"P:ClassLibrary.TypeWithExamples.DateTimeType", new XmlComment(null, null, null, null, null, false, [@"2022-01-01T00:00:00Z"], null, null));
+            cache.Add(@"P:ClassLibrary.TypeWithExamples.DateOnlyType", new XmlComment(null, null, null, null, null, false, [@"2022-01-01"], null, null));
+            cache.Add(@"P:ClassLibrary.TypeWithExamples.StringType", new XmlComment(null, null, null, null, null, false, [@"Hello, World!"], null, null));
+            cache.Add(@"P:ClassLibrary.TypeWithExamples.GuidType", new XmlComment(null, null, null, null, null, false, [@"2d8f1eac-b5c6-4e29-8c62-4d9d75ef3d3d"], null, null));
+            cache.Add(@"P:ClassLibrary.TypeWithExamples.TimeOnlyType", new XmlComment(null, null, null, null, null, false, [@"12:30:45"], null, null));
+            cache.Add(@"P:ClassLibrary.TypeWithExamples.TimeSpanType", new XmlComment(null, null, null, null, null, false, [@"P3DT4H5M"], null, null));
+            cache.Add(@"P:ClassLibrary.TypeWithExamples.ByteType", new XmlComment(null, null, null, null, null, false, [@"255"], null, null));
+            cache.Add(@"P:ClassLibrary.TypeWithExamples.DecimalType", new XmlComment(null, null, null, null, null, false, [@"3.14159265359"], null, null));
+            cache.Add(@"P:ClassLibrary.TypeWithExamples.UriType", new XmlComment(null, null, null, null, null, false, [@"https://example.com"], null, null));
+            cache.Add(@"P:ClassLibrary.Holder`1.Value", new XmlComment(@"The value to hold.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:ClassLibrary.Endpoints.ExternalMethod(System.String)", new XmlComment(@"An external method.", null, null, null, null, false, null, [new XmlParameterComment(@"name", @"The name of the tester. Defaults to ""Tester"".", null, false)], null));
+            cache.Add(@"M:ClassLibrary.Endpoints.CreateHolder``1(``0)", new XmlComment(@"Creates a holder for the specified value.", null, null, @"A holder for the specified value.", null, false, [@"{ value: 42 }"], [new XmlParameterComment(@"value", @"The value to hold.", null, false)], null));
+
+
+            return cache;
+        }
+    }
 
-            // Check member kind
-            if (MemberKind != other.MemberKind) return false;
+    file static class DocumentationCommentIdHelper
+    {
+        /// <summary>
+        /// Generates a documentation comment ID for a type.
+        /// Example: T:Namespace.Outer+Inner`1 becomes T:Namespace.Outer.Inner`1
+        /// </summary>
+        public static string CreateDocumentationId(this Type type)
+        {
+            if (type == null)
+            {
+                throw new ArgumentNullException(nameof(type));
+            }
 
-            // Check declaring type, handling generic types
-            if (!TypesEqual(DeclaringType, other.DeclaringType)) return false;
+            return "T:" + GetTypeDocId(type, includeGenericArguments: false, omitGenericArity: false);
+        }
 
-            // Check name
-            if (Name != other.Name) return false;
+        /// <summary>
+        /// Generates a documentation comment ID for a property.
+        /// Example: P:Namespace.ContainingType.PropertyName or for an indexer P:Namespace.ContainingType.Item(System.Int32)
+        /// </summary>
+        public static string CreateDocumentationId(this PropertyInfo property)
+        {
+            if (property == null)
+            {
+                throw new ArgumentNullException(nameof(property));
+            }
+
+            var sb = new StringBuilder();
+            sb.Append("P:");
 
-            // For methods, check return type and parameters
-            if (MemberKind == MemberType.Method)
+            if (property.DeclaringType != null)
             {
-                if (!TypesEqual(ReturnType, other.ReturnType)) return false;
-                if (Parameters is null || other.Parameters is null) return false;
-                if (Parameters.Length != other.Parameters.Length) return false;
+                sb.Append(GetTypeDocId(property.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
+            }
 
-                for (int i = 0; i < Parameters.Length; i++)
+            sb.Append('.');
+            sb.Append(property.Name);
+
+            // For indexers, include the parameter list.
+            var indexParams = property.GetIndexParameters();
+            if (indexParams.Length > 0)
+            {
+                sb.Append('(');
+                for (int i = 0; i < indexParams.Length; i++)
                 {
-                    if (!TypesEqual(Parameters[i], other.Parameters[i])) return false;
+                    if (i > 0)
+                    {
+                        sb.Append(',');
+                    }
+
+                    sb.Append(GetTypeDocId(indexParams[i].ParameterType, includeGenericArguments: true, omitGenericArity: false));
                 }
+                sb.Append(')');
             }
 
-            return true;
+            return sb.ToString();
         }
 
-        private static bool TypesEqual(Type? type1, Type? type2)
+        /// <summary>
+        /// Generates a documentation comment ID for a method (or constructor).
+        /// For example:
+        ///   M:Namespace.ContainingType.MethodName(ParamType1,ParamType2)~ReturnType
+        ///   M:Namespace.ContainingType.#ctor(ParamType)
+        /// </summary>
+        public static string CreateDocumentationId(this MethodInfo method)
         {
-            if (type1 == type2) return true;
-            if (type1 == null || type2 == null) return false;
+            if (method == null)
+            {
+                throw new ArgumentNullException(nameof(method));
+            }
 
-            if (type1.IsGenericType && type2.IsGenericType)
+            var sb = new StringBuilder();
+            sb.Append("M:");
+
+            // Append the fully qualified name of the declaring type.
+            if (method.DeclaringType != null)
             {
-                return type1.GetGenericTypeDefinition() == type2.GetGenericTypeDefinition();
+                sb.Append(GetTypeDocId(method.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
             }
 
-            return type1 == type2;
-        }
+            sb.Append('.');
 
-        public override int GetHashCode()
-        {
-            var hash = new HashCode();
-            hash.Add(GetTypeHashCode(DeclaringType));
-            hash.Add(MemberKind);
-            hash.Add(Name);
+            // Append the method name, handling constructors specially.
+            if (method.IsConstructor)
+            {
+                sb.Append(method.IsStatic ? "#cctor" : "#ctor");
+            }
+            else
+            {
+                sb.Append(method.Name);
+                if (method.IsGenericMethod)
+                {
+                    sb.Append("``");
+                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", method.GetGenericArguments().Length);
+                }
+            }
 
-            if (MemberKind == MemberType.Method)
+            // Append the parameter list, if any.
+            var parameters = method.GetParameters();
+            if (parameters.Length > 0)
             {
-                hash.Add(GetTypeHashCode(ReturnType));
-                if (Parameters is not null)
+                sb.Append('(');
+                for (int i = 0; i < parameters.Length; i++)
                 {
-                    foreach (var param in Parameters)
+                    if (i > 0)
                     {
-                        hash.Add(GetTypeHashCode(param));
+                        sb.Append(',');
                     }
+
+                    // Omit the generic arity for the parameter type.
+                    sb.Append(GetTypeDocId(parameters[i].ParameterType, includeGenericArguments: true, omitGenericArity: true));
                 }
+                sb.Append(')');
             }
 
-            return hash.ToHashCode();
-        }
+            // Append the return type after a '~' (if the method returns a value).
+            if (method.ReturnType != typeof(void))
+            {
+                sb.Append('~');
+                // Omit the generic arity for the return type.
+                sb.Append(GetTypeDocId(method.ReturnType, includeGenericArguments: true, omitGenericArity: true));
+            }
 
-        private static int GetTypeHashCode(Type? type)
-        {
-            if (type == null) return 0;
-            return type.IsGenericType ? type.GetGenericTypeDefinition().GetHashCode() : type.GetHashCode();
+            return sb.ToString();
         }
 
-        public static MemberKey FromMethodInfo(MethodInfo method)
+        /// <summary>
+        /// Generates a documentation ID string for a type.
+        /// This method handles nested types (replacing '+' with '.'),
+        /// generic types, arrays, pointers, by-ref types, and generic parameters.
+        /// The <paramref name="includeGenericArguments"/> flag controls whether
+        /// constructed generic type arguments are emitted, while <paramref name="omitGenericArity"/>
+        /// controls whether the generic arity marker (e.g. "`1") is appended.
+        /// </summary>
+        private static string GetTypeDocId(Type type, bool includeGenericArguments, bool omitGenericArity)
         {
-            return new MemberKey(
-                method.DeclaringType,
-                MemberType.Method,
-                method.Name,
-                method.ReturnType.IsGenericParameter ? typeof(object) : method.ReturnType,
-                method.GetParameters().Select(p => p.ParameterType.IsGenericParameter ? typeof(object) : p.ParameterType).ToArray());
-        }
+            if (type.IsGenericParameter)
+            {
+                // Use `` for method-level generic parameters and ` for type-level.
+                if (type.DeclaringMethod != null)
+                {
+                    return "``" + type.GenericParameterPosition;
+                }
+                else if (type.DeclaringType != null)
+                {
+                    return "`" + type.GenericParameterPosition;
+                }
+                else
+                {
+                    return type.Name;
+                }
+            }
 
-        public static MemberKey FromPropertyInfo(PropertyInfo property)
-        {
-            return new MemberKey(
-                property.DeclaringType,
-                MemberType.Property,
-                property.Name,
-                null,
-                null);
-        }
+            if (type.IsGenericType)
+            {
+                Type genericDef = type.GetGenericTypeDefinition();
+                string fullName = genericDef.FullName ?? genericDef.Name;
 
-        public static MemberKey FromTypeInfo(Type type)
-        {
-            return new MemberKey(
-                type,
-                MemberType.Type,
-                null,
-                null,
-                null);
-        }
-    }
+                var sb = new StringBuilder(fullName.Length);
 
-    file enum MemberType
-    {
-        Type,
-        Property,
-        Method
-    }
+                // Replace '+' with '.' for nested types
+                for (var i = 0; i < fullName.Length; i++)
+                {
+                    char c = fullName[i];
+                    if (c == '+')
+                    {
+                        sb.Append('.');
+                    }
+                    else if (c == '`')
+                    {
+                        break;
+                    }
+                    else
+                    {
+                        sb.Append(c);
+                    }
+                }
 
-    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
-    file static class XmlCommentCache
-    {
-        private static Dictionary<MemberKey, XmlComment>? _cache;
-        public static Dictionary<MemberKey, XmlComment> Cache => _cache ??= GenerateCacheEntries();
+                if (!omitGenericArity)
+                {
+                    int arity = genericDef.GetGenericArguments().Length;
+                    sb.Append('`');
+                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", arity);
+                }
 
-        private static Dictionary<MemberKey, XmlComment> GenerateCacheEntries()
-        {
-            var _cache = new Dictionary<MemberKey, XmlComment>();
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.Todo), MemberType.Type, null, null, []), new XmlComment(@"This is a todo item.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.Project), MemberType.Type, null, null, []), new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.Project), MemberType.Method, ".ctor", typeof(void), [typeof(global::System.String), typeof(global::System.String)]), new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.ProjectBoard.BoardItem), MemberType.Type, null, null, []), new XmlComment(@"An item on the board.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.ProjectBoard.BoardItem), MemberType.Property, "Name", null, []), new XmlComment(@"The identifier of the board item. Defaults to ""name"".", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.ProjectRecord), MemberType.Type, null, null, []), new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, [new XmlParameterComment(@"Name", @"The name of the project.", null, false), new XmlParameterComment(@"Description", @"The description of the project.", null, false)], null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.ProjectRecord), MemberType.Method, ".ctor", typeof(void), [typeof(global::System.String), typeof(global::System.String)]), new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, [new XmlParameterComment(@"Name", @"The name of the project.", null, false), new XmlParameterComment(@"Description", @"The description of the project.", null, false)], null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.ProjectRecord), MemberType.Property, "Name", null, []), new XmlComment(@"The name of the project.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.ProjectRecord), MemberType.Property, "Description", null, []), new XmlComment(@"The description of the project.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TodoWithDescription), MemberType.Property, "Id", null, []), new XmlComment(@"The identifier of the todo.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TodoWithDescription), MemberType.Property, "Name", null, []), new XmlComment(null, null, null, null, @"The name of the todo.", false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TodoWithDescription), MemberType.Property, "Description", null, []), new XmlComment(@"A description of the todo.", null, null, null, @"Another description of the todo.", false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "BooleanType", null, []), new XmlComment(null, null, null, null, null, false, [@"true"], null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "IntegerType", null, []), new XmlComment(null, null, null, null, null, false, [@"42"], null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "LongType", null, []), new XmlComment(null, null, null, null, null, false, [@"1234567890123456789"], null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "DoubleType", null, []), new XmlComment(null, null, null, null, null, false, [@"3.14"], null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "FloatType", null, []), new XmlComment(null, null, null, null, null, false, [@"3.14"], null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "DateTimeType", null, []), new XmlComment(null, null, null, null, null, false, [@"2022-01-01T00:00:00Z"], null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "DateOnlyType", null, []), new XmlComment(null, null, null, null, null, false, [@"2022-01-01"], null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "StringType", null, []), new XmlComment(null, null, null, null, null, false, [@"Hello, World!"], null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "GuidType", null, []), new XmlComment(null, null, null, null, null, false, [@"2d8f1eac-b5c6-4e29-8c62-4d9d75ef3d3d"], null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "TimeOnlyType", null, []), new XmlComment(null, null, null, null, null, false, [@"12:30:45"], null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "TimeSpanType", null, []), new XmlComment(null, null, null, null, null, false, [@"P3DT4H5M"], null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "ByteType", null, []), new XmlComment(null, null, null, null, null, false, [@"255"], null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "DecimalType", null, []), new XmlComment(null, null, null, null, null, false, [@"3.14159265359"], null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "UriType", null, []), new XmlComment(null, null, null, null, null, false, [@"https://example.com"], null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.Holder<>), MemberType.Property, "Value", null, []), new XmlComment(@"The value to hold.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.Endpoints), MemberType.Method, "ExternalMethod", typeof(void), [typeof(global::System.String)]), new XmlComment(@"An external method.", null, null, null, null, false, null, [new XmlParameterComment(@"name", @"The name of the tester. Defaults to ""Tester"".", null, false)], null));
-            _cache.Add(new MemberKey(typeof(global::ClassLibrary.Endpoints), MemberType.Method, "CreateHolder", typeof(global::ClassLibrary.Holder<>), [typeof(object)]), new XmlComment(@"Creates a holder for the specified value.", null, null, @"A holder for the specified value.", null, false, [@"{ value: 42 }"], [new XmlParameterComment(@"value", @"The value to hold.", null, false)], null));
-
-
-            return _cache;
-        }
+                if (includeGenericArguments && !type.IsGenericTypeDefinition)
+                {
+                    var typeArgs = type.GetGenericArguments();
+                    sb.Append('{');
 
-        internal static bool TryGetXmlComment(Type? type, MethodInfo? methodInfo, [NotNullWhen(true)] out XmlComment? xmlComment)
-        {
-            if (methodInfo is null)
-            {
-                return Cache.TryGetValue(new MemberKey(type, MemberType.Property, null, null, null), out xmlComment);
-            }
+                    for (int i = 0; i < typeArgs.Length; i++)
+                    {
+                        if (i > 0)
+                        {
+                            sb.Append(',');
+                        }
 
-            return Cache.TryGetValue(MemberKey.FromMethodInfo(methodInfo), out xmlComment);
-        }
+                        sb.Append(GetTypeDocId(typeArgs[i], includeGenericArguments, omitGenericArity));
+                    }
 
-        internal static bool TryGetXmlComment(Type? type, string? memberName, [NotNullWhen(true)] out XmlComment? xmlComment)
-        {
-            return Cache.TryGetValue(new MemberKey(type, memberName is null ? MemberType.Type : MemberType.Property, memberName, null, null), out xmlComment);
+                    sb.Append('}');
+                }
+
+                return sb.ToString();
+            }
+
+            // For non-generic types, use FullName (if available) and replace nested type separators.
+            return (type.FullName ?? type.Name).Replace('+', '.');
         }
     }
 
@@ -248,7 +330,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
             {
                 return Task.CompletedTask;
             }
-            if (XmlCommentCache.TryGetXmlComment(methodInfo.DeclaringType, methodInfo, out var methodComment))
+            if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
             {
                 if (methodComment.Summary is { } summary)
                 {
@@ -321,7 +403,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
         {
             if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
             {
-                if (XmlCommentCache.TryGetXmlComment(propertyInfo.DeclaringType, propertyInfo.Name, out var propertyComment))
+                if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
                 {
                     schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
                     if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -330,7 +412,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
                     }
                 }
             }
-            if (XmlCommentCache.TryGetXmlComment(context.JsonTypeInfo.Type, (string?)null, out var typeComment))
+            if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
             {
                 schema.Description = typeComment.Summary;
                 if (typeComment.Examples?.FirstOrDefault() is { } jsonString)

+ 251 - 153
src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs

@@ -29,8 +29,10 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
     using System;
     using System.Collections.Generic;
     using System.Diagnostics.CodeAnalysis;
+    using System.Globalization;
     using System.Linq;
     using System.Reflection;
+    using System.Text;
     using System.Text.Json;
     using System.Text.Json.Nodes;
     using System.Threading;
@@ -60,133 +62,17 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
     [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
     file record XmlResponseComment(string Code, string? Description, string? Example);
 
-    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
-    file sealed record MemberKey(
-        Type? DeclaringType,
-        MemberType MemberKind,
-        string? Name,
-        Type? ReturnType,
-        Type[]? Parameters) : IEquatable<MemberKey>
-    {
-        public bool Equals(MemberKey? other)
-        {
-            if (other is null) return false;
-
-            // Check member kind
-            if (MemberKind != other.MemberKind) return false;
-
-            // Check declaring type, handling generic types
-            if (!TypesEqual(DeclaringType, other.DeclaringType)) return false;
-
-            // Check name
-            if (Name != other.Name) return false;
-
-            // For methods, check return type and parameters
-            if (MemberKind == MemberType.Method)
-            {
-                if (!TypesEqual(ReturnType, other.ReturnType)) return false;
-                if (Parameters is null || other.Parameters is null) return false;
-                if (Parameters.Length != other.Parameters.Length) return false;
-
-                for (int i = 0; i < Parameters.Length; i++)
-                {
-                    if (!TypesEqual(Parameters[i], other.Parameters[i])) return false;
-                }
-            }
-
-            return true;
-        }
-
-        private static bool TypesEqual(Type? type1, Type? type2)
-        {
-            if (type1 == type2) return true;
-            if (type1 == null || type2 == null) return false;
-
-            if (type1.IsGenericType && type2.IsGenericType)
-            {
-                return type1.GetGenericTypeDefinition() == type2.GetGenericTypeDefinition();
-            }
-
-            return type1 == type2;
-        }
-
-        public override int GetHashCode()
-        {
-            var hash = new HashCode();
-            hash.Add(GetTypeHashCode(DeclaringType));
-            hash.Add(MemberKind);
-            hash.Add(Name);
-
-            if (MemberKind == MemberType.Method)
-            {
-                hash.Add(GetTypeHashCode(ReturnType));
-                if (Parameters is not null)
-                {
-                    foreach (var param in Parameters)
-                    {
-                        hash.Add(GetTypeHashCode(param));
-                    }
-                }
-            }
-
-            return hash.ToHashCode();
-        }
-
-        private static int GetTypeHashCode(Type? type)
-        {
-            if (type == null) return 0;
-            return type.IsGenericType ? type.GetGenericTypeDefinition().GetHashCode() : type.GetHashCode();
-        }
-
-        public static MemberKey FromMethodInfo(MethodInfo method)
-        {
-            return new MemberKey(
-                method.DeclaringType,
-                MemberType.Method,
-                method.Name,
-                method.ReturnType.IsGenericParameter ? typeof(object) : method.ReturnType,
-                method.GetParameters().Select(p => p.ParameterType.IsGenericParameter ? typeof(object) : p.ParameterType).ToArray());
-        }
-
-        public static MemberKey FromPropertyInfo(PropertyInfo property)
-        {
-            return new MemberKey(
-                property.DeclaringType,
-                MemberType.Property,
-                property.Name,
-                null,
-                null);
-        }
-
-        public static MemberKey FromTypeInfo(Type type)
-        {
-            return new MemberKey(
-                type,
-                MemberType.Type,
-                null,
-                null,
-                null);
-        }
-    }
-
-    file enum MemberType
-    {
-        Type,
-        Property,
-        Method
-    }
-
     [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
     file static class XmlCommentCache
     {
-        private static Dictionary<MemberKey, XmlComment>? _cache;
-        public static Dictionary<MemberKey, XmlComment> Cache => _cache ??= GenerateCacheEntries();
+        private static Dictionary<string, XmlComment>? _cache;
+        public static Dictionary<string, XmlComment> Cache => _cache ??= GenerateCacheEntries();
 
-        private static Dictionary<MemberKey, XmlComment> GenerateCacheEntries()
+        private static Dictionary<string, XmlComment> GenerateCacheEntries()
         {
-            var _cache = new Dictionary<MemberKey, XmlComment>();
+            var cache = new Dictionary<string, XmlComment>();
 
-            _cache.Add(new MemberKey(typeof(global::ExampleClass), MemberType.Type, null, null, []), new XmlComment(@"Every class and member should have a one sentence
+            cache.Add(@"T:ExampleClass", new XmlComment(@"Every class and member should have a one sentence
 summary describing its purpose.", null, @"     You can expand on that one sentence summary to
      provide more information for readers. In this case,
      the `ExampleClass` provides different C#
@@ -230,82 +116,294 @@ would typically use the ""term"" element.
 
 Note: paragraphs are double spaced. Use the *br*
 tag for single spaced lines.", null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::Person), MemberType.Type, null, null, []), new XmlComment(@"This is an example of a positional record.", null, @"There isn't a way to add XML comments for properties
+            cache.Add(@"T:Person", new XmlComment(@"This is an example of a positional record.", null, @"There isn't a way to add XML comments for properties
 created for positional records, yet. The language
 design team is still considering what tags should
 be supported, and where. Currently, you can use
 the ""param"" tag to describe the parameters to the
 primary constructor.", null, null, false, null, [new XmlParameterComment(@"FirstName", @"This tag will apply to the primary constructor parameter.", null, false), new XmlParameterComment(@"LastName", @"This tag will apply to the primary constructor parameter.", null, false)], null));
-            _cache.Add(new MemberKey(typeof(global::MainClass), MemberType.Type, null, null, []), new XmlComment(@"A summary about this class.", null, @"These remarks would explain more about this class.
+            cache.Add(@"T:MainClass", new XmlComment(@"A summary about this class.", null, @"These remarks would explain more about this class.
 In this example, these comments also explain the
 general information about the derived class.", null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::DerivedClass), MemberType.Type, null, null, []), new XmlComment(@"A summary about this class.", null, @"These remarks would explain more about this class.
+            cache.Add(@"T:DerivedClass", new XmlComment(@"A summary about this class.", null, @"These remarks would explain more about this class.
 In this example, these comments also explain the
 general information about the derived class.", null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ITestInterface), MemberType.Type, null, null, []), new XmlComment(@"This interface would describe all the methods in
+            cache.Add(@"T:ITestInterface", new XmlComment(@"This interface would describe all the methods in
 its contract.", null, @"While elided for brevity, each method or property
 in this interface would contain docs that you want
 to duplicate in each implementing class.", null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ImplementingClass), MemberType.Type, null, null, []), new XmlComment(@"This interface would describe all the methods in
+            cache.Add(@"T:ImplementingClass", new XmlComment(@"This interface would describe all the methods in
 its contract.", null, @"While elided for brevity, each method or property
 in this interface would contain docs that you want
 to duplicate in each implementing class.", null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::InheritOnlyReturns), MemberType.Type, null, null, []), new XmlComment(@"This class shows hows you can ""inherit"" the doc
+            cache.Add(@"T:InheritOnlyReturns", new XmlComment(@"This class shows hows you can ""inherit"" the doc
 comments from one method in another method.", null, @"You can inherit all comments, or only a specific tag,
 represented by an xpath expression.", null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::InheritAllButRemarks), MemberType.Type, null, null, []), new XmlComment(@"This class shows an example of sharing comments across methods.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::GenericClass<>), MemberType.Type, null, null, []), new XmlComment(@"This is a generic class.", null, @"This example shows how to specify the GenericClass&lt;T&gt;
+            cache.Add(@"T:InheritAllButRemarks", new XmlComment(@"This class shows an example of sharing comments across methods.", null, null, null, null, false, null, null, null));
+            cache.Add(@"T:GenericClass`1", new XmlComment(@"This is a generic class.", null, @"This example shows how to specify the GenericClass&lt;T&gt;
 type as a cref attribute.
 In generic classes and methods, you'll often want to reference the
 generic type, or the type parameter.", null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ParamsAndParamRefs), MemberType.Type, null, null, []), new XmlComment(@"This shows examples of typeparamref and typeparam tags", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ExampleClass), MemberType.Property, "Label", null, []), new XmlComment(null, null, @"    The string? ExampleClass.Label is a <see langword=""string"" />
+            cache.Add(@"T:GenericParent", new XmlComment(@"This class validates the behavior for mapping
+generic types to open generics for use in
+typeof expressions.", null, null, null, null, false, null, null, null));
+            cache.Add(@"T:ParamsAndParamRefs", new XmlComment(@"This shows examples of typeparamref and typeparam tags", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:ExampleClass.Label", new XmlComment(null, null, @"    The string? ExampleClass.Label is a <see langword=""string"" />
     that you use for a label.
     Note that there isn't a way to provide a ""cref"" to
 each accessor, only to the property itself.", null, @"The `Label` property represents a label
 for this instance.", false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::Person), MemberType.Property, "FirstName", null, []), new XmlComment(@"This tag will apply to the primary constructor parameter.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::Person), MemberType.Property, "LastName", null, []), new XmlComment(@"This tag will apply to the primary constructor parameter.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ExampleClass), MemberType.Method, "Add", typeof(global::System.Int32), [typeof(global::System.Int32), typeof(global::System.Int32)]), new XmlComment(@"Adds two integers and returns the result.", null, null, @"The sum of two integers.", null, false, [@"    ```int c = Math.Add(4, 5);
+            cache.Add(@"P:Person.FirstName", new XmlComment(@"This tag will apply to the primary constructor parameter.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:Person.LastName", new XmlComment(@"This tag will apply to the primary constructor parameter.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:GenericParent.Id", new XmlComment(@"This property is a nullable value type.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:GenericParent.Name", new XmlComment(@"This property is a nullable reference type.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:GenericParent.TaskOfTupleProp", new XmlComment(@"This property is a generic type containing a tuple.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:GenericParent.TupleWithGenericProp", new XmlComment(@"This property is a tuple with a generic type inside.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:GenericParent.TupleWithNestedGenericProp", new XmlComment(@"This property is a tuple with a nested generic type inside.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:ExampleClass.Add(System.Int32,System.Int32)~System.Int32", new XmlComment(@"Adds two integers and returns the result.", null, null, @"The sum of two integers.", null, false, [@"    ```int c = Math.Add(4, 5);
 if (c &gt; 10)
 {
     Console.WriteLine(c);
 }```"], [new XmlParameterComment(@"left", @"The left operand of the addition.", null, false), new XmlParameterComment(@"right", @"The right operand of the addition.", null, false)], null));
-            _cache.Add(new MemberKey(typeof(global::ExampleClass), MemberType.Method, "AddAsync", typeof(global::System.Threading.Tasks.Task<>), [typeof(global::System.Int32), typeof(global::System.Int32)]), new XmlComment(@"This method is an example of a method that
+            cache.Add(@"M:ExampleClass.AddAsync(System.Int32,System.Int32)~System.Threading.Tasks.Task{System.Int32}", new XmlComment(@"This method is an example of a method that
 returns an awaitable item.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ExampleClass), MemberType.Method, "DoNothingAsync", typeof(global::System.Threading.Tasks.Task), []), new XmlComment(@"This method is an example of a method that
+            cache.Add(@"M:ExampleClass.DoNothingAsync~System.Threading.Tasks.Task", new XmlComment(@"This method is an example of a method that
 returns a Task which should map to a void return type.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ExampleClass), MemberType.Method, "AddNumbers", typeof(global::System.Int32), [typeof(global::System.Int32[])]), new XmlComment(@"This method is an example of a method that consumes
+            cache.Add(@"M:ExampleClass.AddNumbers(System.Int32[])~System.Int32", new XmlComment(@"This method is an example of a method that consumes
 an params array.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ITestInterface), MemberType.Method, "Method", typeof(global::System.Int32), [typeof(global::System.Int32)]), new XmlComment(@"This method is part of the test interface.", null, @"This content would be inherited by classes
+            cache.Add(@"M:ITestInterface.Method(System.Int32)~System.Int32", new XmlComment(@"This method is part of the test interface.", null, @"This content would be inherited by classes
 that implement this interface when the
 implementing class uses ""inheritdoc""", @"The value of arg", null, false, null, [new XmlParameterComment(@"arg", @"The argument to the method", null, false)], null));
-            _cache.Add(new MemberKey(typeof(global::InheritOnlyReturns), MemberType.Method, "MyParentMethod", typeof(global::System.Boolean), [typeof(global::System.Boolean)]), new XmlComment(@"In this example, this summary is only visible for this method.", null, null, @"A boolean", null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::InheritOnlyReturns), MemberType.Method, "MyChildMethod", typeof(global::System.Boolean), []), new XmlComment(null, null, null, @"A boolean", null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::InheritAllButRemarks), MemberType.Method, "MyParentMethod", typeof(global::System.Boolean), [typeof(global::System.Boolean)]), new XmlComment(@"In this example, this summary is visible on all the methods.", null, @"The remarks can be inherited by other methods
+            cache.Add(@"M:InheritOnlyReturns.MyParentMethod(System.Boolean)~System.Boolean", new XmlComment(@"In this example, this summary is only visible for this method.", null, null, @"A boolean", null, false, null, null, null));
+            cache.Add(@"M:InheritOnlyReturns.MyChildMethod~System.Boolean", new XmlComment(null, null, null, @"A boolean", null, false, null, null, null));
+            cache.Add(@"M:InheritAllButRemarks.MyParentMethod(System.Boolean)~System.Boolean", new XmlComment(@"In this example, this summary is visible on all the methods.", null, @"The remarks can be inherited by other methods
 using the xpath expression.", @"A boolean", null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::InheritAllButRemarks), MemberType.Method, "MyChildMethod", typeof(global::System.Boolean), []), new XmlComment(@"In this example, this summary is visible on all the methods.", null, null, @"A boolean", null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ParamsAndParamRefs), MemberType.Method, "GetGenericValue", typeof(object), [typeof(object)]), new XmlComment(@"The GetGenericValue method.", null, @"This sample shows how to specify the T ParamsAndParamRefs.GetGenericValue&lt;T&gt;(T para)
+            cache.Add(@"M:InheritAllButRemarks.MyChildMethod~System.Boolean", new XmlComment(@"In this example, this summary is visible on all the methods.", null, null, @"A boolean", null, false, null, null, null));
+            cache.Add(@"M:GenericParent.GetTaskOfTuple~System.Threading.Tasks.Task{System.ValueTuple{System.Int32,System.String}}", new XmlComment(@"This method returns a generic type containing a tuple.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:GenericParent.GetTupleOfTask~System.ValueTuple{System.Int32,System.Collections.Generic.Dictionary{System.Int32,System.String}}", new XmlComment(@"This method returns a tuple with a generic type inside.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:GenericParent.GetTupleOfTask1``1~System.ValueTuple{System.Int32,System.Collections.Generic.Dictionary{System.Int32,``0}}", new XmlComment(@"This method return a tuple with a generic type containing a
+type parameter inside.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:GenericParent.GetTupleOfTask2``1~System.ValueTuple{``0,System.Collections.Generic.Dictionary{System.Int32,System.String}}", new XmlComment(@"This method return a tuple with a generic type containing a
+type parameter inside.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:GenericParent.GetNestedGeneric~System.Collections.Generic.Dictionary{System.Int32,System.Collections.Generic.Dictionary{System.Int32,System.String}}", new XmlComment(@"This method returns a nested generic with all types resolved.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:GenericParent.GetNestedGeneric1``1~System.Collections.Generic.Dictionary{System.Int32,System.Collections.Generic.Dictionary{System.Int32,``0}}", new XmlComment(@"This method returns a nested generic with a type parameter.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:ParamsAndParamRefs.GetGenericValue``1(``0)~``0", new XmlComment(@"The GetGenericValue method.", null, @"This sample shows how to specify the T ParamsAndParamRefs.GetGenericValue&lt;T&gt;(T para)
 method as a cref attribute.
 The parameter and return value are both of an arbitrary type,
 T", null, null, false, null, null, null));
 
-            return _cache;
+            return cache;
+        }
+    }
+
+    file static class DocumentationCommentIdHelper
+    {
+        /// <summary>
+        /// Generates a documentation comment ID for a type.
+        /// Example: T:Namespace.Outer+Inner`1 becomes T:Namespace.Outer.Inner`1
+        /// </summary>
+        public static string CreateDocumentationId(this Type type)
+        {
+            if (type == null)
+            {
+                throw new ArgumentNullException(nameof(type));
+            }
+
+            return "T:" + GetTypeDocId(type, includeGenericArguments: false, omitGenericArity: false);
         }
 
-        internal static bool TryGetXmlComment(Type? type, MethodInfo? methodInfo, [NotNullWhen(true)] out XmlComment? xmlComment)
+        /// <summary>
+        /// Generates a documentation comment ID for a property.
+        /// Example: P:Namespace.ContainingType.PropertyName or for an indexer P:Namespace.ContainingType.Item(System.Int32)
+        /// </summary>
+        public static string CreateDocumentationId(this PropertyInfo property)
         {
-            if (methodInfo is null)
+            if (property == null)
+            {
+                throw new ArgumentNullException(nameof(property));
+            }
+
+            var sb = new StringBuilder();
+            sb.Append("P:");
+
+            if (property.DeclaringType != null)
+            {
+                sb.Append(GetTypeDocId(property.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
+            }
+
+            sb.Append('.');
+            sb.Append(property.Name);
+
+            // For indexers, include the parameter list.
+            var indexParams = property.GetIndexParameters();
+            if (indexParams.Length > 0)
+            {
+                sb.Append('(');
+                for (int i = 0; i < indexParams.Length; i++)
+                {
+                    if (i > 0)
+                    {
+                        sb.Append(',');
+                    }
+
+                    sb.Append(GetTypeDocId(indexParams[i].ParameterType, includeGenericArguments: true, omitGenericArity: false));
+                }
+                sb.Append(')');
+            }
+
+            return sb.ToString();
+        }
+
+        /// <summary>
+        /// Generates a documentation comment ID for a method (or constructor).
+        /// For example:
+        ///   M:Namespace.ContainingType.MethodName(ParamType1,ParamType2)~ReturnType
+        ///   M:Namespace.ContainingType.#ctor(ParamType)
+        /// </summary>
+        public static string CreateDocumentationId(this MethodInfo method)
+        {
+            if (method == null)
+            {
+                throw new ArgumentNullException(nameof(method));
+            }
+
+            var sb = new StringBuilder();
+            sb.Append("M:");
+
+            // Append the fully qualified name of the declaring type.
+            if (method.DeclaringType != null)
+            {
+                sb.Append(GetTypeDocId(method.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
+            }
+
+            sb.Append('.');
+
+            // Append the method name, handling constructors specially.
+            if (method.IsConstructor)
             {
-                return Cache.TryGetValue(new MemberKey(type, MemberType.Property, null, null, null), out xmlComment);
+                sb.Append(method.IsStatic ? "#cctor" : "#ctor");
             }
+            else
+            {
+                sb.Append(method.Name);
+                if (method.IsGenericMethod)
+                {
+                    sb.Append("``");
+                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", method.GetGenericArguments().Length);
+                }
+            }
+
+            // Append the parameter list, if any.
+            var parameters = method.GetParameters();
+            if (parameters.Length > 0)
+            {
+                sb.Append('(');
+                for (int i = 0; i < parameters.Length; i++)
+                {
+                    if (i > 0)
+                    {
+                        sb.Append(',');
+                    }
 
-            return Cache.TryGetValue(MemberKey.FromMethodInfo(methodInfo), out xmlComment);
+                    // Omit the generic arity for the parameter type.
+                    sb.Append(GetTypeDocId(parameters[i].ParameterType, includeGenericArguments: true, omitGenericArity: true));
+                }
+                sb.Append(')');
+            }
+
+            // Append the return type after a '~' (if the method returns a value).
+            if (method.ReturnType != typeof(void))
+            {
+                sb.Append('~');
+                // Omit the generic arity for the return type.
+                sb.Append(GetTypeDocId(method.ReturnType, includeGenericArguments: true, omitGenericArity: true));
+            }
+
+            return sb.ToString();
         }
 
-        internal static bool TryGetXmlComment(Type? type, string? memberName, [NotNullWhen(true)] out XmlComment? xmlComment)
+        /// <summary>
+        /// Generates a documentation ID string for a type.
+        /// This method handles nested types (replacing '+' with '.'),
+        /// generic types, arrays, pointers, by-ref types, and generic parameters.
+        /// The <paramref name="includeGenericArguments"/> flag controls whether
+        /// constructed generic type arguments are emitted, while <paramref name="omitGenericArity"/>
+        /// controls whether the generic arity marker (e.g. "`1") is appended.
+        /// </summary>
+        private static string GetTypeDocId(Type type, bool includeGenericArguments, bool omitGenericArity)
         {
-            return Cache.TryGetValue(new MemberKey(type, memberName is null ? MemberType.Type : MemberType.Property, memberName, null, null), out xmlComment);
+            if (type.IsGenericParameter)
+            {
+                // Use `` for method-level generic parameters and ` for type-level.
+                if (type.DeclaringMethod != null)
+                {
+                    return "``" + type.GenericParameterPosition;
+                }
+                else if (type.DeclaringType != null)
+                {
+                    return "`" + type.GenericParameterPosition;
+                }
+                else
+                {
+                    return type.Name;
+                }
+            }
+
+            if (type.IsGenericType)
+            {
+                Type genericDef = type.GetGenericTypeDefinition();
+                string fullName = genericDef.FullName ?? genericDef.Name;
+
+                var sb = new StringBuilder(fullName.Length);
+
+                // Replace '+' with '.' for nested types
+                for (var i = 0; i < fullName.Length; i++)
+                {
+                    char c = fullName[i];
+                    if (c == '+')
+                    {
+                        sb.Append('.');
+                    }
+                    else if (c == '`')
+                    {
+                        break;
+                    }
+                    else
+                    {
+                        sb.Append(c);
+                    }
+                }
+
+                if (!omitGenericArity)
+                {
+                    int arity = genericDef.GetGenericArguments().Length;
+                    sb.Append('`');
+                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", arity);
+                }
+
+                if (includeGenericArguments && !type.IsGenericTypeDefinition)
+                {
+                    var typeArgs = type.GetGenericArguments();
+                    sb.Append('{');
+
+                    for (int i = 0; i < typeArgs.Length; i++)
+                    {
+                        if (i > 0)
+                        {
+                            sb.Append(',');
+                        }
+
+                        sb.Append(GetTypeDocId(typeArgs[i], includeGenericArguments, omitGenericArity));
+                    }
+
+                    sb.Append('}');
+                }
+
+                return sb.ToString();
+            }
+
+            // For non-generic types, use FullName (if available) and replace nested type separators.
+            return (type.FullName ?? type.Name).Replace('+', '.');
         }
     }
 
@@ -322,7 +420,7 @@ T", null, null, false, null, null, null));
             {
                 return Task.CompletedTask;
             }
-            if (XmlCommentCache.TryGetXmlComment(methodInfo.DeclaringType, methodInfo, out var methodComment))
+            if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
             {
                 if (methodComment.Summary is { } summary)
                 {
@@ -395,7 +493,7 @@ T", null, null, false, null, null, null));
         {
             if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
             {
-                if (XmlCommentCache.TryGetXmlComment(propertyInfo.DeclaringType, propertyInfo.Name, out var propertyComment))
+                if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
                 {
                     schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
                     if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -404,7 +502,7 @@ T", null, null, false, null, null, null));
                     }
                 }
             }
-            if (XmlCommentCache.TryGetXmlComment(context.JsonTypeInfo.Type, (string?)null, out var typeComment))
+            if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
             {
                 schema.Description = typeComment.Summary;
                 if (typeComment.Examples?.FirstOrDefault() is { } jsonString)

+ 189 - 107
src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs

@@ -29,8 +29,10 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
     using System;
     using System.Collections.Generic;
     using System.Diagnostics.CodeAnalysis;
+    using System.Globalization;
     using System.Linq;
     using System.Reflection;
+    using System.Text;
     using System.Text.Json;
     using System.Text.Json.Nodes;
     using System.Threading;
@@ -61,152 +63,232 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
     file record XmlResponseComment(string Code, string? Description, string? Example);
 
     [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
-    file sealed record MemberKey(
-        Type? DeclaringType,
-        MemberType MemberKind,
-        string? Name,
-        Type? ReturnType,
-        Type[]? Parameters) : IEquatable<MemberKey>
+    file static class XmlCommentCache
     {
-        public bool Equals(MemberKey? other)
+        private static Dictionary<string, XmlComment>? _cache;
+        public static Dictionary<string, XmlComment> Cache => _cache ??= GenerateCacheEntries();
+
+        private static Dictionary<string, XmlComment> GenerateCacheEntries()
         {
-            if (other is null) return false;
+            var cache = new Dictionary<string, XmlComment>();
 
-            // Check member kind
-            if (MemberKind != other.MemberKind) return false;
+            cache.Add(@"M:TestController.Get~System.String", new XmlComment(@"A summary of the action.", @"A description of the action.", null, null, null, false, null, null, null));
+            cache.Add(@"M:Test2Controller.Get(System.String)~System.String", new XmlComment(null, null, null, null, null, false, null, [new XmlParameterComment(@"name", @"The name of the person.", null, false)], [new XmlResponseComment(@"200", @"Returns the greeting.", @"")]));
+            cache.Add(@"M:Test2Controller.Get(System.Int32)~System.String", new XmlComment(null, null, null, null, null, false, null, [new XmlParameterComment(@"id", @"The id associated with the request.", null, false)], null));
+            cache.Add(@"M:Test2Controller.Post(Todo)~System.String", new XmlComment(null, null, null, null, null, false, null, [new XmlParameterComment(@"todo", @"The todo to insert into the database.", null, false)], null));
 
-            // Check declaring type, handling generic types
-            if (!TypesEqual(DeclaringType, other.DeclaringType)) return false;
+            return cache;
+        }
+    }
 
-            // Check name
-            if (Name != other.Name) return false;
+    file static class DocumentationCommentIdHelper
+    {
+        /// <summary>
+        /// Generates a documentation comment ID for a type.
+        /// Example: T:Namespace.Outer+Inner`1 becomes T:Namespace.Outer.Inner`1
+        /// </summary>
+        public static string CreateDocumentationId(this Type type)
+        {
+            if (type == null)
+            {
+                throw new ArgumentNullException(nameof(type));
+            }
 
-            // For methods, check return type and parameters
-            if (MemberKind == MemberType.Method)
+            return "T:" + GetTypeDocId(type, includeGenericArguments: false, omitGenericArity: false);
+        }
+
+        /// <summary>
+        /// Generates a documentation comment ID for a property.
+        /// Example: P:Namespace.ContainingType.PropertyName or for an indexer P:Namespace.ContainingType.Item(System.Int32)
+        /// </summary>
+        public static string CreateDocumentationId(this PropertyInfo property)
+        {
+            if (property == null)
             {
-                if (!TypesEqual(ReturnType, other.ReturnType)) return false;
-                if (Parameters is null || other.Parameters is null) return false;
-                if (Parameters.Length != other.Parameters.Length) return false;
+                throw new ArgumentNullException(nameof(property));
+            }
+
+            var sb = new StringBuilder();
+            sb.Append("P:");
+
+            if (property.DeclaringType != null)
+            {
+                sb.Append(GetTypeDocId(property.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
+            }
 
-                for (int i = 0; i < Parameters.Length; i++)
+            sb.Append('.');
+            sb.Append(property.Name);
+
+            // For indexers, include the parameter list.
+            var indexParams = property.GetIndexParameters();
+            if (indexParams.Length > 0)
+            {
+                sb.Append('(');
+                for (int i = 0; i < indexParams.Length; i++)
                 {
-                    if (!TypesEqual(Parameters[i], other.Parameters[i])) return false;
+                    if (i > 0)
+                    {
+                        sb.Append(',');
+                    }
+
+                    sb.Append(GetTypeDocId(indexParams[i].ParameterType, includeGenericArguments: true, omitGenericArity: false));
                 }
+                sb.Append(')');
             }
 
-            return true;
+            return sb.ToString();
         }
 
-        private static bool TypesEqual(Type? type1, Type? type2)
+        /// <summary>
+        /// Generates a documentation comment ID for a method (or constructor).
+        /// For example:
+        ///   M:Namespace.ContainingType.MethodName(ParamType1,ParamType2)~ReturnType
+        ///   M:Namespace.ContainingType.#ctor(ParamType)
+        /// </summary>
+        public static string CreateDocumentationId(this MethodInfo method)
         {
-            if (type1 == type2) return true;
-            if (type1 == null || type2 == null) return false;
+            if (method == null)
+            {
+                throw new ArgumentNullException(nameof(method));
+            }
+
+            var sb = new StringBuilder();
+            sb.Append("M:");
 
-            if (type1.IsGenericType && type2.IsGenericType)
+            // Append the fully qualified name of the declaring type.
+            if (method.DeclaringType != null)
             {
-                return type1.GetGenericTypeDefinition() == type2.GetGenericTypeDefinition();
+                sb.Append(GetTypeDocId(method.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
             }
 
-            return type1 == type2;
-        }
+            sb.Append('.');
 
-        public override int GetHashCode()
-        {
-            var hash = new HashCode();
-            hash.Add(GetTypeHashCode(DeclaringType));
-            hash.Add(MemberKind);
-            hash.Add(Name);
+            // Append the method name, handling constructors specially.
+            if (method.IsConstructor)
+            {
+                sb.Append(method.IsStatic ? "#cctor" : "#ctor");
+            }
+            else
+            {
+                sb.Append(method.Name);
+                if (method.IsGenericMethod)
+                {
+                    sb.Append("``");
+                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", method.GetGenericArguments().Length);
+                }
+            }
 
-            if (MemberKind == MemberType.Method)
+            // Append the parameter list, if any.
+            var parameters = method.GetParameters();
+            if (parameters.Length > 0)
             {
-                hash.Add(GetTypeHashCode(ReturnType));
-                if (Parameters is not null)
+                sb.Append('(');
+                for (int i = 0; i < parameters.Length; i++)
                 {
-                    foreach (var param in Parameters)
+                    if (i > 0)
                     {
-                        hash.Add(GetTypeHashCode(param));
+                        sb.Append(',');
                     }
+
+                    // Omit the generic arity for the parameter type.
+                    sb.Append(GetTypeDocId(parameters[i].ParameterType, includeGenericArguments: true, omitGenericArity: true));
                 }
+                sb.Append(')');
             }
 
-            return hash.ToHashCode();
-        }
+            // Append the return type after a '~' (if the method returns a value).
+            if (method.ReturnType != typeof(void))
+            {
+                sb.Append('~');
+                // Omit the generic arity for the return type.
+                sb.Append(GetTypeDocId(method.ReturnType, includeGenericArguments: true, omitGenericArity: true));
+            }
 
-        private static int GetTypeHashCode(Type? type)
-        {
-            if (type == null) return 0;
-            return type.IsGenericType ? type.GetGenericTypeDefinition().GetHashCode() : type.GetHashCode();
+            return sb.ToString();
         }
 
-        public static MemberKey FromMethodInfo(MethodInfo method)
+        /// <summary>
+        /// Generates a documentation ID string for a type.
+        /// This method handles nested types (replacing '+' with '.'),
+        /// generic types, arrays, pointers, by-ref types, and generic parameters.
+        /// The <paramref name="includeGenericArguments"/> flag controls whether
+        /// constructed generic type arguments are emitted, while <paramref name="omitGenericArity"/>
+        /// controls whether the generic arity marker (e.g. "`1") is appended.
+        /// </summary>
+        private static string GetTypeDocId(Type type, bool includeGenericArguments, bool omitGenericArity)
         {
-            return new MemberKey(
-                method.DeclaringType,
-                MemberType.Method,
-                method.Name,
-                method.ReturnType.IsGenericParameter ? typeof(object) : method.ReturnType,
-                method.GetParameters().Select(p => p.ParameterType.IsGenericParameter ? typeof(object) : p.ParameterType).ToArray());
-        }
+            if (type.IsGenericParameter)
+            {
+                // Use `` for method-level generic parameters and ` for type-level.
+                if (type.DeclaringMethod != null)
+                {
+                    return "``" + type.GenericParameterPosition;
+                }
+                else if (type.DeclaringType != null)
+                {
+                    return "`" + type.GenericParameterPosition;
+                }
+                else
+                {
+                    return type.Name;
+                }
+            }
 
-        public static MemberKey FromPropertyInfo(PropertyInfo property)
-        {
-            return new MemberKey(
-                property.DeclaringType,
-                MemberType.Property,
-                property.Name,
-                null,
-                null);
-        }
+            if (type.IsGenericType)
+            {
+                Type genericDef = type.GetGenericTypeDefinition();
+                string fullName = genericDef.FullName ?? genericDef.Name;
 
-        public static MemberKey FromTypeInfo(Type type)
-        {
-            return new MemberKey(
-                type,
-                MemberType.Type,
-                null,
-                null,
-                null);
-        }
-    }
+                var sb = new StringBuilder(fullName.Length);
 
-    file enum MemberType
-    {
-        Type,
-        Property,
-        Method
-    }
+                // Replace '+' with '.' for nested types
+                for (var i = 0; i < fullName.Length; i++)
+                {
+                    char c = fullName[i];
+                    if (c == '+')
+                    {
+                        sb.Append('.');
+                    }
+                    else if (c == '`')
+                    {
+                        break;
+                    }
+                    else
+                    {
+                        sb.Append(c);
+                    }
+                }
 
-    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
-    file static class XmlCommentCache
-    {
-        private static Dictionary<MemberKey, XmlComment>? _cache;
-        public static Dictionary<MemberKey, XmlComment> Cache => _cache ??= GenerateCacheEntries();
+                if (!omitGenericArity)
+                {
+                    int arity = genericDef.GetGenericArguments().Length;
+                    sb.Append('`');
+                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", arity);
+                }
 
-        private static Dictionary<MemberKey, XmlComment> GenerateCacheEntries()
-        {
-            var _cache = new Dictionary<MemberKey, XmlComment>();
+                if (includeGenericArguments && !type.IsGenericTypeDefinition)
+                {
+                    var typeArgs = type.GetGenericArguments();
+                    sb.Append('{');
+
+                    for (int i = 0; i < typeArgs.Length; i++)
+                    {
+                        if (i > 0)
+                        {
+                            sb.Append(',');
+                        }
 
-            _cache.Add(new MemberKey(typeof(global::TestController), MemberType.Method, "Get", typeof(global::System.String), []), new XmlComment(@"A summary of the action.", @"A description of the action.", null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::Test2Controller), MemberType.Method, "Get", typeof(global::System.String), [typeof(global::System.String)]), new XmlComment(null, null, null, null, null, false, null, [new XmlParameterComment(@"name", @"The name of the person.", null, false)], [new XmlResponseComment(@"200", @"Returns the greeting.", @"")]));
-            _cache.Add(new MemberKey(typeof(global::Test2Controller), MemberType.Method, "Get", typeof(global::System.String), [typeof(global::System.Int32)]), new XmlComment(null, null, null, null, null, false, null, [new XmlParameterComment(@"id", @"The id associated with the request.", null, false)], null));
-            _cache.Add(new MemberKey(typeof(global::Test2Controller), MemberType.Method, "Post", typeof(global::System.String), [typeof(global::Todo)]), new XmlComment(null, null, null, null, null, false, null, [new XmlParameterComment(@"todo", @"The todo to insert into the database.", null, false)], null));
+                        sb.Append(GetTypeDocId(typeArgs[i], includeGenericArguments, omitGenericArity));
+                    }
 
-            return _cache;
-        }
+                    sb.Append('}');
+                }
 
-        internal static bool TryGetXmlComment(Type? type, MethodInfo? methodInfo, [NotNullWhen(true)] out XmlComment? xmlComment)
-        {
-            if (methodInfo is null)
-            {
-                return Cache.TryGetValue(new MemberKey(type, MemberType.Property, null, null, null), out xmlComment);
+                return sb.ToString();
             }
 
-            return Cache.TryGetValue(MemberKey.FromMethodInfo(methodInfo), out xmlComment);
-        }
-
-        internal static bool TryGetXmlComment(Type? type, string? memberName, [NotNullWhen(true)] out XmlComment? xmlComment)
-        {
-            return Cache.TryGetValue(new MemberKey(type, memberName is null ? MemberType.Type : MemberType.Property, memberName, null, null), out xmlComment);
+            // For non-generic types, use FullName (if available) and replace nested type separators.
+            return (type.FullName ?? type.Name).Replace('+', '.');
         }
     }
 
@@ -223,7 +305,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
             {
                 return Task.CompletedTask;
             }
-            if (XmlCommentCache.TryGetXmlComment(methodInfo.DeclaringType, methodInfo, out var methodComment))
+            if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
             {
                 if (methodComment.Summary is { } summary)
                 {
@@ -296,7 +378,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
         {
             if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
             {
-                if (XmlCommentCache.TryGetXmlComment(propertyInfo.DeclaringType, propertyInfo.Name, out var propertyComment))
+                if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
                 {
                     schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
                     if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -305,7 +387,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
                     }
                 }
             }
-            if (XmlCommentCache.TryGetXmlComment(context.JsonTypeInfo.Type, (string?)null, out var typeComment))
+            if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
             {
                 schema.Description = typeComment.Summary;
                 if (typeComment.Examples?.FirstOrDefault() is { } jsonString)

+ 208 - 116
src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs

@@ -29,8 +29,10 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
     using System;
     using System.Collections.Generic;
     using System.Diagnostics.CodeAnalysis;
+    using System.Globalization;
     using System.Linq;
     using System.Reflection;
+    using System.Text;
     using System.Text.Json;
     using System.Text.Json.Nodes;
     using System.Threading;
@@ -61,160 +63,250 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
     file record XmlResponseComment(string Code, string? Description, string? Example);
 
     [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
-    file sealed record MemberKey(
-        Type? DeclaringType,
-        MemberType MemberKind,
-        string? Name,
-        Type? ReturnType,
-        Type[]? Parameters) : IEquatable<MemberKey>
+    file static class XmlCommentCache
     {
-        public bool Equals(MemberKey? other)
+        private static Dictionary<string, XmlComment>? _cache;
+        public static Dictionary<string, XmlComment> Cache => _cache ??= GenerateCacheEntries();
+
+        private static Dictionary<string, XmlComment> GenerateCacheEntries()
         {
-            if (other is null) return false;
+            var cache = new Dictionary<string, XmlComment>();
+
+            cache.Add(@"M:RouteHandlerExtensionMethods.Get~System.String", new XmlComment(@"A summary of the action.", @"A description of the action.", null, null, null, false, null, null, null));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Get2(System.String)~System.String", new XmlComment(null, null, null, null, null, false, null, [new XmlParameterComment(@"name", @"The name of the person.", null, false)], [new XmlResponseComment(@"200", @"Returns the greeting.", @"")]));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Get3(System.String)~System.String", new XmlComment(null, null, null, null, null, false, null, [new XmlParameterComment(@"name", @"The name of the person.", @"Testy McTester", false)], null));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Get4~Microsoft.AspNetCore.Http.HttpResults.NotFound{System.String}", new XmlComment(null, null, null, null, null, false, null, null, [new XmlResponseComment(@"404", @"Indicates that the value was not found.", @"")]));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Get5~Microsoft.AspNetCore.Http.HttpResults.Results{Microsoft.AspNetCore.Http.HttpResults.NotFound{System.String},Microsoft.AspNetCore.Http.HttpResults.Ok{System.String},Microsoft.AspNetCore.Http.HttpResults.Created}", new XmlComment(null, null, null, null, null, false, null, null, [new XmlResponseComment(@"200", @"Indicates that the value is even.", @""), new XmlResponseComment(@"201", @"Indicates that the value is less than 50.", @""), new XmlResponseComment(@"404", @"Indicates that the value was not found.", @"")]));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Post6(User)~Microsoft.AspNetCore.Http.IResult", new XmlComment(@"Creates a new user.", null, @"Sample request:
+    POST /6
+    {
+        ""username"": ""johndoe"",
+        ""email"": ""[email protected]""
+    }", null, null, false, null, [new XmlParameterComment(@"user", @"The user information.", @"{""username"": ""johndoe"", ""email"": ""[email protected]""}", false)], [new XmlResponseComment(@"201", @"Successfully created the user.", @""), new XmlResponseComment(@"400", @"If the user data is invalid.", @"")]));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Put7(System.Nullable{System.Int32},System.String)~Microsoft.AspNetCore.Http.IResult", new XmlComment(@"Updates an existing record.", null, null, null, null, false, null, [new XmlParameterComment(@"id", @"Legacy ID parameter - use uuid instead.", null, true), new XmlParameterComment(@"uuid", @"Unique identifier for the record.", null, false)], [new XmlResponseComment(@"204", @"Update successful.", @""), new XmlResponseComment(@"404", @"Legacy response - will be removed.", @"")]));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Get8~System.Threading.Tasks.Task", new XmlComment(@"A summary of Get8.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Get9~System.Threading.Tasks.ValueTask", new XmlComment(@"A summary of Get9.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Get10~System.Threading.Tasks.Task", new XmlComment(@"A summary of Get10.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Get11~System.Threading.Tasks.ValueTask", new XmlComment(@"A summary of Get11.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Get12~System.Threading.Tasks.Task{System.String}", new XmlComment(@"A summary of Get12.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Get13~System.Threading.Tasks.ValueTask{System.String}", new XmlComment(@"A summary of Get13.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Get14~System.Threading.Tasks.Task{Holder{System.String}}", new XmlComment(@"A summary of Get14.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Get15~System.Threading.Tasks.Task{Holder{System.String}}", new XmlComment(@"A summary of Get15.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Post16(Example)", new XmlComment(@"A summary of Post16.", null, null, null, null, false, null, null, null));
+            cache.Add(@"M:RouteHandlerExtensionMethods.Get17(System.Int32[])~System.Int32[][]", new XmlComment(@"A summary of Get17.", null, null, null, null, false, null, null, null));
+
+            return cache;
+        }
+    }
 
-            // Check member kind
-            if (MemberKind != other.MemberKind) return false;
+    file static class DocumentationCommentIdHelper
+    {
+        /// <summary>
+        /// Generates a documentation comment ID for a type.
+        /// Example: T:Namespace.Outer+Inner`1 becomes T:Namespace.Outer.Inner`1
+        /// </summary>
+        public static string CreateDocumentationId(this Type type)
+        {
+            if (type == null)
+            {
+                throw new ArgumentNullException(nameof(type));
+            }
+
+            return "T:" + GetTypeDocId(type, includeGenericArguments: false, omitGenericArity: false);
+        }
 
-            // Check declaring type, handling generic types
-            if (!TypesEqual(DeclaringType, other.DeclaringType)) return false;
+        /// <summary>
+        /// Generates a documentation comment ID for a property.
+        /// Example: P:Namespace.ContainingType.PropertyName or for an indexer P:Namespace.ContainingType.Item(System.Int32)
+        /// </summary>
+        public static string CreateDocumentationId(this PropertyInfo property)
+        {
+            if (property == null)
+            {
+                throw new ArgumentNullException(nameof(property));
+            }
 
-            // Check name
-            if (Name != other.Name) return false;
+            var sb = new StringBuilder();
+            sb.Append("P:");
 
-            // For methods, check return type and parameters
-            if (MemberKind == MemberType.Method)
+            if (property.DeclaringType != null)
             {
-                if (!TypesEqual(ReturnType, other.ReturnType)) return false;
-                if (Parameters is null || other.Parameters is null) return false;
-                if (Parameters.Length != other.Parameters.Length) return false;
+                sb.Append(GetTypeDocId(property.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
+            }
+
+            sb.Append('.');
+            sb.Append(property.Name);
 
-                for (int i = 0; i < Parameters.Length; i++)
+            // For indexers, include the parameter list.
+            var indexParams = property.GetIndexParameters();
+            if (indexParams.Length > 0)
+            {
+                sb.Append('(');
+                for (int i = 0; i < indexParams.Length; i++)
                 {
-                    if (!TypesEqual(Parameters[i], other.Parameters[i])) return false;
+                    if (i > 0)
+                    {
+                        sb.Append(',');
+                    }
+
+                    sb.Append(GetTypeDocId(indexParams[i].ParameterType, includeGenericArguments: true, omitGenericArity: false));
                 }
+                sb.Append(')');
             }
 
-            return true;
+            return sb.ToString();
         }
 
-        private static bool TypesEqual(Type? type1, Type? type2)
+        /// <summary>
+        /// Generates a documentation comment ID for a method (or constructor).
+        /// For example:
+        ///   M:Namespace.ContainingType.MethodName(ParamType1,ParamType2)~ReturnType
+        ///   M:Namespace.ContainingType.#ctor(ParamType)
+        /// </summary>
+        public static string CreateDocumentationId(this MethodInfo method)
         {
-            if (type1 == type2) return true;
-            if (type1 == null || type2 == null) return false;
+            if (method == null)
+            {
+                throw new ArgumentNullException(nameof(method));
+            }
 
-            if (type1.IsGenericType && type2.IsGenericType)
+            var sb = new StringBuilder();
+            sb.Append("M:");
+
+            // Append the fully qualified name of the declaring type.
+            if (method.DeclaringType != null)
             {
-                return type1.GetGenericTypeDefinition() == type2.GetGenericTypeDefinition();
+                sb.Append(GetTypeDocId(method.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
             }
 
-            return type1 == type2;
-        }
+            sb.Append('.');
 
-        public override int GetHashCode()
-        {
-            var hash = new HashCode();
-            hash.Add(GetTypeHashCode(DeclaringType));
-            hash.Add(MemberKind);
-            hash.Add(Name);
+            // Append the method name, handling constructors specially.
+            if (method.IsConstructor)
+            {
+                sb.Append(method.IsStatic ? "#cctor" : "#ctor");
+            }
+            else
+            {
+                sb.Append(method.Name);
+                if (method.IsGenericMethod)
+                {
+                    sb.Append("``");
+                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", method.GetGenericArguments().Length);
+                }
+            }
 
-            if (MemberKind == MemberType.Method)
+            // Append the parameter list, if any.
+            var parameters = method.GetParameters();
+            if (parameters.Length > 0)
             {
-                hash.Add(GetTypeHashCode(ReturnType));
-                if (Parameters is not null)
+                sb.Append('(');
+                for (int i = 0; i < parameters.Length; i++)
                 {
-                    foreach (var param in Parameters)
+                    if (i > 0)
                     {
-                        hash.Add(GetTypeHashCode(param));
+                        sb.Append(',');
                     }
+
+                    // Omit the generic arity for the parameter type.
+                    sb.Append(GetTypeDocId(parameters[i].ParameterType, includeGenericArguments: true, omitGenericArity: true));
                 }
+                sb.Append(')');
             }
 
-            return hash.ToHashCode();
-        }
+            // Append the return type after a '~' (if the method returns a value).
+            if (method.ReturnType != typeof(void))
+            {
+                sb.Append('~');
+                // Omit the generic arity for the return type.
+                sb.Append(GetTypeDocId(method.ReturnType, includeGenericArguments: true, omitGenericArity: true));
+            }
 
-        private static int GetTypeHashCode(Type? type)
-        {
-            if (type == null) return 0;
-            return type.IsGenericType ? type.GetGenericTypeDefinition().GetHashCode() : type.GetHashCode();
+            return sb.ToString();
         }
 
-        public static MemberKey FromMethodInfo(MethodInfo method)
+        /// <summary>
+        /// Generates a documentation ID string for a type.
+        /// This method handles nested types (replacing '+' with '.'),
+        /// generic types, arrays, pointers, by-ref types, and generic parameters.
+        /// The <paramref name="includeGenericArguments"/> flag controls whether
+        /// constructed generic type arguments are emitted, while <paramref name="omitGenericArity"/>
+        /// controls whether the generic arity marker (e.g. "`1") is appended.
+        /// </summary>
+        private static string GetTypeDocId(Type type, bool includeGenericArguments, bool omitGenericArity)
         {
-            return new MemberKey(
-                method.DeclaringType,
-                MemberType.Method,
-                method.Name,
-                method.ReturnType.IsGenericParameter ? typeof(object) : method.ReturnType,
-                method.GetParameters().Select(p => p.ParameterType.IsGenericParameter ? typeof(object) : p.ParameterType).ToArray());
-        }
+            if (type.IsGenericParameter)
+            {
+                // Use `` for method-level generic parameters and ` for type-level.
+                if (type.DeclaringMethod != null)
+                {
+                    return "``" + type.GenericParameterPosition;
+                }
+                else if (type.DeclaringType != null)
+                {
+                    return "`" + type.GenericParameterPosition;
+                }
+                else
+                {
+                    return type.Name;
+                }
+            }
 
-        public static MemberKey FromPropertyInfo(PropertyInfo property)
-        {
-            return new MemberKey(
-                property.DeclaringType,
-                MemberType.Property,
-                property.Name,
-                null,
-                null);
-        }
+            if (type.IsGenericType)
+            {
+                Type genericDef = type.GetGenericTypeDefinition();
+                string fullName = genericDef.FullName ?? genericDef.Name;
 
-        public static MemberKey FromTypeInfo(Type type)
-        {
-            return new MemberKey(
-                type,
-                MemberType.Type,
-                null,
-                null,
-                null);
-        }
-    }
+                var sb = new StringBuilder(fullName.Length);
 
-    file enum MemberType
-    {
-        Type,
-        Property,
-        Method
-    }
+                // Replace '+' with '.' for nested types
+                for (var i = 0; i < fullName.Length; i++)
+                {
+                    char c = fullName[i];
+                    if (c == '+')
+                    {
+                        sb.Append('.');
+                    }
+                    else if (c == '`')
+                    {
+                        break;
+                    }
+                    else
+                    {
+                        sb.Append(c);
+                    }
+                }
 
-    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
-    file static class XmlCommentCache
-    {
-        private static Dictionary<MemberKey, XmlComment>? _cache;
-        public static Dictionary<MemberKey, XmlComment> Cache => _cache ??= GenerateCacheEntries();
+                if (!omitGenericArity)
+                {
+                    int arity = genericDef.GetGenericArguments().Length;
+                    sb.Append('`');
+                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", arity);
+                }
 
-        private static Dictionary<MemberKey, XmlComment> GenerateCacheEntries()
-        {
-            var _cache = new Dictionary<MemberKey, XmlComment>();
-
-            _cache.Add(new MemberKey(typeof(global::RouteHandlerExtensionMethods), MemberType.Method, "Get", typeof(global::System.String), []), new XmlComment(@"A summary of the action.", @"A description of the action.", null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::RouteHandlerExtensionMethods), MemberType.Method, "Get2", typeof(global::System.String), [typeof(global::System.String)]), new XmlComment(null, null, null, null, null, false, null, [new XmlParameterComment(@"name", @"The name of the person.", null, false)], [new XmlResponseComment(@"200", @"Returns the greeting.", @"")]));
-            _cache.Add(new MemberKey(typeof(global::RouteHandlerExtensionMethods), MemberType.Method, "Get3", typeof(global::System.String), [typeof(global::System.String)]), new XmlComment(null, null, null, null, null, false, null, [new XmlParameterComment(@"name", @"The name of the person.", @"Testy McTester", false)], null));
-            _cache.Add(new MemberKey(typeof(global::RouteHandlerExtensionMethods), MemberType.Method, "Get4", typeof(global::Microsoft.AspNetCore.Http.HttpResults.NotFound<>), []), new XmlComment(null, null, null, null, null, false, null, null, [new XmlResponseComment(@"404", @"Indicates that the value was not found.", @"")]));
-            _cache.Add(new MemberKey(typeof(global::RouteHandlerExtensionMethods), MemberType.Method, "Get5", typeof(global::Microsoft.AspNetCore.Http.HttpResults.Results<,,>), []), new XmlComment(null, null, null, null, null, false, null, null, [new XmlResponseComment(@"200", @"Indicates that the value is even.", @""), new XmlResponseComment(@"201", @"Indicates that the value is less than 50.", @""), new XmlResponseComment(@"404", @"Indicates that the value was not found.", @"")]));
-            _cache.Add(new MemberKey(typeof(global::RouteHandlerExtensionMethods), MemberType.Method, "Post6", typeof(global::Microsoft.AspNetCore.Http.IResult), [typeof(global::User)]), new XmlComment(@"Creates a new user.", null, @"Sample request:
-    POST /6
-    {
-        ""username"": ""johndoe"",
-        ""email"": ""[email protected]""
-    }", null, null, false, null, [new XmlParameterComment(@"user", @"The user information.", @"{""username"": ""johndoe"", ""email"": ""[email protected]""}", false)], [new XmlResponseComment(@"201", @"Successfully created the user.", @""), new XmlResponseComment(@"400", @"If the user data is invalid.", @"")]));
-            _cache.Add(new MemberKey(typeof(global::RouteHandlerExtensionMethods), MemberType.Method, "Put7", typeof(global::Microsoft.AspNetCore.Http.IResult), [typeof(global::System.Int32?), typeof(global::System.String)]), new XmlComment(@"Updates an existing record.", null, null, null, null, false, null, [new XmlParameterComment(@"id", @"Legacy ID parameter - use uuid instead.", null, true), new XmlParameterComment(@"uuid", @"Unique identifier for the record.", null, false)], [new XmlResponseComment(@"204", @"Update successful.", @""), new XmlResponseComment(@"404", @"Legacy response - will be removed.", @"")]));
+                if (includeGenericArguments && !type.IsGenericTypeDefinition)
+                {
+                    var typeArgs = type.GetGenericArguments();
+                    sb.Append('{');
 
-            return _cache;
-        }
+                    for (int i = 0; i < typeArgs.Length; i++)
+                    {
+                        if (i > 0)
+                        {
+                            sb.Append(',');
+                        }
 
-        internal static bool TryGetXmlComment(Type? type, MethodInfo? methodInfo, [NotNullWhen(true)] out XmlComment? xmlComment)
-        {
-            if (methodInfo is null)
-            {
-                return Cache.TryGetValue(new MemberKey(type, MemberType.Property, null, null, null), out xmlComment);
-            }
+                        sb.Append(GetTypeDocId(typeArgs[i], includeGenericArguments, omitGenericArity));
+                    }
 
-            return Cache.TryGetValue(MemberKey.FromMethodInfo(methodInfo), out xmlComment);
-        }
+                    sb.Append('}');
+                }
 
-        internal static bool TryGetXmlComment(Type? type, string? memberName, [NotNullWhen(true)] out XmlComment? xmlComment)
-        {
-            return Cache.TryGetValue(new MemberKey(type, memberName is null ? MemberType.Type : MemberType.Property, memberName, null, null), out xmlComment);
+                return sb.ToString();
+            }
+
+            // For non-generic types, use FullName (if available) and replace nested type separators.
+            return (type.FullName ?? type.Name).Replace('+', '.');
         }
     }
 
@@ -231,7 +323,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
             {
                 return Task.CompletedTask;
             }
-            if (XmlCommentCache.TryGetXmlComment(methodInfo.DeclaringType, methodInfo, out var methodComment))
+            if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
             {
                 if (methodComment.Summary is { } summary)
                 {
@@ -304,7 +396,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
         {
             if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
             {
-                if (XmlCommentCache.TryGetXmlComment(propertyInfo.DeclaringType, propertyInfo.Name, out var propertyComment))
+                if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
                 {
                     schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
                     if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -313,7 +405,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
                     }
                 }
             }
-            if (XmlCommentCache.TryGetXmlComment(context.JsonTypeInfo.Type, (string?)null, out var typeComment))
+            if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
             {
                 schema.Description = typeComment.Summary;
                 if (typeComment.Examples?.FirstOrDefault() is { } jsonString)

+ 217 - 135
src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs

@@ -29,8 +29,10 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
     using System;
     using System.Collections.Generic;
     using System.Diagnostics.CodeAnalysis;
+    using System.Globalization;
     using System.Linq;
     using System.Reflection;
+    using System.Text;
     using System.Text.Json;
     using System.Text.Json.Nodes;
     using System.Threading;
@@ -61,178 +63,258 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
     file record XmlResponseComment(string Code, string? Description, string? Example);
 
     [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
-    file sealed record MemberKey(
-        Type? DeclaringType,
-        MemberType MemberKind,
-        string? Name,
-        Type? ReturnType,
-        Type[]? Parameters) : IEquatable<MemberKey>
+    file static class XmlCommentCache
     {
-        public bool Equals(MemberKey? other)
+        private static Dictionary<string, XmlComment>? _cache;
+        public static Dictionary<string, XmlComment> Cache => _cache ??= GenerateCacheEntries();
+
+        private static Dictionary<string, XmlComment> GenerateCacheEntries()
         {
-            if (other is null) return false;
+            var cache = new Dictionary<string, XmlComment>();
+
+            cache.Add(@"T:Todo", new XmlComment(@"This is a todo item.", null, null, null, null, false, null, null, null));
+            cache.Add(@"T:Project", new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, null, null));
+            cache.Add(@"T:ProjectBoard.BoardItem", new XmlComment(@"An item on the board.", null, null, null, null, false, null, null, null));
+            cache.Add(@"T:ProjectBoard.ProtectedInternalElement", new XmlComment(@"Can find this XML comment.", null, null, null, null, false, null, null, null));
+            cache.Add(@"T:ProjectRecord", new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, [new XmlParameterComment(@"Name", @"The name of the project.", null, false), new XmlParameterComment(@"Description", @"The description of the project.", null, false)], null));
+            cache.Add(@"T:User", new XmlComment(null, null, null, null, null, false, null, null, null));
+            cache.Add(@"P:ProjectBoard.ProtectedInternalElement.Name", new XmlComment(@"The unique identifier for the element.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:ProjectRecord.Name", new XmlComment(@"The name of the project.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:ProjectRecord.Description", new XmlComment(@"The description of the project.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:TodoWithDescription.Id", new XmlComment(@"The identifier of the todo.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:TodoWithDescription.Name", new XmlComment(null, null, null, null, @"The name of the todo.", false, null, null, null));
+            cache.Add(@"P:TodoWithDescription.Description", new XmlComment(@"A description of the the todo.", null, null, null, @"Another description of the todo.", false, null, null, null));
+            cache.Add(@"P:TypeWithExamples.BooleanType", new XmlComment(null, null, null, null, null, false, [@"true"], null, null));
+            cache.Add(@"P:TypeWithExamples.IntegerType", new XmlComment(null, null, null, null, null, false, [@"42"], null, null));
+            cache.Add(@"P:TypeWithExamples.LongType", new XmlComment(null, null, null, null, null, false, [@"1234567890123456789"], null, null));
+            cache.Add(@"P:TypeWithExamples.DoubleType", new XmlComment(null, null, null, null, null, false, [@"3.14"], null, null));
+            cache.Add(@"P:TypeWithExamples.FloatType", new XmlComment(null, null, null, null, null, false, [@"3.14"], null, null));
+            cache.Add(@"P:TypeWithExamples.DateTimeType", new XmlComment(null, null, null, null, null, false, [@"2022-01-01T00:00:00Z"], null, null));
+            cache.Add(@"P:TypeWithExamples.DateOnlyType", new XmlComment(null, null, null, null, null, false, [@"2022-01-01"], null, null));
+            cache.Add(@"P:TypeWithExamples.StringType", new XmlComment(null, null, null, null, null, false, [@"Hello, World!"], null, null));
+            cache.Add(@"P:TypeWithExamples.GuidType", new XmlComment(null, null, null, null, null, false, [@"2d8f1eac-b5c6-4e29-8c62-4d9d75ef3d3d"], null, null));
+            cache.Add(@"P:TypeWithExamples.TimeOnlyType", new XmlComment(null, null, null, null, null, false, [@"12:30:45"], null, null));
+            cache.Add(@"P:TypeWithExamples.TimeSpanType", new XmlComment(null, null, null, null, null, false, [@"P3DT4H5M"], null, null));
+            cache.Add(@"P:TypeWithExamples.ByteType", new XmlComment(null, null, null, null, null, false, [@"255"], null, null));
+            cache.Add(@"P:TypeWithExamples.DecimalType", new XmlComment(null, null, null, null, null, false, [@"3.14159265359"], null, null));
+            cache.Add(@"P:TypeWithExamples.UriType", new XmlComment(null, null, null, null, null, false, [@"https://example.com"], null, null));
+            cache.Add(@"P:IUser.Id", new XmlComment(@"The unique identifier for the user.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:IUser.Name", new XmlComment(@"The user's display name.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:User.Id", new XmlComment(@"The unique identifier for the user.", null, null, null, null, false, null, null, null));
+            cache.Add(@"P:User.Name", new XmlComment(@"The user's display name.", null, null, null, null, false, null, null, null));
+
+            return cache;
+        }
+    }
 
-            // Check member kind
-            if (MemberKind != other.MemberKind) return false;
+    file static class DocumentationCommentIdHelper
+    {
+        /// <summary>
+        /// Generates a documentation comment ID for a type.
+        /// Example: T:Namespace.Outer+Inner`1 becomes T:Namespace.Outer.Inner`1
+        /// </summary>
+        public static string CreateDocumentationId(this Type type)
+        {
+            if (type == null)
+            {
+                throw new ArgumentNullException(nameof(type));
+            }
 
-            // Check declaring type, handling generic types
-            if (!TypesEqual(DeclaringType, other.DeclaringType)) return false;
+            return "T:" + GetTypeDocId(type, includeGenericArguments: false, omitGenericArity: false);
+        }
 
-            // Check name
-            if (Name != other.Name) return false;
+        /// <summary>
+        /// Generates a documentation comment ID for a property.
+        /// Example: P:Namespace.ContainingType.PropertyName or for an indexer P:Namespace.ContainingType.Item(System.Int32)
+        /// </summary>
+        public static string CreateDocumentationId(this PropertyInfo property)
+        {
+            if (property == null)
+            {
+                throw new ArgumentNullException(nameof(property));
+            }
+
+            var sb = new StringBuilder();
+            sb.Append("P:");
 
-            // For methods, check return type and parameters
-            if (MemberKind == MemberType.Method)
+            if (property.DeclaringType != null)
             {
-                if (!TypesEqual(ReturnType, other.ReturnType)) return false;
-                if (Parameters is null || other.Parameters is null) return false;
-                if (Parameters.Length != other.Parameters.Length) return false;
+                sb.Append(GetTypeDocId(property.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
+            }
 
-                for (int i = 0; i < Parameters.Length; i++)
+            sb.Append('.');
+            sb.Append(property.Name);
+
+            // For indexers, include the parameter list.
+            var indexParams = property.GetIndexParameters();
+            if (indexParams.Length > 0)
+            {
+                sb.Append('(');
+                for (int i = 0; i < indexParams.Length; i++)
                 {
-                    if (!TypesEqual(Parameters[i], other.Parameters[i])) return false;
+                    if (i > 0)
+                    {
+                        sb.Append(',');
+                    }
+
+                    sb.Append(GetTypeDocId(indexParams[i].ParameterType, includeGenericArguments: true, omitGenericArity: false));
                 }
+                sb.Append(')');
             }
 
-            return true;
+            return sb.ToString();
         }
 
-        private static bool TypesEqual(Type? type1, Type? type2)
+        /// <summary>
+        /// Generates a documentation comment ID for a method (or constructor).
+        /// For example:
+        ///   M:Namespace.ContainingType.MethodName(ParamType1,ParamType2)~ReturnType
+        ///   M:Namespace.ContainingType.#ctor(ParamType)
+        /// </summary>
+        public static string CreateDocumentationId(this MethodInfo method)
         {
-            if (type1 == type2) return true;
-            if (type1 == null || type2 == null) return false;
+            if (method == null)
+            {
+                throw new ArgumentNullException(nameof(method));
+            }
 
-            if (type1.IsGenericType && type2.IsGenericType)
+            var sb = new StringBuilder();
+            sb.Append("M:");
+
+            // Append the fully qualified name of the declaring type.
+            if (method.DeclaringType != null)
             {
-                return type1.GetGenericTypeDefinition() == type2.GetGenericTypeDefinition();
+                sb.Append(GetTypeDocId(method.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
             }
 
-            return type1 == type2;
-        }
+            sb.Append('.');
 
-        public override int GetHashCode()
-        {
-            var hash = new HashCode();
-            hash.Add(GetTypeHashCode(DeclaringType));
-            hash.Add(MemberKind);
-            hash.Add(Name);
+            // Append the method name, handling constructors specially.
+            if (method.IsConstructor)
+            {
+                sb.Append(method.IsStatic ? "#cctor" : "#ctor");
+            }
+            else
+            {
+                sb.Append(method.Name);
+                if (method.IsGenericMethod)
+                {
+                    sb.Append("``");
+                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", method.GetGenericArguments().Length);
+                }
+            }
 
-            if (MemberKind == MemberType.Method)
+            // Append the parameter list, if any.
+            var parameters = method.GetParameters();
+            if (parameters.Length > 0)
             {
-                hash.Add(GetTypeHashCode(ReturnType));
-                if (Parameters is not null)
+                sb.Append('(');
+                for (int i = 0; i < parameters.Length; i++)
                 {
-                    foreach (var param in Parameters)
+                    if (i > 0)
                     {
-                        hash.Add(GetTypeHashCode(param));
+                        sb.Append(',');
                     }
+
+                    // Omit the generic arity for the parameter type.
+                    sb.Append(GetTypeDocId(parameters[i].ParameterType, includeGenericArguments: true, omitGenericArity: true));
                 }
+                sb.Append(')');
             }
 
-            return hash.ToHashCode();
-        }
+            // Append the return type after a '~' (if the method returns a value).
+            if (method.ReturnType != typeof(void))
+            {
+                sb.Append('~');
+                // Omit the generic arity for the return type.
+                sb.Append(GetTypeDocId(method.ReturnType, includeGenericArguments: true, omitGenericArity: true));
+            }
 
-        private static int GetTypeHashCode(Type? type)
-        {
-            if (type == null) return 0;
-            return type.IsGenericType ? type.GetGenericTypeDefinition().GetHashCode() : type.GetHashCode();
+            return sb.ToString();
         }
 
-        public static MemberKey FromMethodInfo(MethodInfo method)
+        /// <summary>
+        /// Generates a documentation ID string for a type.
+        /// This method handles nested types (replacing '+' with '.'),
+        /// generic types, arrays, pointers, by-ref types, and generic parameters.
+        /// The <paramref name="includeGenericArguments"/> flag controls whether
+        /// constructed generic type arguments are emitted, while <paramref name="omitGenericArity"/>
+        /// controls whether the generic arity marker (e.g. "`1") is appended.
+        /// </summary>
+        private static string GetTypeDocId(Type type, bool includeGenericArguments, bool omitGenericArity)
         {
-            return new MemberKey(
-                method.DeclaringType,
-                MemberType.Method,
-                method.Name,
-                method.ReturnType.IsGenericParameter ? typeof(object) : method.ReturnType,
-                method.GetParameters().Select(p => p.ParameterType.IsGenericParameter ? typeof(object) : p.ParameterType).ToArray());
-        }
+            if (type.IsGenericParameter)
+            {
+                // Use `` for method-level generic parameters and ` for type-level.
+                if (type.DeclaringMethod != null)
+                {
+                    return "``" + type.GenericParameterPosition;
+                }
+                else if (type.DeclaringType != null)
+                {
+                    return "`" + type.GenericParameterPosition;
+                }
+                else
+                {
+                    return type.Name;
+                }
+            }
 
-        public static MemberKey FromPropertyInfo(PropertyInfo property)
-        {
-            return new MemberKey(
-                property.DeclaringType,
-                MemberType.Property,
-                property.Name,
-                null,
-                null);
-        }
+            if (type.IsGenericType)
+            {
+                Type genericDef = type.GetGenericTypeDefinition();
+                string fullName = genericDef.FullName ?? genericDef.Name;
 
-        public static MemberKey FromTypeInfo(Type type)
-        {
-            return new MemberKey(
-                type,
-                MemberType.Type,
-                null,
-                null,
-                null);
-        }
-    }
+                var sb = new StringBuilder(fullName.Length);
 
-    file enum MemberType
-    {
-        Type,
-        Property,
-        Method
-    }
+                // Replace '+' with '.' for nested types
+                for (var i = 0; i < fullName.Length; i++)
+                {
+                    char c = fullName[i];
+                    if (c == '+')
+                    {
+                        sb.Append('.');
+                    }
+                    else if (c == '`')
+                    {
+                        break;
+                    }
+                    else
+                    {
+                        sb.Append(c);
+                    }
+                }
 
-    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
-    file static class XmlCommentCache
-    {
-        private static Dictionary<MemberKey, XmlComment>? _cache;
-        public static Dictionary<MemberKey, XmlComment> Cache => _cache ??= GenerateCacheEntries();
+                if (!omitGenericArity)
+                {
+                    int arity = genericDef.GetGenericArguments().Length;
+                    sb.Append('`');
+                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", arity);
+                }
 
-        private static Dictionary<MemberKey, XmlComment> GenerateCacheEntries()
-        {
-            var _cache = new Dictionary<MemberKey, XmlComment>();
-
-            _cache.Add(new MemberKey(typeof(global::Todo), MemberType.Type, null, null, []), new XmlComment(@"This is a todo item.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::Project), MemberType.Type, null, null, []), new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ProjectBoard.BoardItem), MemberType.Type, null, null, []), new XmlComment(@"An item on the board.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ProjectBoard.ProtectedInternalElement), MemberType.Type, null, null, []), new XmlComment(@"Can find this XML comment.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ProjectRecord), MemberType.Type, null, null, []), new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, [new XmlParameterComment(@"Name", @"The name of the project.", null, false), new XmlParameterComment(@"Description", @"The description of the project.", null, false)], null));
-            _cache.Add(new MemberKey(typeof(global::User), MemberType.Type, null, null, []), new XmlComment(null, null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ProjectBoard.ProtectedInternalElement), MemberType.Property, "Name", null, []), new XmlComment(@"The unique identifier for the element.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ProjectRecord), MemberType.Property, "Name", null, []), new XmlComment(@"The name of the project.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::ProjectRecord), MemberType.Property, "Description", null, []), new XmlComment(@"The description of the project.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::TodoWithDescription), MemberType.Property, "Id", null, []), new XmlComment(@"The identifier of the todo.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::TodoWithDescription), MemberType.Property, "Name", null, []), new XmlComment(null, null, null, null, @"The name of the todo.", false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::TodoWithDescription), MemberType.Property, "Description", null, []), new XmlComment(@"A description of the the todo.", null, null, null, @"Another description of the todo.", false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::TypeWithExamples), MemberType.Property, "BooleanType", null, []), new XmlComment(null, null, null, null, null, false, [@"true"], null, null));
-            _cache.Add(new MemberKey(typeof(global::TypeWithExamples), MemberType.Property, "IntegerType", null, []), new XmlComment(null, null, null, null, null, false, [@"42"], null, null));
-            _cache.Add(new MemberKey(typeof(global::TypeWithExamples), MemberType.Property, "LongType", null, []), new XmlComment(null, null, null, null, null, false, [@"1234567890123456789"], null, null));
-            _cache.Add(new MemberKey(typeof(global::TypeWithExamples), MemberType.Property, "DoubleType", null, []), new XmlComment(null, null, null, null, null, false, [@"3.14"], null, null));
-            _cache.Add(new MemberKey(typeof(global::TypeWithExamples), MemberType.Property, "FloatType", null, []), new XmlComment(null, null, null, null, null, false, [@"3.14"], null, null));
-            _cache.Add(new MemberKey(typeof(global::TypeWithExamples), MemberType.Property, "DateTimeType", null, []), new XmlComment(null, null, null, null, null, false, [@"2022-01-01T00:00:00Z"], null, null));
-            _cache.Add(new MemberKey(typeof(global::TypeWithExamples), MemberType.Property, "DateOnlyType", null, []), new XmlComment(null, null, null, null, null, false, [@"2022-01-01"], null, null));
-            _cache.Add(new MemberKey(typeof(global::TypeWithExamples), MemberType.Property, "StringType", null, []), new XmlComment(null, null, null, null, null, false, [@"Hello, World!"], null, null));
-            _cache.Add(new MemberKey(typeof(global::TypeWithExamples), MemberType.Property, "GuidType", null, []), new XmlComment(null, null, null, null, null, false, [@"2d8f1eac-b5c6-4e29-8c62-4d9d75ef3d3d"], null, null));
-            _cache.Add(new MemberKey(typeof(global::TypeWithExamples), MemberType.Property, "TimeOnlyType", null, []), new XmlComment(null, null, null, null, null, false, [@"12:30:45"], null, null));
-            _cache.Add(new MemberKey(typeof(global::TypeWithExamples), MemberType.Property, "TimeSpanType", null, []), new XmlComment(null, null, null, null, null, false, [@"P3DT4H5M"], null, null));
-            _cache.Add(new MemberKey(typeof(global::TypeWithExamples), MemberType.Property, "ByteType", null, []), new XmlComment(null, null, null, null, null, false, [@"255"], null, null));
-            _cache.Add(new MemberKey(typeof(global::TypeWithExamples), MemberType.Property, "DecimalType", null, []), new XmlComment(null, null, null, null, null, false, [@"3.14159265359"], null, null));
-            _cache.Add(new MemberKey(typeof(global::TypeWithExamples), MemberType.Property, "UriType", null, []), new XmlComment(null, null, null, null, null, false, [@"https://example.com"], null, null));
-            _cache.Add(new MemberKey(typeof(global::IUser), MemberType.Property, "Id", null, []), new XmlComment(@"The unique identifier for the user.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::IUser), MemberType.Property, "Name", null, []), new XmlComment(@"The user's display name.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::User), MemberType.Property, "Id", null, []), new XmlComment(@"The unique identifier for the user.", null, null, null, null, false, null, null, null));
-            _cache.Add(new MemberKey(typeof(global::User), MemberType.Property, "Name", null, []), new XmlComment(@"The user's display name.", null, null, null, null, false, null, null, null));
-
-            return _cache;
-        }
+                if (includeGenericArguments && !type.IsGenericTypeDefinition)
+                {
+                    var typeArgs = type.GetGenericArguments();
+                    sb.Append('{');
 
-        internal static bool TryGetXmlComment(Type? type, MethodInfo? methodInfo, [NotNullWhen(true)] out XmlComment? xmlComment)
-        {
-            if (methodInfo is null)
-            {
-                return Cache.TryGetValue(new MemberKey(type, MemberType.Property, null, null, null), out xmlComment);
-            }
+                    for (int i = 0; i < typeArgs.Length; i++)
+                    {
+                        if (i > 0)
+                        {
+                            sb.Append(',');
+                        }
 
-            return Cache.TryGetValue(MemberKey.FromMethodInfo(methodInfo), out xmlComment);
-        }
+                        sb.Append(GetTypeDocId(typeArgs[i], includeGenericArguments, omitGenericArity));
+                    }
 
-        internal static bool TryGetXmlComment(Type? type, string? memberName, [NotNullWhen(true)] out XmlComment? xmlComment)
-        {
-            return Cache.TryGetValue(new MemberKey(type, memberName is null ? MemberType.Type : MemberType.Property, memberName, null, null), out xmlComment);
+                    sb.Append('}');
+                }
+
+                return sb.ToString();
+            }
+
+            // For non-generic types, use FullName (if available) and replace nested type separators.
+            return (type.FullName ?? type.Name).Replace('+', '.');
         }
     }
 
@@ -249,7 +331,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
             {
                 return Task.CompletedTask;
             }
-            if (XmlCommentCache.TryGetXmlComment(methodInfo.DeclaringType, methodInfo, out var methodComment))
+            if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
             {
                 if (methodComment.Summary is { } summary)
                 {
@@ -322,7 +404,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
         {
             if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
             {
-                if (XmlCommentCache.TryGetXmlComment(propertyInfo.DeclaringType, propertyInfo.Name, out var propertyComment))
+                if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
                 {
                     schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
                     if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -331,7 +413,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
                     }
                 }
             }
-            if (XmlCommentCache.TryGetXmlComment(context.JsonTypeInfo.Type, (string?)null, out var typeComment))
+            if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
             {
                 schema.Description = typeComment.Summary;
                 if (typeComment.Examples?.FirstOrDefault() is { } jsonString)

+ 3 - 0
src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddFileTests.cs

@@ -3,6 +3,7 @@
 
 using System.Text.RegularExpressions;
 using System.Xml;
+using Microsoft.AspNetCore.InternalTesting;
 using Microsoft.DotNet.OpenApi.Tests;
 using Xunit.Abstractions;
 
@@ -143,6 +144,7 @@ public class OpenApiAddFileTests : OpenApiTestBase
     }
 
     [Fact]
+    [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/61225")]
     public async Task OpenApi_Add_FromJson()
     {
         var project = CreateBasicProject(withOpenApi: true);
@@ -183,6 +185,7 @@ public class OpenApiAddFileTests : OpenApiTestBase
     }
 
     [Fact]
+    [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/61225")]
     public async Task OpenApi_Add_MultipleTimes_OnlyOneReference()
     {
         var project = CreateBasicProject(withOpenApi: true);

+ 2 - 0
src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.InternalTesting;
 using Microsoft.DotNet.OpenApi.Tests;
 using Xunit.Abstractions;
 
@@ -12,6 +13,7 @@ public class OpenApiAddURLTests : OpenApiTestBase
     public OpenApiAddURLTests(ITestOutputHelper output) : base(output) { }
 
     [Fact]
+    [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/61225")]
     public async Task OpenApi_Add_Url_WithContentDisposition()
     {
         var project = CreateBasicProject(withOpenApi: false);