From 4d97c9b4caf77be209266c5ae710472ba9085caa Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 12 Jul 2025 00:40:48 +0000
Subject: [PATCH 1/6] Initial plan
From 7215cca5d7da8663c05edd3cb40fadea17ec6b82 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 12 Jul 2025 00:52:25 +0000
Subject: [PATCH 2/6] Implement NormalizeDocId fix for XML comment generator
Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com>
---
src/OpenApi/gen/XmlCommentGenerator.Parser.cs | 34 +-
.../XmlCommentDocumentationIdTests.cs | 90 +++
...ApiXmlCommentSupport.generated.received.cs | 592 ++++++++++++++++++
...ApiXmlCommentSupport.generated.received.cs | 475 ++++++++++++++
...ApiXmlCommentSupport.generated.received.cs | 493 +++++++++++++++
...ApiXmlCommentSupport.generated.received.cs | 472 ++++++++++++++
6 files changed, 2152 insertions(+), 4 deletions(-)
create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/XmlCommentDocumentationIdTests.cs
create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs
create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.received.cs
create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.received.cs
create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.received.cs
diff --git a/src/OpenApi/gen/XmlCommentGenerator.Parser.cs b/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
index 0463486167df..f86c9407733b 100644
--- a/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
+++ b/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
@@ -14,6 +14,32 @@ namespace Microsoft.AspNetCore.OpenApi.SourceGenerators;
public sealed partial class XmlCommentGenerator
{
+ ///
+ /// Normalizes a documentation comment ID to match the compiler-style format.
+ /// Strips the return type suffix for ordinary methods but retains it for conversion operators.
+ ///
+ /// The documentation comment ID to normalize.
+ /// The normalized documentation comment ID.
+ internal static string NormalizeDocId(string docId)
+ {
+ // Find the tilde character that indicates the return type suffix
+ var tildeIndex = docId.IndexOf('~');
+ if (tildeIndex == -1)
+ {
+ // No return type suffix, return as-is
+ return docId;
+ }
+
+ // Check if this is a conversion operator (op_Implicit or op_Explicit)
+ // For these operators, we need to keep the return type suffix
+ if (docId.Contains("op_Implicit") || docId.Contains("op_Explicit"))
+ {
+ return docId;
+ }
+
+ // For ordinary methods, strip the return type suffix
+ return docId.Substring(0, tildeIndex);
+ }
internal static List<(string, string)> ParseXmlFile(AdditionalText additionalText, CancellationToken cancellationToken)
{
var text = additionalText.GetText(cancellationToken);
@@ -37,7 +63,7 @@ public sealed partial class XmlCommentGenerator
var name = member.Attribute(DocumentationCommentXmlNames.NameAttributeName)?.Value;
if (name is not null)
{
- comments.Add((name, member.ToString()));
+ comments.Add((NormalizeDocId(name), member.ToString()));
}
}
return comments;
@@ -54,7 +80,7 @@ public sealed partial class XmlCommentGenerator
if (DocumentationCommentId.CreateDeclarationId(type) is string name &&
type.GetDocumentationCommentXml(CultureInfo.InvariantCulture, expandIncludes: true, cancellationToken: cancellationToken) is string xml)
{
- comments.Add((name, xml));
+ comments.Add((NormalizeDocId(name), xml));
}
}
var properties = visitor.GetPublicProperties();
@@ -63,7 +89,7 @@ public sealed partial class XmlCommentGenerator
if (DocumentationCommentId.CreateDeclarationId(property) is string name &&
property.GetDocumentationCommentXml(CultureInfo.InvariantCulture, expandIncludes: true, cancellationToken: cancellationToken) is string xml)
{
- comments.Add((name, xml));
+ comments.Add((NormalizeDocId(name), xml));
}
}
var methods = visitor.GetPublicMethods();
@@ -77,7 +103,7 @@ public sealed partial class XmlCommentGenerator
if (DocumentationCommentId.CreateDeclarationId(method) is string name &&
method.GetDocumentationCommentXml(CultureInfo.InvariantCulture, expandIncludes: true, cancellationToken: cancellationToken) is string xml)
{
- comments.Add((name, xml));
+ comments.Add((NormalizeDocId(name), xml));
}
}
return comments;
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/XmlCommentDocumentationIdTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/XmlCommentDocumentationIdTests.cs
new file mode 100644
index 000000000000..fbf42c1e5092
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/XmlCommentDocumentationIdTests.cs
@@ -0,0 +1,90 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Net.Http;
+using System.Text.Json.Nodes;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests;
+
+[UsesVerify]
+public class XmlCommentDocumentationIdTests
+{
+ [Fact]
+ public async Task CanMergeXmlCommentsWithDifferentDocumentationIdFormats()
+ {
+ // This test verifies that XML comments from referenced assemblies (without return type suffix)
+ // are properly merged with in-memory symbols (with return type suffix)
+ var source = """
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using ReferencedLibrary;
+
+var builder = WebApplication.CreateBuilder();
+
+builder.Services.AddOpenApi();
+
+var app = builder.Build();
+
+app.MapPost("/test-method", ReferencedLibrary.TestApi.TestMethod);
+
+app.Run();
+""";
+
+ var referencedLibrarySource = """
+using System;
+using System.Threading.Tasks;
+
+namespace ReferencedLibrary;
+
+public static class TestApi
+{
+ ///
+ /// This method should have its XML comment merged properly.
+ ///
+ /// The identifier for the test.
+ /// A task representing the asynchronous operation.
+ public static Task TestMethod(int id)
+ {
+ return Task.CompletedTask;
+ }
+}
+""";
+
+ var references = new Dictionary>
+ {
+ { "ReferencedLibrary", [referencedLibrarySource] }
+ };
+
+ var generator = new XmlCommentGenerator();
+ await SnapshotTestHelper.Verify(source, generator, references, out var compilation, out var additionalAssemblies);
+ await SnapshotTestHelper.VerifyOpenApi(compilation, additionalAssemblies, document =>
+ {
+ var path = document.Paths["/test-method"].Operations[HttpMethod.Post];
+
+ // Verify that the XML comment from the referenced library was properly merged
+ // This would fail before the fix because the documentation IDs didn't match
+ Assert.NotNull(path.Summary);
+ Assert.Equal("This method should have its XML comment merged properly.", path.Summary);
+
+ // Verify the parameter comment is also available
+ Assert.NotNull(path.Parameters);
+ Assert.Single(path.Parameters);
+ Assert.Equal("The identifier for the test.", path.Parameters[0].Description);
+ });
+ }
+
+ [Theory]
+ [InlineData("M:Sample.MyMethod(System.Int32)~System.Threading.Tasks.Task", "M:Sample.MyMethod(System.Int32)")]
+ [InlineData("M:Sample.MyMethod(System.Int32)", "M:Sample.MyMethod(System.Int32)")]
+ [InlineData("M:Sample.op_Implicit(System.Int32)~Sample.MyClass", "M:Sample.op_Implicit(System.Int32)~Sample.MyClass")]
+ [InlineData("M:Sample.op_Explicit(System.Int32)~Sample.MyClass", "M:Sample.op_Explicit(System.Int32)~Sample.MyClass")]
+ [InlineData("T:Sample.MyClass", "T:Sample.MyClass")]
+ [InlineData("P:Sample.MyClass.MyProperty", "P:Sample.MyClass.MyProperty")]
+ public void NormalizeDocId_ReturnsExpectedResult(string input, string expected)
+ {
+ var result = XmlCommentGenerator.NormalizeDocId(input);
+ Assert.Equal(expected, result);
+ }
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs
new file mode 100644
index 000000000000..cfd4045037dd
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs
@@ -0,0 +1,592 @@
+//HintName: OpenApiXmlCommentSupport.generated.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+#nullable enable
+// Suppress warnings about obsolete types and members
+// in generated code
+#pragma warning disable CS0612, CS0618
+
+namespace System.Runtime.CompilerServices
+{
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ file sealed class InterceptsLocationAttribute : System.Attribute
+ {
+ public InterceptsLocationAttribute(int version, string data)
+ {
+ }
+ }
+}
+
+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;
+ using System.Threading.Tasks;
+ using Microsoft.AspNetCore.OpenApi;
+ using Microsoft.AspNetCore.Mvc.Controllers;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.OpenApi;
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file record XmlComment(
+ string? Summary,
+ string? Description,
+ string? Remarks,
+ string? Returns,
+ string? Value,
+ bool Deprecated,
+ List? Examples,
+ List? Parameters,
+ List? Responses);
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file record XmlParameterComment(string? Name, string? Description, string? Example, bool Deprecated);
+
+ [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 static class XmlCommentCache
+ {
+ private static Dictionary? _cache;
+ public static Dictionary Cache => _cache ??= GenerateCacheEntries();
+
+ private static Dictionary GenerateCacheEntries()
+ {
+ var cache = new Dictionary();
+
+ 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#
+ elements to show how you would add documentation
+ comments for most elements in a typical class.
+ The remarks can add multiple paragraphs, so you can
+write detailed information for developers that use
+your work. You should add everything needed for
+readers to be successful. This class contains
+examples for the following:
+ * Summary
+
+This should provide a one sentence summary of the class or member.
+* Remarks
+
+This is typically a more detailed description of the class or member
+* para
+
+The para tag separates a section into multiple paragraphs
+* list
+
+Provides a list of terms or elements
+* returns, param
+
+Used to describe parameters and return values
+* value
+Used to describe properties
+* exception
+
+Used to describe exceptions that may be thrown
+* c, cref, see, seealso
+
+These provide code style and links to other
+documentation elements
+* example, code
+
+These are used for code examples
+ The list above uses the ""table"" style. You could
+also use the ""bullet"" or ""number"" style. Neither
+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(@"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(@"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(@"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(@"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(@"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(@"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(@"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<T>
+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(@"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(@"T:DisposableType", new XmlComment(@"A class that implements the IDisposable interface.", null, null, null, null, false, null, null, null));
+ cache.Add(@"P:ExampleClass.Label", new XmlComment(null, null, @" The string? ExampleClass.Label is a `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(@"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)", 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 > 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(@"M:ExampleClass.AddAsync(System.Int32,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(@"M:ExampleClass.DoNothingAsync", 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(@"M:ExampleClass.AddNumbers(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(@"M:ITestInterface.Method(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(@"M:InheritOnlyReturns.MyParentMethod(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", new XmlComment(null, null, null, @"A boolean", null, false, null, null, null));
+ cache.Add(@"M:InheritAllButRemarks.MyParentMethod(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(@"M:InheritAllButRemarks.MyChildMethod", 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", new XmlComment(@"This method returns a generic type containing a tuple.", null, null, null, null, false, null, null, null));
+ cache.Add(@"M:GenericParent.GetTupleOfTask", 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", 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", 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", 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", 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)", new XmlComment(@"The GetGenericValue method.", null, @"This sample shows how to specify the T ParamsAndParamRefs.GetGenericValue<T>(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));
+ cache.Add(@"M:DisposableType.Dispose", new XmlComment(null, null, null, null, null, false, null, null, null));
+
+ return cache;
+ }
+ }
+
+ file static class DocumentationCommentIdHelper
+ {
+ ///
+ /// Generates a documentation comment ID for a type.
+ /// Example: T:Namespace.Outer+Inner`1 becomes T:Namespace.Outer.Inner`1
+ ///
+ public static string CreateDocumentationId(this Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException(nameof(type));
+ }
+
+ return "T:" + GetTypeDocId(type, includeGenericArguments: false, omitGenericArity: false);
+ }
+
+ ///
+ /// Generates a documentation comment ID for a property.
+ /// Example: P:Namespace.ContainingType.PropertyName or for an indexer P:Namespace.ContainingType.Item(System.Int32)
+ ///
+ public static string CreateDocumentationId(this PropertyInfo property)
+ {
+ 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();
+ }
+
+ ///
+ /// Generates a documentation comment ID for a method (or constructor).
+ /// For example:
+ /// M:Namespace.ContainingType.MethodName(ParamType1,ParamType2)~ReturnType
+ /// M:Namespace.ContainingType.#ctor(ParamType)
+ ///
+ 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)
+ {
+ 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(',');
+ }
+
+ // 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();
+ }
+
+ ///
+ /// 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 flag controls whether
+ /// constructed generic type arguments are emitted, while
+ /// controls whether the generic arity marker (e.g. "`1") is appended.
+ ///
+ private static string GetTypeDocId(Type type, bool includeGenericArguments, bool omitGenericArity)
+ {
+ 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('+', '.');
+ }
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file class XmlCommentOperationTransformer : IOpenApiOperationTransformer
+ {
+ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
+ {
+ var methodInfo = context.Description.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
+ ? controllerActionDescriptor.MethodInfo
+ : context.Description.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
+
+ if (methodInfo is null)
+ {
+ return Task.CompletedTask;
+ }
+ if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
+ {
+ if (methodComment.Summary is { } summary)
+ {
+ operation.Summary = summary;
+ }
+ if (methodComment.Description is { } description)
+ {
+ operation.Description = description;
+ }
+ if (methodComment.Remarks is { } remarks)
+ {
+ operation.Description = remarks;
+ }
+ if (methodComment.Parameters is { Count: > 0})
+ {
+ foreach (var parameterComment in methodComment.Parameters)
+ {
+ var parameterInfo = methodInfo.GetParameters().SingleOrDefault(info => info.Name == parameterComment.Name);
+ var operationParameter = operation.Parameters?.SingleOrDefault(parameter => parameter.Name == parameterComment.Name);
+ if (operationParameter is not null)
+ {
+ var targetOperationParameter = UnwrapOpenApiParameter(operationParameter);
+ targetOperationParameter.Description = parameterComment.Description;
+ if (parameterComment.Example is { } jsonString)
+ {
+ targetOperationParameter.Example = jsonString.Parse();
+ }
+ targetOperationParameter.Deprecated = parameterComment.Deprecated;
+ }
+ else
+ {
+ var requestBody = operation.RequestBody;
+ if (requestBody is not null)
+ {
+ requestBody.Description = parameterComment.Description;
+ if (parameterComment.Example is { } jsonString)
+ {
+ var content = requestBody?.Content?.Values;
+ if (content is null)
+ {
+ continue;
+ }
+ foreach (var mediaType in content)
+ {
+ mediaType.Example = jsonString.Parse();
+ }
+ }
+ }
+ }
+ }
+ }
+ // Applies `` on XML comments for operation with single response value.
+ if (methodComment.Returns is { } returns && operation.Responses is { Count: 1 })
+ {
+ var response = operation.Responses.First();
+ response.Value.Description = returns;
+ }
+ // Applies `` on XML comments for operation with multiple response values.
+ if (methodComment.Responses is { Count: > 0} && operation.Responses is { Count: > 0 })
+ {
+ foreach (var response in operation.Responses)
+ {
+ var responseComment = methodComment.Responses.SingleOrDefault(xmlResponse => xmlResponse.Code == response.Key);
+ if (responseComment is not null)
+ {
+ response.Value.Description = responseComment.Description;
+ }
+ }
+ }
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceParameter)
+ {
+ if (sourceParameter is OpenApiParameterReference parameterReference)
+ {
+ if (parameterReference.Target is OpenApiParameter target)
+ {
+ return target;
+ }
+ else
+ {
+ throw new InvalidOperationException($"The input schema must be an {nameof(OpenApiParameter)} or {nameof(OpenApiParameterReference)}.");
+ }
+ }
+ else if (sourceParameter is OpenApiParameter directParameter)
+ {
+ return directParameter;
+ }
+ else
+ {
+ throw new InvalidOperationException($"The input schema must be an {nameof(OpenApiParameter)} or {nameof(OpenApiParameterReference)}.");
+ }
+ }
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file class XmlCommentSchemaTransformer : IOpenApiSchemaTransformer
+ {
+ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
+ {
+ if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ {
+ if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
+ {
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ }
+ if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
+ {
+ schema.Description = typeComment.Summary;
+ if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ return Task.CompletedTask;
+ }
+ }
+
+ file static class JsonNodeExtensions
+ {
+ public static JsonNode? Parse(this string? json)
+ {
+ if (json is null)
+ {
+ return null;
+ }
+
+ try
+ {
+ return JsonNode.Parse(json);
+ }
+ catch (JsonException)
+ {
+ try
+ {
+ // If parsing fails, try wrapping in quotes to make it a valid JSON string
+ return JsonNode.Parse($"\"{json.Replace("\"", "\\\"")}\"");
+ }
+ catch (JsonException)
+ {
+ return null;
+ }
+ }
+ }
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file static class GeneratedServiceCollectionExtensions
+ {
+ [InterceptsLocation]
+ public static IServiceCollection AddOpenApi(this IServiceCollection services)
+ {
+ return services.AddOpenApi("v1", options =>
+ {
+ options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+ options.AddOperationTransformer(new XmlCommentOperationTransformer());
+ });
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.received.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.received.cs
new file mode 100644
index 000000000000..448667404712
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.received.cs
@@ -0,0 +1,475 @@
+//HintName: OpenApiXmlCommentSupport.generated.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+#nullable enable
+// Suppress warnings about obsolete types and members
+// in generated code
+#pragma warning disable CS0612, CS0618
+
+namespace System.Runtime.CompilerServices
+{
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ file sealed class InterceptsLocationAttribute : System.Attribute
+ {
+ public InterceptsLocationAttribute(int version, string data)
+ {
+ }
+ }
+}
+
+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;
+ using System.Threading.Tasks;
+ using Microsoft.AspNetCore.OpenApi;
+ using Microsoft.AspNetCore.Mvc.Controllers;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.OpenApi;
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file record XmlComment(
+ string? Summary,
+ string? Description,
+ string? Remarks,
+ string? Returns,
+ string? Value,
+ bool Deprecated,
+ List? Examples,
+ List? Parameters,
+ List? Responses);
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file record XmlParameterComment(string? Name, string? Description, string? Example, bool Deprecated);
+
+ [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 static class XmlCommentCache
+ {
+ private static Dictionary? _cache;
+ public static Dictionary Cache => _cache ??= GenerateCacheEntries();
+
+ private static Dictionary GenerateCacheEntries()
+ {
+ var cache = new Dictionary();
+
+ cache.Add(@"M:TestController.Get", 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)", 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)", 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)", new XmlComment(null, null, null, null, null, false, null, [new XmlParameterComment(@"todo", @"The todo to insert into the database.", null, false)], null));
+
+ return cache;
+ }
+ }
+
+ file static class DocumentationCommentIdHelper
+ {
+ ///
+ /// Generates a documentation comment ID for a type.
+ /// Example: T:Namespace.Outer+Inner`1 becomes T:Namespace.Outer.Inner`1
+ ///
+ public static string CreateDocumentationId(this Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException(nameof(type));
+ }
+
+ return "T:" + GetTypeDocId(type, includeGenericArguments: false, omitGenericArity: false);
+ }
+
+ ///
+ /// Generates a documentation comment ID for a property.
+ /// Example: P:Namespace.ContainingType.PropertyName or for an indexer P:Namespace.ContainingType.Item(System.Int32)
+ ///
+ public static string CreateDocumentationId(this PropertyInfo property)
+ {
+ 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();
+ }
+
+ ///
+ /// Generates a documentation comment ID for a method (or constructor).
+ /// For example:
+ /// M:Namespace.ContainingType.MethodName(ParamType1,ParamType2)~ReturnType
+ /// M:Namespace.ContainingType.#ctor(ParamType)
+ ///
+ 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)
+ {
+ 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(',');
+ }
+
+ // 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();
+ }
+
+ ///
+ /// 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 flag controls whether
+ /// constructed generic type arguments are emitted, while
+ /// controls whether the generic arity marker (e.g. "`1") is appended.
+ ///
+ private static string GetTypeDocId(Type type, bool includeGenericArguments, bool omitGenericArity)
+ {
+ 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('+', '.');
+ }
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file class XmlCommentOperationTransformer : IOpenApiOperationTransformer
+ {
+ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
+ {
+ var methodInfo = context.Description.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
+ ? controllerActionDescriptor.MethodInfo
+ : context.Description.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
+
+ if (methodInfo is null)
+ {
+ return Task.CompletedTask;
+ }
+ if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
+ {
+ if (methodComment.Summary is { } summary)
+ {
+ operation.Summary = summary;
+ }
+ if (methodComment.Description is { } description)
+ {
+ operation.Description = description;
+ }
+ if (methodComment.Remarks is { } remarks)
+ {
+ operation.Description = remarks;
+ }
+ if (methodComment.Parameters is { Count: > 0})
+ {
+ foreach (var parameterComment in methodComment.Parameters)
+ {
+ var parameterInfo = methodInfo.GetParameters().SingleOrDefault(info => info.Name == parameterComment.Name);
+ var operationParameter = operation.Parameters?.SingleOrDefault(parameter => parameter.Name == parameterComment.Name);
+ if (operationParameter is not null)
+ {
+ var targetOperationParameter = UnwrapOpenApiParameter(operationParameter);
+ targetOperationParameter.Description = parameterComment.Description;
+ if (parameterComment.Example is { } jsonString)
+ {
+ targetOperationParameter.Example = jsonString.Parse();
+ }
+ targetOperationParameter.Deprecated = parameterComment.Deprecated;
+ }
+ else
+ {
+ var requestBody = operation.RequestBody;
+ if (requestBody is not null)
+ {
+ requestBody.Description = parameterComment.Description;
+ if (parameterComment.Example is { } jsonString)
+ {
+ var content = requestBody?.Content?.Values;
+ if (content is null)
+ {
+ continue;
+ }
+ foreach (var mediaType in content)
+ {
+ mediaType.Example = jsonString.Parse();
+ }
+ }
+ }
+ }
+ }
+ }
+ // Applies `` on XML comments for operation with single response value.
+ if (methodComment.Returns is { } returns && operation.Responses is { Count: 1 })
+ {
+ var response = operation.Responses.First();
+ response.Value.Description = returns;
+ }
+ // Applies `` on XML comments for operation with multiple response values.
+ if (methodComment.Responses is { Count: > 0} && operation.Responses is { Count: > 0 })
+ {
+ foreach (var response in operation.Responses)
+ {
+ var responseComment = methodComment.Responses.SingleOrDefault(xmlResponse => xmlResponse.Code == response.Key);
+ if (responseComment is not null)
+ {
+ response.Value.Description = responseComment.Description;
+ }
+ }
+ }
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceParameter)
+ {
+ if (sourceParameter is OpenApiParameterReference parameterReference)
+ {
+ if (parameterReference.Target is OpenApiParameter target)
+ {
+ return target;
+ }
+ else
+ {
+ throw new InvalidOperationException($"The input schema must be an {nameof(OpenApiParameter)} or {nameof(OpenApiParameterReference)}.");
+ }
+ }
+ else if (sourceParameter is OpenApiParameter directParameter)
+ {
+ return directParameter;
+ }
+ else
+ {
+ throw new InvalidOperationException($"The input schema must be an {nameof(OpenApiParameter)} or {nameof(OpenApiParameterReference)}.");
+ }
+ }
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file class XmlCommentSchemaTransformer : IOpenApiSchemaTransformer
+ {
+ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
+ {
+ if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ {
+ if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
+ {
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ }
+ if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
+ {
+ schema.Description = typeComment.Summary;
+ if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ return Task.CompletedTask;
+ }
+ }
+
+ file static class JsonNodeExtensions
+ {
+ public static JsonNode? Parse(this string? json)
+ {
+ if (json is null)
+ {
+ return null;
+ }
+
+ try
+ {
+ return JsonNode.Parse(json);
+ }
+ catch (JsonException)
+ {
+ try
+ {
+ // If parsing fails, try wrapping in quotes to make it a valid JSON string
+ return JsonNode.Parse($"\"{json.Replace("\"", "\\\"")}\"");
+ }
+ catch (JsonException)
+ {
+ return null;
+ }
+ }
+ }
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file static class GeneratedServiceCollectionExtensions
+ {
+ [InterceptsLocation]
+ public static IServiceCollection AddOpenApi(this IServiceCollection services)
+ {
+ return services.AddOpenApi("v1", options =>
+ {
+ options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+ options.AddOperationTransformer(new XmlCommentOperationTransformer());
+ });
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.received.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.received.cs
new file mode 100644
index 000000000000..c5c44ed109bc
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.received.cs
@@ -0,0 +1,493 @@
+//HintName: OpenApiXmlCommentSupport.generated.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+#nullable enable
+// Suppress warnings about obsolete types and members
+// in generated code
+#pragma warning disable CS0612, CS0618
+
+namespace System.Runtime.CompilerServices
+{
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ file sealed class InterceptsLocationAttribute : System.Attribute
+ {
+ public InterceptsLocationAttribute(int version, string data)
+ {
+ }
+ }
+}
+
+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;
+ using System.Threading.Tasks;
+ using Microsoft.AspNetCore.OpenApi;
+ using Microsoft.AspNetCore.Mvc.Controllers;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.OpenApi;
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file record XmlComment(
+ string? Summary,
+ string? Description,
+ string? Remarks,
+ string? Returns,
+ string? Value,
+ bool Deprecated,
+ List? Examples,
+ List? Parameters,
+ List? Responses);
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file record XmlParameterComment(string? Name, string? Description, string? Example, bool Deprecated);
+
+ [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 static class XmlCommentCache
+ {
+ private static Dictionary? _cache;
+ public static Dictionary Cache => _cache ??= GenerateCacheEntries();
+
+ private static Dictionary GenerateCacheEntries()
+ {
+ var cache = new Dictionary();
+
+ cache.Add(@"M:RouteHandlerExtensionMethods.Get", new XmlComment(@"A summary of the action.", @"A description of the action.", null, @"Returns the greeting.", null, false, null, null, null));
+ cache.Add(@"M:RouteHandlerExtensionMethods.Get2(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)", new XmlComment(null, null, null, @"Returns the greeting.", null, false, null, [new XmlParameterComment(@"name", @"The name of the person.", @"Testy McTester", false)], null));
+ cache.Add(@"M:RouteHandlerExtensionMethods.Get4", new XmlComment(null, null, null, @"Indicates that the value was not found.", null, false, null, null, null));
+ cache.Add(@"M:RouteHandlerExtensionMethods.Get5", new XmlComment(null, null, null, @"This gets ignored.", 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)", new XmlComment(@"Creates a new user.", null, @"Sample request:
+ POST /6
+ {
+ ""username"": ""johndoe"",
+ ""email"": ""john@example.com""
+ }", null, null, false, null, [new XmlParameterComment(@"user", @"The user information.", @"{""username"": ""johndoe"", ""email"": ""john@example.com""}", 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)", 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", new XmlComment(@"A summary of Get8.", null, null, null, null, false, null, null, null));
+ cache.Add(@"M:RouteHandlerExtensionMethods.Get9", new XmlComment(@"A summary of Get9.", null, null, null, null, false, null, null, null));
+ cache.Add(@"M:RouteHandlerExtensionMethods.Get10", new XmlComment(@"A summary of Get10.", null, null, null, null, false, null, null, null));
+ cache.Add(@"M:RouteHandlerExtensionMethods.Get11", new XmlComment(@"A summary of Get11.", null, null, null, null, false, null, null, null));
+ cache.Add(@"M:RouteHandlerExtensionMethods.Get12", new XmlComment(@"A summary of Get12.", null, null, null, null, false, null, null, null));
+ cache.Add(@"M:RouteHandlerExtensionMethods.Get13", new XmlComment(@"A summary of Get13.", null, null, null, null, false, null, null, null));
+ cache.Add(@"M:RouteHandlerExtensionMethods.Get14", new XmlComment(@"A summary of Get14.", null, null, @"Returns the greeting.", null, false, null, null, null));
+ cache.Add(@"M:RouteHandlerExtensionMethods.Get15", new XmlComment(@"A summary of Get15.", null, null, null, null, false, null, null, [new XmlResponseComment(@"200", @"Returns the greeting.", @"")]));
+ 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[])", new XmlComment(@"A summary of Get17.", null, null, null, null, false, null, null, null));
+
+ return cache;
+ }
+ }
+
+ file static class DocumentationCommentIdHelper
+ {
+ ///
+ /// Generates a documentation comment ID for a type.
+ /// Example: T:Namespace.Outer+Inner`1 becomes T:Namespace.Outer.Inner`1
+ ///
+ public static string CreateDocumentationId(this Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException(nameof(type));
+ }
+
+ return "T:" + GetTypeDocId(type, includeGenericArguments: false, omitGenericArity: false);
+ }
+
+ ///
+ /// Generates a documentation comment ID for a property.
+ /// Example: P:Namespace.ContainingType.PropertyName or for an indexer P:Namespace.ContainingType.Item(System.Int32)
+ ///
+ public static string CreateDocumentationId(this PropertyInfo property)
+ {
+ 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();
+ }
+
+ ///
+ /// Generates a documentation comment ID for a method (or constructor).
+ /// For example:
+ /// M:Namespace.ContainingType.MethodName(ParamType1,ParamType2)~ReturnType
+ /// M:Namespace.ContainingType.#ctor(ParamType)
+ ///
+ 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)
+ {
+ 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(',');
+ }
+
+ // 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();
+ }
+
+ ///
+ /// 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 flag controls whether
+ /// constructed generic type arguments are emitted, while
+ /// controls whether the generic arity marker (e.g. "`1") is appended.
+ ///
+ private static string GetTypeDocId(Type type, bool includeGenericArguments, bool omitGenericArity)
+ {
+ 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('+', '.');
+ }
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file class XmlCommentOperationTransformer : IOpenApiOperationTransformer
+ {
+ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
+ {
+ var methodInfo = context.Description.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
+ ? controllerActionDescriptor.MethodInfo
+ : context.Description.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
+
+ if (methodInfo is null)
+ {
+ return Task.CompletedTask;
+ }
+ if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
+ {
+ if (methodComment.Summary is { } summary)
+ {
+ operation.Summary = summary;
+ }
+ if (methodComment.Description is { } description)
+ {
+ operation.Description = description;
+ }
+ if (methodComment.Remarks is { } remarks)
+ {
+ operation.Description = remarks;
+ }
+ if (methodComment.Parameters is { Count: > 0})
+ {
+ foreach (var parameterComment in methodComment.Parameters)
+ {
+ var parameterInfo = methodInfo.GetParameters().SingleOrDefault(info => info.Name == parameterComment.Name);
+ var operationParameter = operation.Parameters?.SingleOrDefault(parameter => parameter.Name == parameterComment.Name);
+ if (operationParameter is not null)
+ {
+ var targetOperationParameter = UnwrapOpenApiParameter(operationParameter);
+ targetOperationParameter.Description = parameterComment.Description;
+ if (parameterComment.Example is { } jsonString)
+ {
+ targetOperationParameter.Example = jsonString.Parse();
+ }
+ targetOperationParameter.Deprecated = parameterComment.Deprecated;
+ }
+ else
+ {
+ var requestBody = operation.RequestBody;
+ if (requestBody is not null)
+ {
+ requestBody.Description = parameterComment.Description;
+ if (parameterComment.Example is { } jsonString)
+ {
+ var content = requestBody?.Content?.Values;
+ if (content is null)
+ {
+ continue;
+ }
+ foreach (var mediaType in content)
+ {
+ mediaType.Example = jsonString.Parse();
+ }
+ }
+ }
+ }
+ }
+ }
+ // Applies `` on XML comments for operation with single response value.
+ if (methodComment.Returns is { } returns && operation.Responses is { Count: 1 })
+ {
+ var response = operation.Responses.First();
+ response.Value.Description = returns;
+ }
+ // Applies `` on XML comments for operation with multiple response values.
+ if (methodComment.Responses is { Count: > 0} && operation.Responses is { Count: > 0 })
+ {
+ foreach (var response in operation.Responses)
+ {
+ var responseComment = methodComment.Responses.SingleOrDefault(xmlResponse => xmlResponse.Code == response.Key);
+ if (responseComment is not null)
+ {
+ response.Value.Description = responseComment.Description;
+ }
+ }
+ }
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceParameter)
+ {
+ if (sourceParameter is OpenApiParameterReference parameterReference)
+ {
+ if (parameterReference.Target is OpenApiParameter target)
+ {
+ return target;
+ }
+ else
+ {
+ throw new InvalidOperationException($"The input schema must be an {nameof(OpenApiParameter)} or {nameof(OpenApiParameterReference)}.");
+ }
+ }
+ else if (sourceParameter is OpenApiParameter directParameter)
+ {
+ return directParameter;
+ }
+ else
+ {
+ throw new InvalidOperationException($"The input schema must be an {nameof(OpenApiParameter)} or {nameof(OpenApiParameterReference)}.");
+ }
+ }
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file class XmlCommentSchemaTransformer : IOpenApiSchemaTransformer
+ {
+ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
+ {
+ if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ {
+ if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
+ {
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ }
+ if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
+ {
+ schema.Description = typeComment.Summary;
+ if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ return Task.CompletedTask;
+ }
+ }
+
+ file static class JsonNodeExtensions
+ {
+ public static JsonNode? Parse(this string? json)
+ {
+ if (json is null)
+ {
+ return null;
+ }
+
+ try
+ {
+ return JsonNode.Parse(json);
+ }
+ catch (JsonException)
+ {
+ try
+ {
+ // If parsing fails, try wrapping in quotes to make it a valid JSON string
+ return JsonNode.Parse($"\"{json.Replace("\"", "\\\"")}\"");
+ }
+ catch (JsonException)
+ {
+ return null;
+ }
+ }
+ }
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file static class GeneratedServiceCollectionExtensions
+ {
+ [InterceptsLocation]
+ public static IServiceCollection AddOpenApi(this IServiceCollection services)
+ {
+ return services.AddOpenApi("v1", options =>
+ {
+ options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+ options.AddOperationTransformer(new XmlCommentOperationTransformer());
+ });
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.received.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.received.cs
new file mode 100644
index 000000000000..f4843a66a73f
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.received.cs
@@ -0,0 +1,472 @@
+//HintName: OpenApiXmlCommentSupport.generated.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+#nullable enable
+// Suppress warnings about obsolete types and members
+// in generated code
+#pragma warning disable CS0612, CS0618
+
+namespace System.Runtime.CompilerServices
+{
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ file sealed class InterceptsLocationAttribute : System.Attribute
+ {
+ public InterceptsLocationAttribute(int version, string data)
+ {
+ }
+ }
+}
+
+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;
+ using System.Threading.Tasks;
+ using Microsoft.AspNetCore.OpenApi;
+ using Microsoft.AspNetCore.Mvc.Controllers;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.OpenApi;
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file record XmlComment(
+ string? Summary,
+ string? Description,
+ string? Remarks,
+ string? Returns,
+ string? Value,
+ bool Deprecated,
+ List? Examples,
+ List? Parameters,
+ List? Responses);
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file record XmlParameterComment(string? Name, string? Description, string? Example, bool Deprecated);
+
+ [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 static class XmlCommentCache
+ {
+ private static Dictionary? _cache;
+ public static Dictionary Cache => _cache ??= GenerateCacheEntries();
+
+ private static Dictionary GenerateCacheEntries()
+ {
+ var cache = new Dictionary();
+ cache.Add(@"M:ReferencedLibrary.TestApi.TestMethod(System.Int32)", new XmlComment(@"This method should have its XML comment merged properly.", null, null, @"A task representing the asynchronous operation.", null, false, null, [new XmlParameterComment(@"id", @"The identifier for the test.", null, false)], null));
+
+
+ return cache;
+ }
+ }
+
+ file static class DocumentationCommentIdHelper
+ {
+ ///
+ /// Generates a documentation comment ID for a type.
+ /// Example: T:Namespace.Outer+Inner`1 becomes T:Namespace.Outer.Inner`1
+ ///
+ public static string CreateDocumentationId(this Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException(nameof(type));
+ }
+
+ return "T:" + GetTypeDocId(type, includeGenericArguments: false, omitGenericArity: false);
+ }
+
+ ///
+ /// Generates a documentation comment ID for a property.
+ /// Example: P:Namespace.ContainingType.PropertyName or for an indexer P:Namespace.ContainingType.Item(System.Int32)
+ ///
+ public static string CreateDocumentationId(this PropertyInfo property)
+ {
+ 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();
+ }
+
+ ///
+ /// Generates a documentation comment ID for a method (or constructor).
+ /// For example:
+ /// M:Namespace.ContainingType.MethodName(ParamType1,ParamType2)~ReturnType
+ /// M:Namespace.ContainingType.#ctor(ParamType)
+ ///
+ 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)
+ {
+ 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(',');
+ }
+
+ // 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();
+ }
+
+ ///
+ /// 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 flag controls whether
+ /// constructed generic type arguments are emitted, while
+ /// controls whether the generic arity marker (e.g. "`1") is appended.
+ ///
+ private static string GetTypeDocId(Type type, bool includeGenericArguments, bool omitGenericArity)
+ {
+ 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('+', '.');
+ }
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file class XmlCommentOperationTransformer : IOpenApiOperationTransformer
+ {
+ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
+ {
+ var methodInfo = context.Description.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
+ ? controllerActionDescriptor.MethodInfo
+ : context.Description.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
+
+ if (methodInfo is null)
+ {
+ return Task.CompletedTask;
+ }
+ if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
+ {
+ if (methodComment.Summary is { } summary)
+ {
+ operation.Summary = summary;
+ }
+ if (methodComment.Description is { } description)
+ {
+ operation.Description = description;
+ }
+ if (methodComment.Remarks is { } remarks)
+ {
+ operation.Description = remarks;
+ }
+ if (methodComment.Parameters is { Count: > 0})
+ {
+ foreach (var parameterComment in methodComment.Parameters)
+ {
+ var parameterInfo = methodInfo.GetParameters().SingleOrDefault(info => info.Name == parameterComment.Name);
+ var operationParameter = operation.Parameters?.SingleOrDefault(parameter => parameter.Name == parameterComment.Name);
+ if (operationParameter is not null)
+ {
+ var targetOperationParameter = UnwrapOpenApiParameter(operationParameter);
+ targetOperationParameter.Description = parameterComment.Description;
+ if (parameterComment.Example is { } jsonString)
+ {
+ targetOperationParameter.Example = jsonString.Parse();
+ }
+ targetOperationParameter.Deprecated = parameterComment.Deprecated;
+ }
+ else
+ {
+ var requestBody = operation.RequestBody;
+ if (requestBody is not null)
+ {
+ requestBody.Description = parameterComment.Description;
+ if (parameterComment.Example is { } jsonString)
+ {
+ var content = requestBody?.Content?.Values;
+ if (content is null)
+ {
+ continue;
+ }
+ foreach (var mediaType in content)
+ {
+ mediaType.Example = jsonString.Parse();
+ }
+ }
+ }
+ }
+ }
+ }
+ // Applies `` on XML comments for operation with single response value.
+ if (methodComment.Returns is { } returns && operation.Responses is { Count: 1 })
+ {
+ var response = operation.Responses.First();
+ response.Value.Description = returns;
+ }
+ // Applies `` on XML comments for operation with multiple response values.
+ if (methodComment.Responses is { Count: > 0} && operation.Responses is { Count: > 0 })
+ {
+ foreach (var response in operation.Responses)
+ {
+ var responseComment = methodComment.Responses.SingleOrDefault(xmlResponse => xmlResponse.Code == response.Key);
+ if (responseComment is not null)
+ {
+ response.Value.Description = responseComment.Description;
+ }
+ }
+ }
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceParameter)
+ {
+ if (sourceParameter is OpenApiParameterReference parameterReference)
+ {
+ if (parameterReference.Target is OpenApiParameter target)
+ {
+ return target;
+ }
+ else
+ {
+ throw new InvalidOperationException($"The input schema must be an {nameof(OpenApiParameter)} or {nameof(OpenApiParameterReference)}.");
+ }
+ }
+ else if (sourceParameter is OpenApiParameter directParameter)
+ {
+ return directParameter;
+ }
+ else
+ {
+ throw new InvalidOperationException($"The input schema must be an {nameof(OpenApiParameter)} or {nameof(OpenApiParameterReference)}.");
+ }
+ }
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file class XmlCommentSchemaTransformer : IOpenApiSchemaTransformer
+ {
+ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
+ {
+ if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ {
+ if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
+ {
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ }
+ if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
+ {
+ schema.Description = typeComment.Summary;
+ if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ return Task.CompletedTask;
+ }
+ }
+
+ file static class JsonNodeExtensions
+ {
+ public static JsonNode? Parse(this string? json)
+ {
+ if (json is null)
+ {
+ return null;
+ }
+
+ try
+ {
+ return JsonNode.Parse(json);
+ }
+ catch (JsonException)
+ {
+ try
+ {
+ // If parsing fails, try wrapping in quotes to make it a valid JSON string
+ return JsonNode.Parse($"\"{json.Replace("\"", "\\\"")}\"");
+ }
+ catch (JsonException)
+ {
+ return null;
+ }
+ }
+ }
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file static class GeneratedServiceCollectionExtensions
+ {
+ [InterceptsLocation]
+ public static IServiceCollection AddOpenApi(this IServiceCollection services)
+ {
+ return services.AddOpenApi("v1", options =>
+ {
+ options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+ options.AddOperationTransformer(new XmlCommentOperationTransformer());
+ });
+ }
+
+ }
+}
\ No newline at end of file
From 2d427913d804a249a2bd5380c0fc18751a505caa Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 12 Jul 2025 01:01:07 +0000
Subject: [PATCH 3/6] Fix runtime lookup to use normalized documentation IDs
Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com>
---
.../gen/XmlCommentGenerator.Emitter.cs | 33 +-
...piXmlCommentSupport.generated.received.cs} | 33 +-
...piXmlCommentSupport.generated.received.cs} | 33 +-
...ApiXmlCommentSupport.generated.received.cs | 33 +-
...ApiXmlCommentSupport.generated.verified.cs | 592 ------------------
...ApiXmlCommentSupport.generated.received.cs | 33 +-
...ApiXmlCommentSupport.generated.verified.cs | 475 --------------
...ApiXmlCommentSupport.generated.received.cs | 33 +-
...ApiXmlCommentSupport.generated.verified.cs | 493 ---------------
...piXmlCommentSupport.generated.received.cs} | 33 +-
...piXmlCommentSupport.generated.verified.cs} | 33 +-
11 files changed, 240 insertions(+), 1584 deletions(-)
rename src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/{AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs => AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.received.cs} (92%)
rename src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/{AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.verified.cs => AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.received.cs} (93%)
delete mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
delete mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs
delete mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs
rename src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/{SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs => SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs} (93%)
rename src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/{XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.received.cs => XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.verified.cs} (92%)
diff --git a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
index 7e27ffb79b95..6f6d16d41461 100644
--- a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
+++ b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
@@ -302,6 +302,33 @@ private static string GetTypeDocId(Type type, bool includeGenericArguments, bool
// For non-generic types, use FullName (if available) and replace nested type separators.
return (type.FullName ?? type.Name).Replace('+', '.');
}
+
+ ///
+ /// Normalizes a documentation comment ID to match the compiler-style format.
+ /// Strips the return type suffix for ordinary methods but retains it for conversion operators.
+ ///
+ /// The documentation comment ID to normalize.
+ /// The normalized documentation comment ID.
+ public static string NormalizeDocId(string docId)
+ {
+ // Find the tilde character that indicates the return type suffix
+ var tildeIndex = docId.IndexOf('~');
+ if (tildeIndex == -1)
+ {
+ // No return type suffix, return as-is
+ return docId;
+ }
+
+ // Check if this is a conversion operator (op_Implicit or op_Explicit)
+ // For these operators, we need to keep the return type suffix
+ if (docId.Contains("op_Implicit") || docId.Contains("op_Explicit"))
+ {
+ return docId;
+ }
+
+ // For ordinary methods, strip the return type suffix
+ return docId.Substring(0, tildeIndex);
+ }
}
{{GeneratedCodeAttribute}}
@@ -317,7 +344,7 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
{
return Task.CompletedTask;
}
- if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(methodInfo.CreateDocumentationId()), out var methodComment))
{
if (methodComment.Summary is { } summary)
{
@@ -423,7 +450,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
{
if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
- if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -432,7 +459,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
}
}
}
- if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.received.cs
similarity index 92%
rename from src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs
rename to src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.received.cs
index 0f7dc96817c8..f13f5ffc8c83 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.received.cs
@@ -284,6 +284,33 @@ private static string GetTypeDocId(Type type, bool includeGenericArguments, bool
// For non-generic types, use FullName (if available) and replace nested type separators.
return (type.FullName ?? type.Name).Replace('+', '.');
}
+
+ ///
+ /// Normalizes a documentation comment ID to match the compiler-style format.
+ /// Strips the return type suffix for ordinary methods but retains it for conversion operators.
+ ///
+ /// The documentation comment ID to normalize.
+ /// The normalized documentation comment ID.
+ public static string NormalizeDocId(string docId)
+ {
+ // Find the tilde character that indicates the return type suffix
+ var tildeIndex = docId.IndexOf('~');
+ if (tildeIndex == -1)
+ {
+ // No return type suffix, return as-is
+ return docId;
+ }
+
+ // Check if this is a conversion operator (op_Implicit or op_Explicit)
+ // For these operators, we need to keep the return type suffix
+ if (docId.Contains("op_Implicit") || docId.Contains("op_Explicit"))
+ {
+ return docId;
+ }
+
+ // For ordinary methods, strip the return type suffix
+ return docId.Substring(0, tildeIndex);
+ }
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
@@ -299,7 +326,7 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
{
return Task.CompletedTask;
}
- if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(methodInfo.CreateDocumentationId()), out var methodComment))
{
if (methodComment.Summary is { } summary)
{
@@ -405,7 +432,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
{
if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
- if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -414,7 +441,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
}
}
}
- if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.received.cs
similarity index 93%
rename from src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.verified.cs
rename to src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.received.cs
index 10b09abc3d01..ed5d669b673a 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.verified.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.received.cs
@@ -313,6 +313,33 @@ private static string GetTypeDocId(Type type, bool includeGenericArguments, bool
// For non-generic types, use FullName (if available) and replace nested type separators.
return (type.FullName ?? type.Name).Replace('+', '.');
}
+
+ ///
+ /// Normalizes a documentation comment ID to match the compiler-style format.
+ /// Strips the return type suffix for ordinary methods but retains it for conversion operators.
+ ///
+ /// The documentation comment ID to normalize.
+ /// The normalized documentation comment ID.
+ public static string NormalizeDocId(string docId)
+ {
+ // Find the tilde character that indicates the return type suffix
+ var tildeIndex = docId.IndexOf('~');
+ if (tildeIndex == -1)
+ {
+ // No return type suffix, return as-is
+ return docId;
+ }
+
+ // Check if this is a conversion operator (op_Implicit or op_Explicit)
+ // For these operators, we need to keep the return type suffix
+ if (docId.Contains("op_Implicit") || docId.Contains("op_Explicit"))
+ {
+ return docId;
+ }
+
+ // For ordinary methods, strip the return type suffix
+ return docId.Substring(0, tildeIndex);
+ }
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
@@ -328,7 +355,7 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
{
return Task.CompletedTask;
}
- if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(methodInfo.CreateDocumentationId()), out var methodComment))
{
if (methodComment.Summary is { } summary)
{
@@ -434,7 +461,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
{
if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
- if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -443,7 +470,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
}
}
}
- if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs
index cfd4045037dd..a70de5ef5d3f 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs
@@ -405,6 +405,33 @@ private static string GetTypeDocId(Type type, bool includeGenericArguments, bool
// For non-generic types, use FullName (if available) and replace nested type separators.
return (type.FullName ?? type.Name).Replace('+', '.');
}
+
+ ///
+ /// Normalizes a documentation comment ID to match the compiler-style format.
+ /// Strips the return type suffix for ordinary methods but retains it for conversion operators.
+ ///
+ /// The documentation comment ID to normalize.
+ /// The normalized documentation comment ID.
+ public static string NormalizeDocId(string docId)
+ {
+ // Find the tilde character that indicates the return type suffix
+ var tildeIndex = docId.IndexOf('~');
+ if (tildeIndex == -1)
+ {
+ // No return type suffix, return as-is
+ return docId;
+ }
+
+ // Check if this is a conversion operator (op_Implicit or op_Explicit)
+ // For these operators, we need to keep the return type suffix
+ if (docId.Contains("op_Implicit") || docId.Contains("op_Explicit"))
+ {
+ return docId;
+ }
+
+ // For ordinary methods, strip the return type suffix
+ return docId.Substring(0, tildeIndex);
+ }
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
@@ -420,7 +447,7 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
{
return Task.CompletedTask;
}
- if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(methodInfo.CreateDocumentationId()), out var methodComment))
{
if (methodComment.Summary is { } summary)
{
@@ -526,7 +553,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
{
if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
- if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -535,7 +562,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
}
}
}
- if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
deleted file mode 100644
index 4e6a566bb894..000000000000
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
+++ /dev/null
@@ -1,592 +0,0 @@
-//HintName: OpenApiXmlCommentSupport.generated.cs
-//------------------------------------------------------------------------------
-//
-// This code was generated by a tool.
-//
-// Changes to this file may cause incorrect behavior and will be lost if
-// the code is regenerated.
-//
-//------------------------------------------------------------------------------
-#nullable enable
-// Suppress warnings about obsolete types and members
-// in generated code
-#pragma warning disable CS0612, CS0618
-
-namespace System.Runtime.CompilerServices
-{
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
- file sealed class InterceptsLocationAttribute : System.Attribute
- {
- public InterceptsLocationAttribute(int version, string data)
- {
- }
- }
-}
-
-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;
- using System.Threading.Tasks;
- using Microsoft.AspNetCore.OpenApi;
- using Microsoft.AspNetCore.Mvc.Controllers;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.OpenApi;
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- file record XmlComment(
- string? Summary,
- string? Description,
- string? Remarks,
- string? Returns,
- string? Value,
- bool Deprecated,
- List? Examples,
- List? Parameters,
- List? Responses);
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- file record XmlParameterComment(string? Name, string? Description, string? Example, bool Deprecated);
-
- [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 static class XmlCommentCache
- {
- private static Dictionary? _cache;
- public static Dictionary Cache => _cache ??= GenerateCacheEntries();
-
- private static Dictionary GenerateCacheEntries()
- {
- var cache = new Dictionary();
-
- 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#
- elements to show how you would add documentation
- comments for most elements in a typical class.
- The remarks can add multiple paragraphs, so you can
-write detailed information for developers that use
-your work. You should add everything needed for
-readers to be successful. This class contains
-examples for the following:
- * Summary
-
-This should provide a one sentence summary of the class or member.
-* Remarks
-
-This is typically a more detailed description of the class or member
-* para
-
-The para tag separates a section into multiple paragraphs
-* list
-
-Provides a list of terms or elements
-* returns, param
-
-Used to describe parameters and return values
-* value
-Used to describe properties
-* exception
-
-Used to describe exceptions that may be thrown
-* c, cref, see, seealso
-
-These provide code style and links to other
-documentation elements
-* example, code
-
-These are used for code examples
- The list above uses the ""table"" style. You could
-also use the ""bullet"" or ""number"" style. Neither
-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(@"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(@"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(@"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(@"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(@"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(@"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(@"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<T>
-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(@"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(@"T:DisposableType", new XmlComment(@"A class that implements the IDisposable interface.", null, null, null, null, false, null, null, null));
- cache.Add(@"P:ExampleClass.Label", new XmlComment(null, null, @" The string? ExampleClass.Label is a `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(@"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 > 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(@"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(@"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(@"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(@"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(@"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(@"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<T>(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));
- cache.Add(@"M:DisposableType.Dispose", new XmlComment(null, null, null, null, null, false, null, null, null));
-
- return cache;
- }
- }
-
- file static class DocumentationCommentIdHelper
- {
- ///
- /// Generates a documentation comment ID for a type.
- /// Example: T:Namespace.Outer+Inner`1 becomes T:Namespace.Outer.Inner`1
- ///
- public static string CreateDocumentationId(this Type type)
- {
- if (type == null)
- {
- throw new ArgumentNullException(nameof(type));
- }
-
- return "T:" + GetTypeDocId(type, includeGenericArguments: false, omitGenericArity: false);
- }
-
- ///
- /// Generates a documentation comment ID for a property.
- /// Example: P:Namespace.ContainingType.PropertyName or for an indexer P:Namespace.ContainingType.Item(System.Int32)
- ///
- public static string CreateDocumentationId(this PropertyInfo property)
- {
- 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();
- }
-
- ///
- /// Generates a documentation comment ID for a method (or constructor).
- /// For example:
- /// M:Namespace.ContainingType.MethodName(ParamType1,ParamType2)~ReturnType
- /// M:Namespace.ContainingType.#ctor(ParamType)
- ///
- 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)
- {
- 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(',');
- }
-
- // 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();
- }
-
- ///
- /// 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 flag controls whether
- /// constructed generic type arguments are emitted, while
- /// controls whether the generic arity marker (e.g. "`1") is appended.
- ///
- private static string GetTypeDocId(Type type, bool includeGenericArguments, bool omitGenericArity)
- {
- 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('+', '.');
- }
- }
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- file class XmlCommentOperationTransformer : IOpenApiOperationTransformer
- {
- public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
- {
- var methodInfo = context.Description.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
- ? controllerActionDescriptor.MethodInfo
- : context.Description.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
-
- if (methodInfo is null)
- {
- return Task.CompletedTask;
- }
- if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
- {
- if (methodComment.Summary is { } summary)
- {
- operation.Summary = summary;
- }
- if (methodComment.Description is { } description)
- {
- operation.Description = description;
- }
- if (methodComment.Remarks is { } remarks)
- {
- operation.Description = remarks;
- }
- if (methodComment.Parameters is { Count: > 0})
- {
- foreach (var parameterComment in methodComment.Parameters)
- {
- var parameterInfo = methodInfo.GetParameters().SingleOrDefault(info => info.Name == parameterComment.Name);
- var operationParameter = operation.Parameters?.SingleOrDefault(parameter => parameter.Name == parameterComment.Name);
- if (operationParameter is not null)
- {
- var targetOperationParameter = UnwrapOpenApiParameter(operationParameter);
- targetOperationParameter.Description = parameterComment.Description;
- if (parameterComment.Example is { } jsonString)
- {
- targetOperationParameter.Example = jsonString.Parse();
- }
- targetOperationParameter.Deprecated = parameterComment.Deprecated;
- }
- else
- {
- var requestBody = operation.RequestBody;
- if (requestBody is not null)
- {
- requestBody.Description = parameterComment.Description;
- if (parameterComment.Example is { } jsonString)
- {
- var content = requestBody?.Content?.Values;
- if (content is null)
- {
- continue;
- }
- foreach (var mediaType in content)
- {
- mediaType.Example = jsonString.Parse();
- }
- }
- }
- }
- }
- }
- // Applies `` on XML comments for operation with single response value.
- if (methodComment.Returns is { } returns && operation.Responses is { Count: 1 })
- {
- var response = operation.Responses.First();
- response.Value.Description = returns;
- }
- // Applies `` on XML comments for operation with multiple response values.
- if (methodComment.Responses is { Count: > 0} && operation.Responses is { Count: > 0 })
- {
- foreach (var response in operation.Responses)
- {
- var responseComment = methodComment.Responses.SingleOrDefault(xmlResponse => xmlResponse.Code == response.Key);
- if (responseComment is not null)
- {
- response.Value.Description = responseComment.Description;
- }
- }
- }
- }
-
- return Task.CompletedTask;
- }
-
- private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceParameter)
- {
- if (sourceParameter is OpenApiParameterReference parameterReference)
- {
- if (parameterReference.Target is OpenApiParameter target)
- {
- return target;
- }
- else
- {
- throw new InvalidOperationException($"The input schema must be an {nameof(OpenApiParameter)} or {nameof(OpenApiParameterReference)}.");
- }
- }
- else if (sourceParameter is OpenApiParameter directParameter)
- {
- return directParameter;
- }
- else
- {
- throw new InvalidOperationException($"The input schema must be an {nameof(OpenApiParameter)} or {nameof(OpenApiParameterReference)}.");
- }
- }
- }
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- file class XmlCommentSchemaTransformer : IOpenApiSchemaTransformer
- {
- public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
- {
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
- {
- if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
- {
- schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
- if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
- {
- schema.Example = jsonString.Parse();
- }
- }
- }
- if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
- {
- schema.Description = typeComment.Summary;
- if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
- {
- schema.Example = jsonString.Parse();
- }
- }
- return Task.CompletedTask;
- }
- }
-
- file static class JsonNodeExtensions
- {
- public static JsonNode? Parse(this string? json)
- {
- if (json is null)
- {
- return null;
- }
-
- try
- {
- return JsonNode.Parse(json);
- }
- catch (JsonException)
- {
- try
- {
- // If parsing fails, try wrapping in quotes to make it a valid JSON string
- return JsonNode.Parse($"\"{json.Replace("\"", "\\\"")}\"");
- }
- catch (JsonException)
- {
- return null;
- }
- }
- }
- }
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- file static class GeneratedServiceCollectionExtensions
- {
- [InterceptsLocation]
- public static IServiceCollection AddOpenApi(this IServiceCollection services)
- {
- return services.AddOpenApi("v1", options =>
- {
- options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
- options.AddOperationTransformer(new XmlCommentOperationTransformer());
- });
- }
-
- }
-}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.received.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.received.cs
index 448667404712..f4da876e2123 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.received.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.received.cs
@@ -288,6 +288,33 @@ private static string GetTypeDocId(Type type, bool includeGenericArguments, bool
// For non-generic types, use FullName (if available) and replace nested type separators.
return (type.FullName ?? type.Name).Replace('+', '.');
}
+
+ ///
+ /// Normalizes a documentation comment ID to match the compiler-style format.
+ /// Strips the return type suffix for ordinary methods but retains it for conversion operators.
+ ///
+ /// The documentation comment ID to normalize.
+ /// The normalized documentation comment ID.
+ public static string NormalizeDocId(string docId)
+ {
+ // Find the tilde character that indicates the return type suffix
+ var tildeIndex = docId.IndexOf('~');
+ if (tildeIndex == -1)
+ {
+ // No return type suffix, return as-is
+ return docId;
+ }
+
+ // Check if this is a conversion operator (op_Implicit or op_Explicit)
+ // For these operators, we need to keep the return type suffix
+ if (docId.Contains("op_Implicit") || docId.Contains("op_Explicit"))
+ {
+ return docId;
+ }
+
+ // For ordinary methods, strip the return type suffix
+ return docId.Substring(0, tildeIndex);
+ }
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
@@ -303,7 +330,7 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
{
return Task.CompletedTask;
}
- if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(methodInfo.CreateDocumentationId()), out var methodComment))
{
if (methodComment.Summary is { } summary)
{
@@ -409,7 +436,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
{
if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
- if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -418,7 +445,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
}
}
}
- if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs
deleted file mode 100644
index 76e202554e42..000000000000
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs
+++ /dev/null
@@ -1,475 +0,0 @@
-//HintName: OpenApiXmlCommentSupport.generated.cs
-//------------------------------------------------------------------------------
-//
-// This code was generated by a tool.
-//
-// Changes to this file may cause incorrect behavior and will be lost if
-// the code is regenerated.
-//
-//------------------------------------------------------------------------------
-#nullable enable
-// Suppress warnings about obsolete types and members
-// in generated code
-#pragma warning disable CS0612, CS0618
-
-namespace System.Runtime.CompilerServices
-{
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
- file sealed class InterceptsLocationAttribute : System.Attribute
- {
- public InterceptsLocationAttribute(int version, string data)
- {
- }
- }
-}
-
-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;
- using System.Threading.Tasks;
- using Microsoft.AspNetCore.OpenApi;
- using Microsoft.AspNetCore.Mvc.Controllers;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.OpenApi;
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- file record XmlComment(
- string? Summary,
- string? Description,
- string? Remarks,
- string? Returns,
- string? Value,
- bool Deprecated,
- List? Examples,
- List? Parameters,
- List? Responses);
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- file record XmlParameterComment(string? Name, string? Description, string? Example, bool Deprecated);
-
- [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 static class XmlCommentCache
- {
- private static Dictionary? _cache;
- public static Dictionary Cache => _cache ??= GenerateCacheEntries();
-
- private static Dictionary GenerateCacheEntries()
- {
- var cache = new Dictionary();
-
- 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));
-
- return cache;
- }
- }
-
- file static class DocumentationCommentIdHelper
- {
- ///
- /// Generates a documentation comment ID for a type.
- /// Example: T:Namespace.Outer+Inner`1 becomes T:Namespace.Outer.Inner`1
- ///
- public static string CreateDocumentationId(this Type type)
- {
- if (type == null)
- {
- throw new ArgumentNullException(nameof(type));
- }
-
- return "T:" + GetTypeDocId(type, includeGenericArguments: false, omitGenericArity: false);
- }
-
- ///
- /// Generates a documentation comment ID for a property.
- /// Example: P:Namespace.ContainingType.PropertyName or for an indexer P:Namespace.ContainingType.Item(System.Int32)
- ///
- public static string CreateDocumentationId(this PropertyInfo property)
- {
- 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();
- }
-
- ///
- /// Generates a documentation comment ID for a method (or constructor).
- /// For example:
- /// M:Namespace.ContainingType.MethodName(ParamType1,ParamType2)~ReturnType
- /// M:Namespace.ContainingType.#ctor(ParamType)
- ///
- 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)
- {
- 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(',');
- }
-
- // 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();
- }
-
- ///
- /// 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 flag controls whether
- /// constructed generic type arguments are emitted, while
- /// controls whether the generic arity marker (e.g. "`1") is appended.
- ///
- private static string GetTypeDocId(Type type, bool includeGenericArguments, bool omitGenericArity)
- {
- 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('+', '.');
- }
- }
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- file class XmlCommentOperationTransformer : IOpenApiOperationTransformer
- {
- public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
- {
- var methodInfo = context.Description.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
- ? controllerActionDescriptor.MethodInfo
- : context.Description.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
-
- if (methodInfo is null)
- {
- return Task.CompletedTask;
- }
- if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
- {
- if (methodComment.Summary is { } summary)
- {
- operation.Summary = summary;
- }
- if (methodComment.Description is { } description)
- {
- operation.Description = description;
- }
- if (methodComment.Remarks is { } remarks)
- {
- operation.Description = remarks;
- }
- if (methodComment.Parameters is { Count: > 0})
- {
- foreach (var parameterComment in methodComment.Parameters)
- {
- var parameterInfo = methodInfo.GetParameters().SingleOrDefault(info => info.Name == parameterComment.Name);
- var operationParameter = operation.Parameters?.SingleOrDefault(parameter => parameter.Name == parameterComment.Name);
- if (operationParameter is not null)
- {
- var targetOperationParameter = UnwrapOpenApiParameter(operationParameter);
- targetOperationParameter.Description = parameterComment.Description;
- if (parameterComment.Example is { } jsonString)
- {
- targetOperationParameter.Example = jsonString.Parse();
- }
- targetOperationParameter.Deprecated = parameterComment.Deprecated;
- }
- else
- {
- var requestBody = operation.RequestBody;
- if (requestBody is not null)
- {
- requestBody.Description = parameterComment.Description;
- if (parameterComment.Example is { } jsonString)
- {
- var content = requestBody?.Content?.Values;
- if (content is null)
- {
- continue;
- }
- foreach (var mediaType in content)
- {
- mediaType.Example = jsonString.Parse();
- }
- }
- }
- }
- }
- }
- // Applies `` on XML comments for operation with single response value.
- if (methodComment.Returns is { } returns && operation.Responses is { Count: 1 })
- {
- var response = operation.Responses.First();
- response.Value.Description = returns;
- }
- // Applies `` on XML comments for operation with multiple response values.
- if (methodComment.Responses is { Count: > 0} && operation.Responses is { Count: > 0 })
- {
- foreach (var response in operation.Responses)
- {
- var responseComment = methodComment.Responses.SingleOrDefault(xmlResponse => xmlResponse.Code == response.Key);
- if (responseComment is not null)
- {
- response.Value.Description = responseComment.Description;
- }
- }
- }
- }
-
- return Task.CompletedTask;
- }
-
- private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceParameter)
- {
- if (sourceParameter is OpenApiParameterReference parameterReference)
- {
- if (parameterReference.Target is OpenApiParameter target)
- {
- return target;
- }
- else
- {
- throw new InvalidOperationException($"The input schema must be an {nameof(OpenApiParameter)} or {nameof(OpenApiParameterReference)}.");
- }
- }
- else if (sourceParameter is OpenApiParameter directParameter)
- {
- return directParameter;
- }
- else
- {
- throw new InvalidOperationException($"The input schema must be an {nameof(OpenApiParameter)} or {nameof(OpenApiParameterReference)}.");
- }
- }
- }
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- file class XmlCommentSchemaTransformer : IOpenApiSchemaTransformer
- {
- public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
- {
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
- {
- if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
- {
- schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
- if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
- {
- schema.Example = jsonString.Parse();
- }
- }
- }
- if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
- {
- schema.Description = typeComment.Summary;
- if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
- {
- schema.Example = jsonString.Parse();
- }
- }
- return Task.CompletedTask;
- }
- }
-
- file static class JsonNodeExtensions
- {
- public static JsonNode? Parse(this string? json)
- {
- if (json is null)
- {
- return null;
- }
-
- try
- {
- return JsonNode.Parse(json);
- }
- catch (JsonException)
- {
- try
- {
- // If parsing fails, try wrapping in quotes to make it a valid JSON string
- return JsonNode.Parse($"\"{json.Replace("\"", "\\\"")}\"");
- }
- catch (JsonException)
- {
- return null;
- }
- }
- }
- }
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- file static class GeneratedServiceCollectionExtensions
- {
- [InterceptsLocation]
- public static IServiceCollection AddOpenApi(this IServiceCollection services)
- {
- return services.AddOpenApi("v1", options =>
- {
- options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
- options.AddOperationTransformer(new XmlCommentOperationTransformer());
- });
- }
-
- }
-}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.received.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.received.cs
index c5c44ed109bc..4a2aa376b299 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.received.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.received.cs
@@ -306,6 +306,33 @@ private static string GetTypeDocId(Type type, bool includeGenericArguments, bool
// For non-generic types, use FullName (if available) and replace nested type separators.
return (type.FullName ?? type.Name).Replace('+', '.');
}
+
+ ///
+ /// Normalizes a documentation comment ID to match the compiler-style format.
+ /// Strips the return type suffix for ordinary methods but retains it for conversion operators.
+ ///
+ /// The documentation comment ID to normalize.
+ /// The normalized documentation comment ID.
+ public static string NormalizeDocId(string docId)
+ {
+ // Find the tilde character that indicates the return type suffix
+ var tildeIndex = docId.IndexOf('~');
+ if (tildeIndex == -1)
+ {
+ // No return type suffix, return as-is
+ return docId;
+ }
+
+ // Check if this is a conversion operator (op_Implicit or op_Explicit)
+ // For these operators, we need to keep the return type suffix
+ if (docId.Contains("op_Implicit") || docId.Contains("op_Explicit"))
+ {
+ return docId;
+ }
+
+ // For ordinary methods, strip the return type suffix
+ return docId.Substring(0, tildeIndex);
+ }
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
@@ -321,7 +348,7 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
{
return Task.CompletedTask;
}
- if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(methodInfo.CreateDocumentationId()), out var methodComment))
{
if (methodComment.Summary is { } summary)
{
@@ -427,7 +454,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
{
if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
- if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -436,7 +463,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
}
}
}
- if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs
deleted file mode 100644
index dbce7f0223bf..000000000000
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs
+++ /dev/null
@@ -1,493 +0,0 @@
-//HintName: OpenApiXmlCommentSupport.generated.cs
-//------------------------------------------------------------------------------
-//
-// This code was generated by a tool.
-//
-// Changes to this file may cause incorrect behavior and will be lost if
-// the code is regenerated.
-//
-//------------------------------------------------------------------------------
-#nullable enable
-// Suppress warnings about obsolete types and members
-// in generated code
-#pragma warning disable CS0612, CS0618
-
-namespace System.Runtime.CompilerServices
-{
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
- file sealed class InterceptsLocationAttribute : System.Attribute
- {
- public InterceptsLocationAttribute(int version, string data)
- {
- }
- }
-}
-
-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;
- using System.Threading.Tasks;
- using Microsoft.AspNetCore.OpenApi;
- using Microsoft.AspNetCore.Mvc.Controllers;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.OpenApi;
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- file record XmlComment(
- string? Summary,
- string? Description,
- string? Remarks,
- string? Returns,
- string? Value,
- bool Deprecated,
- List? Examples,
- List? Parameters,
- List? Responses);
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- file record XmlParameterComment(string? Name, string? Description, string? Example, bool Deprecated);
-
- [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 static class XmlCommentCache
- {
- private static Dictionary? _cache;
- public static Dictionary Cache => _cache ??= GenerateCacheEntries();
-
- private static Dictionary GenerateCacheEntries()
- {
- var cache = new Dictionary();
-
- cache.Add(@"M:RouteHandlerExtensionMethods.Get~System.String", new XmlComment(@"A summary of the action.", @"A description of the action.", null, @"Returns the greeting.", 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, @"Returns the greeting.", 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, @"Indicates that the value was not found.", null, false, null, null, null));
- 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, @"This gets ignored.", 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"": ""john@example.com""
- }", null, null, false, null, [new XmlParameterComment(@"user", @"The user information.", @"{""username"": ""johndoe"", ""email"": ""john@example.com""}", 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, @"Returns the greeting.", 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, [new XmlResponseComment(@"200", @"Returns the greeting.", @"")]));
- 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;
- }
- }
-
- file static class DocumentationCommentIdHelper
- {
- ///
- /// Generates a documentation comment ID for a type.
- /// Example: T:Namespace.Outer+Inner`1 becomes T:Namespace.Outer.Inner`1
- ///
- public static string CreateDocumentationId(this Type type)
- {
- if (type == null)
- {
- throw new ArgumentNullException(nameof(type));
- }
-
- return "T:" + GetTypeDocId(type, includeGenericArguments: false, omitGenericArity: false);
- }
-
- ///
- /// Generates a documentation comment ID for a property.
- /// Example: P:Namespace.ContainingType.PropertyName or for an indexer P:Namespace.ContainingType.Item(System.Int32)
- ///
- public static string CreateDocumentationId(this PropertyInfo property)
- {
- 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();
- }
-
- ///
- /// Generates a documentation comment ID for a method (or constructor).
- /// For example:
- /// M:Namespace.ContainingType.MethodName(ParamType1,ParamType2)~ReturnType
- /// M:Namespace.ContainingType.#ctor(ParamType)
- ///
- 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)
- {
- 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(',');
- }
-
- // 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();
- }
-
- ///
- /// 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 flag controls whether
- /// constructed generic type arguments are emitted, while
- /// controls whether the generic arity marker (e.g. "`1") is appended.
- ///
- private static string GetTypeDocId(Type type, bool includeGenericArguments, bool omitGenericArity)
- {
- 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('+', '.');
- }
- }
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- file class XmlCommentOperationTransformer : IOpenApiOperationTransformer
- {
- public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
- {
- var methodInfo = context.Description.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
- ? controllerActionDescriptor.MethodInfo
- : context.Description.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
-
- if (methodInfo is null)
- {
- return Task.CompletedTask;
- }
- if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
- {
- if (methodComment.Summary is { } summary)
- {
- operation.Summary = summary;
- }
- if (methodComment.Description is { } description)
- {
- operation.Description = description;
- }
- if (methodComment.Remarks is { } remarks)
- {
- operation.Description = remarks;
- }
- if (methodComment.Parameters is { Count: > 0})
- {
- foreach (var parameterComment in methodComment.Parameters)
- {
- var parameterInfo = methodInfo.GetParameters().SingleOrDefault(info => info.Name == parameterComment.Name);
- var operationParameter = operation.Parameters?.SingleOrDefault(parameter => parameter.Name == parameterComment.Name);
- if (operationParameter is not null)
- {
- var targetOperationParameter = UnwrapOpenApiParameter(operationParameter);
- targetOperationParameter.Description = parameterComment.Description;
- if (parameterComment.Example is { } jsonString)
- {
- targetOperationParameter.Example = jsonString.Parse();
- }
- targetOperationParameter.Deprecated = parameterComment.Deprecated;
- }
- else
- {
- var requestBody = operation.RequestBody;
- if (requestBody is not null)
- {
- requestBody.Description = parameterComment.Description;
- if (parameterComment.Example is { } jsonString)
- {
- var content = requestBody?.Content?.Values;
- if (content is null)
- {
- continue;
- }
- foreach (var mediaType in content)
- {
- mediaType.Example = jsonString.Parse();
- }
- }
- }
- }
- }
- }
- // Applies `` on XML comments for operation with single response value.
- if (methodComment.Returns is { } returns && operation.Responses is { Count: 1 })
- {
- var response = operation.Responses.First();
- response.Value.Description = returns;
- }
- // Applies `` on XML comments for operation with multiple response values.
- if (methodComment.Responses is { Count: > 0} && operation.Responses is { Count: > 0 })
- {
- foreach (var response in operation.Responses)
- {
- var responseComment = methodComment.Responses.SingleOrDefault(xmlResponse => xmlResponse.Code == response.Key);
- if (responseComment is not null)
- {
- response.Value.Description = responseComment.Description;
- }
- }
- }
- }
-
- return Task.CompletedTask;
- }
-
- private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceParameter)
- {
- if (sourceParameter is OpenApiParameterReference parameterReference)
- {
- if (parameterReference.Target is OpenApiParameter target)
- {
- return target;
- }
- else
- {
- throw new InvalidOperationException($"The input schema must be an {nameof(OpenApiParameter)} or {nameof(OpenApiParameterReference)}.");
- }
- }
- else if (sourceParameter is OpenApiParameter directParameter)
- {
- return directParameter;
- }
- else
- {
- throw new InvalidOperationException($"The input schema must be an {nameof(OpenApiParameter)} or {nameof(OpenApiParameterReference)}.");
- }
- }
- }
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- file class XmlCommentSchemaTransformer : IOpenApiSchemaTransformer
- {
- public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
- {
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
- {
- if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
- {
- schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
- if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
- {
- schema.Example = jsonString.Parse();
- }
- }
- }
- if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
- {
- schema.Description = typeComment.Summary;
- if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
- {
- schema.Example = jsonString.Parse();
- }
- }
- return Task.CompletedTask;
- }
- }
-
- file static class JsonNodeExtensions
- {
- public static JsonNode? Parse(this string? json)
- {
- if (json is null)
- {
- return null;
- }
-
- try
- {
- return JsonNode.Parse(json);
- }
- catch (JsonException)
- {
- try
- {
- // If parsing fails, try wrapping in quotes to make it a valid JSON string
- return JsonNode.Parse($"\"{json.Replace("\"", "\\\"")}\"");
- }
- catch (JsonException)
- {
- return null;
- }
- }
- }
- }
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
- file static class GeneratedServiceCollectionExtensions
- {
- [InterceptsLocation]
- public static IServiceCollection AddOpenApi(this IServiceCollection services)
- {
- return services.AddOpenApi("v1", options =>
- {
- options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
- options.AddOperationTransformer(new XmlCommentOperationTransformer());
- });
- }
-
- }
-}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs
similarity index 93%
rename from src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
rename to src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs
index 915b30278e70..92cc5419f37c 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs
@@ -314,6 +314,33 @@ private static string GetTypeDocId(Type type, bool includeGenericArguments, bool
// For non-generic types, use FullName (if available) and replace nested type separators.
return (type.FullName ?? type.Name).Replace('+', '.');
}
+
+ ///
+ /// Normalizes a documentation comment ID to match the compiler-style format.
+ /// Strips the return type suffix for ordinary methods but retains it for conversion operators.
+ ///
+ /// The documentation comment ID to normalize.
+ /// The normalized documentation comment ID.
+ public static string NormalizeDocId(string docId)
+ {
+ // Find the tilde character that indicates the return type suffix
+ var tildeIndex = docId.IndexOf('~');
+ if (tildeIndex == -1)
+ {
+ // No return type suffix, return as-is
+ return docId;
+ }
+
+ // Check if this is a conversion operator (op_Implicit or op_Explicit)
+ // For these operators, we need to keep the return type suffix
+ if (docId.Contains("op_Implicit") || docId.Contains("op_Explicit"))
+ {
+ return docId;
+ }
+
+ // For ordinary methods, strip the return type suffix
+ return docId.Substring(0, tildeIndex);
+ }
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
@@ -329,7 +356,7 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
{
return Task.CompletedTask;
}
- if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(methodInfo.CreateDocumentationId()), out var methodComment))
{
if (methodComment.Summary is { } summary)
{
@@ -435,7 +462,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
{
if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
- if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -444,7 +471,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
}
}
}
- if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.received.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.verified.cs
similarity index 92%
rename from src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.received.cs
rename to src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.verified.cs
index f4843a66a73f..5c3dcf382281 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.received.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.verified.cs
@@ -285,6 +285,33 @@ private static string GetTypeDocId(Type type, bool includeGenericArguments, bool
// For non-generic types, use FullName (if available) and replace nested type separators.
return (type.FullName ?? type.Name).Replace('+', '.');
}
+
+ ///
+ /// Normalizes a documentation comment ID to match the compiler-style format.
+ /// Strips the return type suffix for ordinary methods but retains it for conversion operators.
+ ///
+ /// The documentation comment ID to normalize.
+ /// The normalized documentation comment ID.
+ public static string NormalizeDocId(string docId)
+ {
+ // Find the tilde character that indicates the return type suffix
+ var tildeIndex = docId.IndexOf('~');
+ if (tildeIndex == -1)
+ {
+ // No return type suffix, return as-is
+ return docId;
+ }
+
+ // Check if this is a conversion operator (op_Implicit or op_Explicit)
+ // For these operators, we need to keep the return type suffix
+ if (docId.Contains("op_Implicit") || docId.Contains("op_Explicit"))
+ {
+ return docId;
+ }
+
+ // For ordinary methods, strip the return type suffix
+ return docId.Substring(0, tildeIndex);
+ }
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
@@ -300,7 +327,7 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
{
return Task.CompletedTask;
}
- if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(methodInfo.CreateDocumentationId()), out var methodComment))
{
if (methodComment.Summary is { } summary)
{
@@ -406,7 +433,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
{
if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
- if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -415,7 +442,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
}
}
}
- if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
From a6c924790acfc023e8238e52253df460e03726da Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 12 Jul 2025 01:02:12 +0000
Subject: [PATCH 4/6] Update snapshots to reflect normalized documentation IDs
Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com>
---
...ceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs} | 0
...dditionalTexts#OpenApiXmlCommentSupport.generated.verified.cs} | 0
...lTagsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs} | 0
...romControllers#OpenApiXmlCommentSupport.generated.verified.cs} | 0
...romMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs} | 0
...mentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs} | 0
6 files changed, 0 insertions(+), 0 deletions(-)
rename src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/{AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.received.cs => AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs} (100%)
rename src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/{AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.received.cs => AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.verified.cs} (100%)
rename src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/{CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs => CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs} (100%)
rename src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/{OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.received.cs => OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs} (100%)
rename src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/{OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.received.cs => OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs} (100%)
rename src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/{SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs => SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs} (100%)
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.received.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs
similarity index 100%
rename from src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.received.cs
rename to src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AddOpenApiTests.CanInterceptAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.received.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.verified.cs
similarity index 100%
rename from src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.received.cs
rename to src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.verified.cs
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
similarity index 100%
rename from src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs
rename to src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.received.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs
similarity index 100%
rename from src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.received.cs
rename to src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.received.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs
similarity index 100%
rename from src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.received.cs
rename to src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
similarity index 100%
rename from src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.received.cs
rename to src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
From 22760db281b232eb541096da22b173acb3b4dae1 Mon Sep 17 00:00:00 2001
From: Safia Abdalla
Date: Mon, 14 Jul 2025 10:50:35 -0700
Subject: [PATCH 5/6] Update src/OpenApi/gen/XmlCommentGenerator.Parser.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/OpenApi/gen/XmlCommentGenerator.Parser.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/OpenApi/gen/XmlCommentGenerator.Parser.cs b/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
index f86c9407733b..7cfd00ad7ae7 100644
--- a/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
+++ b/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
@@ -32,7 +32,7 @@ internal static string NormalizeDocId(string docId)
// Check if this is a conversion operator (op_Implicit or op_Explicit)
// For these operators, we need to keep the return type suffix
- if (docId.Contains("op_Implicit") || docId.Contains("op_Explicit"))
+ if (docId.Contains("op_Implicit", StringComparison.Ordinal) || docId.Contains("op_Explicit", StringComparison.Ordinal))
{
return docId;
}
From 01943b50b39ec6aaead04e3a9e9cdffaa750000d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 14 Jul 2025 19:19:34 +0000
Subject: [PATCH 6/6] Fix missing using System directive for StringComparison
Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com>
---
src/OpenApi/gen/XmlCommentGenerator.Parser.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/OpenApi/gen/XmlCommentGenerator.Parser.cs b/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
index 7cfd00ad7ae7..ba8248194d20 100644
--- a/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
+++ b/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
@@ -1,6 +1,7 @@
// 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.Globalization;
using System.Threading;