diff --git a/src/OpenApi/gen/XmlCommentGenerator.Parser.cs b/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
index fad7e9318e45..6aa1e9aa35d8 100644
--- a/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
+++ b/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
@@ -98,7 +98,7 @@ public sealed partial class XmlCommentGenerator
{
var memberKey = symbol switch
{
- IMethodSymbol methodSymbol => MemberKey.FromMethodSymbol(methodSymbol),
+ IMethodSymbol methodSymbol => MemberKey.FromMethodSymbol(methodSymbol, input.Compilation),
IPropertySymbol propertySymbol => MemberKey.FromPropertySymbol(propertySymbol),
INamedTypeSymbol typeSymbol => MemberKey.FromTypeSymbol(typeSymbol),
_ => null
diff --git a/src/OpenApi/gen/XmlComments/MemberKey.cs b/src/OpenApi/gen/XmlComments/MemberKey.cs
index c6cb3d0741ab..1b8c19741f8f 100644
--- a/src/OpenApi/gen/XmlComments/MemberKey.cs
+++ b/src/OpenApi/gen/XmlComments/MemberKey.cs
@@ -21,19 +21,70 @@ internal sealed record MemberKey(
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters);
- public static MemberKey FromMethodSymbol(IMethodSymbol method)
+ public static MemberKey FromMethodSymbol(IMethodSymbol method, Compilation compilation)
{
+ string returnType;
+ if (method.ReturnsVoid)
+ {
+ returnType = "typeof(void)";
+ }
+ else
+ {
+ // Handle Task/ValueTask for async methods
+ var actualReturnType = method.ReturnType;
+ if (method.IsAsync && actualReturnType is INamedTypeSymbol namedType)
+ {
+ if (namedType.TypeArguments.Length > 0)
+ {
+ actualReturnType = namedType.TypeArguments[0];
+ }
+ else
+ {
+ actualReturnType = compilation.GetSpecialType(SpecialType.System_Void);
+ }
+ }
+
+ returnType = actualReturnType.TypeKind == TypeKind.TypeParameter
+ ? "typeof(object)"
+ : $"typeof({actualReturnType.ToDisplayString(_typeKeyFormat)})";
+ }
+
+ // Handle extension methods by skipping the 'this' parameter
+ var parameters = method.Parameters
+ .Where(p => !p.IsThis)
+ .Select(p =>
+ {
+ if (p.Type.TypeKind == TypeKind.TypeParameter)
+ {
+ return "typeof(object)";
+ }
+
+ // For params arrays, use the array type
+ if (p.IsParams && p.Type is IArrayTypeSymbol arrayType)
+ {
+ return $"typeof({arrayType.ToDisplayString(_typeKeyFormat)})";
+ }
+
+ return $"typeof({p.Type.ToDisplayString(_typeKeyFormat)})";
+ })
+ .ToArray();
+
+ // For generic methods, use the containing type with generic parameters
+ var declaringType = method.ContainingType;
+ var typeDisplay = declaringType.ToDisplayString(_typeKeyFormat);
+
+ // If the method is in a generic type, we need to handle the type parameters
+ if (declaringType.IsGenericType)
+ {
+ typeDisplay = ReplaceGenericArguments(typeDisplay);
+ }
+
return new MemberKey(
- $"typeof({ReplaceGenericArguments(method.ContainingType.ToDisplayString(_typeKeyFormat))})",
+ $"typeof({typeDisplay})",
MemberType.Method,
- method.MetadataName,
- method.ReturnType.TypeKind == TypeKind.TypeParameter
- ? "typeof(object)"
- : $"typeof({method.ReturnType.ToDisplayString(_typeKeyFormat)})",
- [.. method.Parameters.Select(p =>
- p.Type.TypeKind == TypeKind.TypeParameter
- ? "typeof(object)"
- : $"typeof({p.Type.ToDisplayString(_typeKeyFormat)})")]);
+ method.MetadataName, // Use MetadataName to match runtime MethodInfo.Name
+ returnType,
+ parameters);
}
public static MemberKey FromPropertySymbol(IPropertySymbol property)
diff --git a/src/OpenApi/gen/XmlComments/XmlComment.InheritDoc.cs b/src/OpenApi/gen/XmlComments/XmlComment.InheritDoc.cs
index ee6d503449a0..12ce950f24b0 100644
--- a/src/OpenApi/gen/XmlComments/XmlComment.InheritDoc.cs
+++ b/src/OpenApi/gen/XmlComments/XmlComment.InheritDoc.cs
@@ -16,7 +16,7 @@
namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
///
-/// Source code in this class is derived from Roslyn's Xml documentation comment processing
+/// Source code in this class is derived from Roslyn's XML documentation comment processing
/// to support the full resolution of tags.
/// For the original code, see https://github.com/dotnet/roslyn/blob/ef1d7fe925c94e96a93e4c9af50983e0f675a9fd/src/Workspaces/Core/Portable/Shared/Extensions/ISymbolExtensions.cs.
///
diff --git a/src/OpenApi/gen/XmlComments/XmlComment.cs b/src/OpenApi/gen/XmlComments/XmlComment.cs
index e1ece052e315..be47fa913d82 100644
--- a/src/OpenApi/gen/XmlComments/XmlComment.cs
+++ b/src/OpenApi/gen/XmlComments/XmlComment.cs
@@ -191,7 +191,7 @@ private static void ResolveCrefLink(Compilation compilation, XNode node, string
var symbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(cref, compilation);
if (symbol is not null)
{
- var type = symbol.ToDisplayString();
+ var type = symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
item.ReplaceWith(new XText(type));
}
}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/GenerateAdditionalXmlFilesForOpenApiTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/GenerateAdditionalXmlFilesForOpenApiTests.cs
index f23fed7018c9..9f066e5cab77 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/GenerateAdditionalXmlFilesForOpenApiTests.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/GenerateAdditionalXmlFilesForOpenApiTests.cs
@@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.OpenApi.Build.Tests;
public class GenerateAdditionalXmlFilesForOpenApiTests
{
- private static readonly TimeSpan _defaultProcessTimeout = TimeSpan.FromSeconds(45);
+ private static readonly TimeSpan _defaultProcessTimeout = TimeSpan.FromSeconds(120);
[Fact]
public void VerifiesTargetGeneratesXmlFiles()
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/AdditionalTextsTests.Schemas.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/AdditionalTextsTests.Schemas.cs
new file mode 100644
index 000000000000..623b4a8b3694
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/AdditionalTextsTests.Schemas.cs
@@ -0,0 +1,205 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json.Nodes;
+using Microsoft.OpenApi.Models;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests;
+
+[UsesVerify]
+public partial class AdditionalTextsTests
+{
+ [Fact]
+ public async Task CanHandleXmlForSchemasInAdditionalTexts()
+ {
+ var source = """
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using ClassLibrary;
+
+var builder = WebApplication.CreateBuilder();
+
+builder.Services.AddOpenApi();
+
+var app = builder.Build();
+
+app.MapPost("/todo", (Todo todo) => { });
+app.MapPost("/project", (Project project) => { });
+app.MapPost("/board", (ProjectBoard.BoardItem boardItem) => { });
+app.MapPost("/project-record", (ProjectRecord project) => { });
+app.MapPost("/todo-with-description", (TodoWithDescription todo) => { });
+app.MapPost("/type-with-examples", (TypeWithExamples typeWithExamples) => { });
+
+app.Run();
+""";
+
+ var librarySource = """
+using System;
+
+namespace ClassLibrary;
+
+///
+/// This is a todo item.
+///
+public class Todo
+{
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public string Description { get; set; }
+}
+
+///
+/// The project that contains items.
+///
+public record Project(string Name, string Description);
+
+public class ProjectBoard
+{
+ ///
+ /// An item on the board.
+ ///
+ public class BoardItem
+ {
+ public string Name { get; set; }
+ }
+}
+
+///
+/// The project that contains items.
+///
+/// The name of the project.
+/// The description of the project.
+public record ProjectRecord(string Name, string Description);
+
+public class TodoWithDescription
+{
+ ///
+ /// The identifier of the todo.
+ ///
+ public int Id { get; set; }
+ ///
+ /// The name of the todo.
+ ///
+ public string Name { get; set; }
+ ///
+ /// A description of the todo.
+ ///
+ /// Another description of the todo.
+ public string Description { get; set; }
+}
+
+public class TypeWithExamples
+{
+ /// true
+ public bool BooleanType { get; set; }
+ /// 42
+ public int IntegerType { get; set; }
+ /// 1234567890123456789
+ public long LongType { get; set; }
+ /// 3.14
+ public double DoubleType { get; set; }
+ /// 3.14
+ public float FloatType { get; set; }
+ /// 2022-01-01T00:00:00Z
+ public DateTime DateTimeType { get; set; }
+ /// 2022-01-01
+ public DateOnly DateOnlyType { get; set; }
+ /// Hello, World!
+ public string StringType { get; set; }
+ /// 2d8f1eac-b5c6-4e29-8c62-4d9d75ef3d3d
+ public Guid GuidType { get; set; }
+ /// 12:30:45
+ public TimeOnly TimeOnlyType { get; set; }
+ /// P3DT4H5M
+ public TimeSpan TimeSpanType { get; set; }
+ /// 255
+ public byte ByteType { get; set; }
+ /// 3.14159265359
+ public decimal DecimalType { get; set; }
+ /// https://example.com
+ public Uri UriType { get; set; }
+}
+""";
+ var references = new Dictionary>
+ {
+ { "ClassLibrary", [librarySource] }
+ };
+
+ 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["/todo"].Operations[OperationType.Post];
+ var todo = path.RequestBody.Content["application/json"].Schema;
+ Assert.Equal("This is a todo item.", todo.Description);
+
+ path = document.Paths["/project"].Operations[OperationType.Post];
+ var project = path.RequestBody.Content["application/json"].Schema;
+ Assert.Equal("The project that contains Todo items.", project.Description);
+
+ path = document.Paths["/board"].Operations[OperationType.Post];
+ var board = path.RequestBody.Content["application/json"].Schema;
+ Assert.Equal("An item on the board.", board.Description);
+
+ path = document.Paths["/project-record"].Operations[OperationType.Post];
+ project = path.RequestBody.Content["application/json"].Schema;
+
+ Assert.Equal("The name of the project.", project.Properties["name"].Description);
+ Assert.Equal("The description of the project.", project.Properties["description"].Description);
+
+ path = document.Paths["/todo-with-description"].Operations[OperationType.Post];
+ todo = path.RequestBody.Content["application/json"].Schema;
+ Assert.Equal("The identifier of the todo.", todo.Properties["id"].Description);
+ Assert.Equal("The name of the todo.", todo.Properties["name"].Description);
+ Assert.Equal("Another description of the todo.", todo.Properties["description"].Description);
+
+ path = document.Paths["/type-with-examples"].Operations[OperationType.Post];
+ var typeWithExamples = path.RequestBody.Content["application/json"].Schema;
+
+ var booleanTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["booleanType"].Example);
+ Assert.True(booleanTypeExample.GetValue());
+
+ var integerTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["integerType"].Example);
+ Assert.Equal(42, integerTypeExample.GetValue());
+
+ var longTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["longType"].Example);
+ Assert.Equal(1234567890123456789, longTypeExample.GetValue());
+
+ // Broken due to https://github.com/microsoft/OpenAPI.NET/issues/2137
+ // var doubleTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["doubleType"].Example);
+ // Assert.Equal("3.14", doubleTypeExample.GetValue());
+
+ // var floatTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["floatType"].Example);
+ // Assert.Equal(3.14f, floatTypeExample.GetValue());
+
+ // var dateTimeTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["dateTimeType"].Example);
+ // Assert.Equal(DateTime.Parse("2022-01-01T00:00:00Z", CultureInfo.InvariantCulture), dateTimeTypeExample.GetValue());
+
+ // var dateOnlyTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["dateOnlyType"].Example);
+ // Assert.Equal(DateOnly.Parse("2022-01-01", CultureInfo.InvariantCulture), dateOnlyTypeExample.GetValue());
+
+ var stringTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["stringType"].Example);
+ Assert.Equal("Hello, World!", stringTypeExample.GetValue());
+
+ var guidTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["guidType"].Example);
+ Assert.Equal("2d8f1eac-b5c6-4e29-8c62-4d9d75ef3d3d", guidTypeExample.GetValue());
+
+ var byteTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["byteType"].Example);
+ Assert.Equal(255, byteTypeExample.GetValue());
+
+ // Broken due to https://github.com/microsoft/OpenAPI.NET/issues/2137
+ // var timeOnlyTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["timeOnlyType"].Example);
+ // Assert.Equal(TimeOnly.Parse("12:30:45", CultureInfo.InvariantCulture), timeOnlyTypeExample.GetValue());
+
+ // var timeSpanTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["timeSpanType"].Example);
+ // Assert.Equal(TimeSpan.Parse("P3DT4H5M", CultureInfo.InvariantCulture), timeSpanTypeExample.GetValue());
+
+ // var decimalTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["decimalType"].Example);
+ // Assert.Equal(3.14159265359m, decimalTypeExample.GetValue());
+
+ var uriTypeExample = Assert.IsAssignableFrom(typeWithExamples.Properties["uriType"].Example);
+ Assert.Equal("https://example.com", uriTypeExample.GetValue());
+ });
+ }
+}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/CompletenessTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/CompletenessTests.cs
index b0baa35b59dd..8c81ba948ac7 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/CompletenessTests.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/CompletenessTests.cs
@@ -17,6 +17,7 @@ public async Task SupportsAllXmlTagsOnSchemas()
{
var source = """
using System;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
@@ -180,6 +181,38 @@ public static int Add(int left, int right)
return left + right;
}
+
+ ///
+ /// This method is an example of a method that
+ /// returns an awaitable item.
+ ///
+ public static Task AddAsync(int left, int right)
+ {
+ return Task.FromResult(Add(left, right));
+ }
+
+ ///
+ /// This method is an example of a method that
+ /// returns a Task which should map to a void return type.
+ ///
+ public static Task DoNothingAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// This method is an example of a method that consumes
+ /// an params array.
+ ///
+ public static int AddNumbers(params int[] numbers)
+ {
+ var sum = 0;
+ foreach (var number in numbers)
+ {
+ sum += number;
+ }
+ return sum;
+ }
}
///
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs
index c6fd1a38becc..c2d09bdb5971 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs
@@ -29,6 +29,9 @@ public static partial class SnapshotTestHelper
.WithFeatures([new KeyValuePair("InterceptorsNamespaces", "Microsoft.AspNetCore.OpenApi.Generated")]);
public static Task Verify(string source, IIncrementalGenerator generator, out Compilation compilation)
+ => Verify(source, generator, [], out compilation, out _);
+
+ public static Task Verify(string source, IIncrementalGenerator generator, Dictionary> classLibrarySources, out Compilation compilation, out List generatedAssemblies)
{
var references = AppDomain.CurrentDomain.GetAssemblies()
.Where(assembly => !assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location))
@@ -47,12 +50,49 @@ public static Task Verify(string source, IIncrementalGenerator generator, out Co
MetadataReference.CreateFromFile(typeof(System.Text.Json.Nodes.JsonArray).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Uri).Assembly.Location),
- ]);
+ ])
+ .ToList();
+
+ var additionalTexts = new List();
+ generatedAssemblies = [];
+
+ foreach (var classLibrary in classLibrarySources)
+ {
+ var classLibraryCompilation = CSharpCompilation.Create(classLibrary.Key,
+ classLibrary.Value.Select((source, index) => CSharpSyntaxTree.ParseText(source, options: ParseOptions, path: $"{classLibrary.Key}-{index}.cs")),
+ references,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+
+ var ms = new MemoryStream();
+ using var xmlStream = new MemoryStream();
+ var emitResult = classLibraryCompilation.Emit(ms, xmlDocumentationStream: xmlStream);
+
+ if (!emitResult.Success)
+ {
+ throw new InvalidOperationException($"Failed to compile class library {classLibrary.Key}: {string.Join(Environment.NewLine, emitResult.Diagnostics)}");
+ }
+
+ ms.Seek(0, SeekOrigin.Begin);
+ xmlStream.Seek(0, SeekOrigin.Begin);
+
+ var assembly = ms.ToArray();
+ generatedAssemblies.Add(assembly);
+ references.Add(MetadataReference.CreateFromImage(assembly));
+
+ var xmlText = Encoding.UTF8.GetString(xmlStream.ToArray());
+ additionalTexts.Add(new TestAdditionalText($"{classLibrary.Key}.xml", xmlText));
+ }
+
var inputCompilation = CSharpCompilation.Create("OpenApiXmlCommentGeneratorSample",
[CSharpSyntaxTree.ParseText(source, options: ParseOptions, path: "Program.cs")],
references,
new CSharpCompilationOptions(OutputKind.ConsoleApplication));
- var driver = CSharpGeneratorDriver.Create(generators: [generator.AsSourceGenerator()], parseOptions: ParseOptions);
+
+ var driver = CSharpGeneratorDriver.Create(
+ generators: [generator.AsSourceGenerator()],
+ additionalTexts: additionalTexts,
+ parseOptions: ParseOptions);
+
return Verifier
.Verify(driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out compilation, out var diagnostics))
.ScrubLinesWithReplace(line => InterceptsLocationRegex().Replace(line, "[InterceptsLocation]"))
@@ -62,6 +102,9 @@ public static Task Verify(string source, IIncrementalGenerator generator, out Co
}
public static async Task VerifyOpenApi(Compilation compilation, Action verifyFunc)
+ => await VerifyOpenApi(compilation, [], verifyFunc);
+
+ public static async Task VerifyOpenApi(Compilation compilation, List generatedAssemblies, Action verifyFunc)
{
var assemblyName = compilation.AssemblyName;
var symbolsName = Path.ChangeExtension(assemblyName, "pdb");
@@ -100,6 +143,10 @@ public static async Task VerifyOpenApi(Compilation compilation, Action path;
+
+ public override SourceText GetText(CancellationToken cancellationToken = default)
+ {
+ return SourceText.From(text, Encoding.UTF8);
+ }
+ }
}
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.verified.cs
new file mode 100644
index 000000000000..69425f1352d9
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts#OpenApiXmlCommentSupport.generated.verified.cs
@@ -0,0 +1,380 @@
+//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
+
+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.Linq;
+ using System.Reflection;
+ 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.Models;
+ using Microsoft.OpenApi.Models.References;
+ using Microsoft.OpenApi.Any;
+
+ [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 sealed record MemberKey(
+ Type? DeclaringType,
+ MemberType MemberKind,
+ string? Name,
+ Type? ReturnType,
+ Type[]? Parameters) : IEquatable
+ {
+ public bool Equals(MemberKey? other)
+ {
+ if (other is null) return false;
+
+ // Check member kind
+ if (MemberKind != other.MemberKind) return false;
+
+ // Check declaring type, handling generic types
+ if (!TypesEqual(DeclaringType, other.DeclaringType)) return false;
+
+ // Check name
+ if (Name != other.Name) return false;
+
+ // For methods, check return type and parameters
+ if (MemberKind == MemberType.Method)
+ {
+ if (!TypesEqual(ReturnType, other.ReturnType)) return false;
+ if (Parameters is null || other.Parameters is null) return false;
+ if (Parameters.Length != other.Parameters.Length) return false;
+
+ for (int i = 0; i < Parameters.Length; i++)
+ {
+ if (!TypesEqual(Parameters[i], other.Parameters[i])) return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool TypesEqual(Type? type1, Type? type2)
+ {
+ if (type1 == type2) return true;
+ if (type1 == null || type2 == null) return false;
+
+ if (type1.IsGenericType && type2.IsGenericType)
+ {
+ return type1.GetGenericTypeDefinition() == type2.GetGenericTypeDefinition();
+ }
+
+ return type1 == type2;
+ }
+
+ public override int GetHashCode()
+ {
+ var hash = new HashCode();
+ hash.Add(GetTypeHashCode(DeclaringType));
+ hash.Add(MemberKind);
+ hash.Add(Name);
+
+ if (MemberKind == MemberType.Method)
+ {
+ hash.Add(GetTypeHashCode(ReturnType));
+ if (Parameters is not null)
+ {
+ foreach (var param in Parameters)
+ {
+ hash.Add(GetTypeHashCode(param));
+ }
+ }
+ }
+
+ return hash.ToHashCode();
+ }
+
+ private static int GetTypeHashCode(Type? type)
+ {
+ if (type == null) return 0;
+ return type.IsGenericType ? type.GetGenericTypeDefinition().GetHashCode() : type.GetHashCode();
+ }
+
+ public static MemberKey FromMethodInfo(MethodInfo method)
+ {
+ return new MemberKey(
+ method.DeclaringType,
+ MemberType.Method,
+ method.Name,
+ method.ReturnType.IsGenericParameter ? typeof(object) : method.ReturnType,
+ method.GetParameters().Select(p => p.ParameterType.IsGenericParameter ? typeof(object) : p.ParameterType).ToArray());
+ }
+
+ public static MemberKey FromPropertyInfo(PropertyInfo property)
+ {
+ return new MemberKey(
+ property.DeclaringType,
+ MemberType.Property,
+ property.Name,
+ null,
+ null);
+ }
+
+ public static MemberKey FromTypeInfo(Type type)
+ {
+ return new MemberKey(
+ type,
+ MemberType.Type,
+ null,
+ null,
+ null);
+ }
+ }
+
+ file enum MemberType
+ {
+ Type,
+ Property,
+ Method
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file static class XmlCommentCache
+ {
+ private static Dictionary? _cache;
+ public static Dictionary Cache => _cache ??= GenerateCacheEntries();
+
+ private static Dictionary GenerateCacheEntries()
+ {
+ var _cache = new Dictionary();
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.Todo), MemberType.Type, null, null, []), new XmlComment(@"This is a todo item.", null, null, null, null, false, null, null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.Project), MemberType.Type, null, null, []), new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.Project), MemberType.Method, ".ctor", typeof(void), [typeof(global::System.String), typeof(global::System.String)]), new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.ProjectBoard.BoardItem), MemberType.Type, null, null, []), new XmlComment(@"An item on the board.", null, null, null, null, false, null, null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.ProjectRecord), MemberType.Type, null, null, []), new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, [new XmlParameterComment(@"Name", @"The name of the project.", null, false), new XmlParameterComment(@"Description", @"The description of the project.", null, false)], null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.ProjectRecord), MemberType.Method, ".ctor", typeof(void), [typeof(global::System.String), typeof(global::System.String)]), new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, [new XmlParameterComment(@"Name", @"The name of the project.", null, false), new XmlParameterComment(@"Description", @"The description of the project.", null, false)], null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.ProjectRecord), MemberType.Property, "Name", null, []), new XmlComment(@"The name of the project.", null, null, null, null, false, null, null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.ProjectRecord), MemberType.Property, "Description", null, []), new XmlComment(@"The description of the project.", null, null, null, null, false, null, null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TodoWithDescription), MemberType.Property, "Id", null, []), new XmlComment(@"The identifier of the todo.", null, null, null, null, false, null, null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TodoWithDescription), MemberType.Property, "Name", null, []), new XmlComment(null, null, null, null, @"The name of the todo.", false, null, null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TodoWithDescription), MemberType.Property, "Description", null, []), new XmlComment(@"A description of the todo.", null, null, null, @"Another description of the todo.", false, null, null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "BooleanType", null, []), new XmlComment(null, null, null, null, null, false, [@"true"], null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "IntegerType", null, []), new XmlComment(null, null, null, null, null, false, [@"42"], null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "LongType", null, []), new XmlComment(null, null, null, null, null, false, [@"1234567890123456789"], null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "DoubleType", null, []), new XmlComment(null, null, null, null, null, false, [@"3.14"], null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "FloatType", null, []), new XmlComment(null, null, null, null, null, false, [@"3.14"], null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "DateTimeType", null, []), new XmlComment(null, null, null, null, null, false, [@"2022-01-01T00:00:00Z"], null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "DateOnlyType", null, []), new XmlComment(null, null, null, null, null, false, [@"2022-01-01"], null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "StringType", null, []), new XmlComment(null, null, null, null, null, false, [@"Hello, World!"], null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "GuidType", null, []), new XmlComment(null, null, null, null, null, false, [@"2d8f1eac-b5c6-4e29-8c62-4d9d75ef3d3d"], null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "TimeOnlyType", null, []), new XmlComment(null, null, null, null, null, false, [@"12:30:45"], null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "TimeSpanType", null, []), new XmlComment(null, null, null, null, null, false, [@"P3DT4H5M"], null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "ByteType", null, []), new XmlComment(null, null, null, null, null, false, [@"255"], null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "DecimalType", null, []), new XmlComment(null, null, null, null, null, false, [@"3.14159265359"], null, null));
+ _cache.Add(new MemberKey(typeof(global::ClassLibrary.TypeWithExamples), MemberType.Property, "UriType", null, []), new XmlComment(null, null, null, null, null, false, [@"https://example.com"], null, null));
+
+
+ return _cache;
+ }
+
+ internal static bool TryGetXmlComment(Type? type, MethodInfo? methodInfo, [NotNullWhen(true)] out XmlComment? xmlComment)
+ {
+ if (methodInfo is null)
+ {
+ return Cache.TryGetValue(new MemberKey(type, MemberType.Property, null, null, null), out xmlComment);
+ }
+
+ return Cache.TryGetValue(MemberKey.FromMethodInfo(methodInfo), out xmlComment);
+ }
+
+ internal static bool TryGetXmlComment(Type? type, string? memberName, [NotNullWhen(true)] out XmlComment? xmlComment)
+ {
+ return Cache.TryGetValue(new MemberKey(type, memberName is null ? MemberType.Type : MemberType.Property, memberName, null, null), out xmlComment);
+ }
+ }
+
+ [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.TryGetXmlComment(methodInfo.DeclaringType, methodInfo, 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 = operationParameter is OpenApiParameterReference reference
+ ? reference.Target
+ : (OpenApiParameter)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)
+ {
+ foreach (var mediaType in requestBody.Content.Values)
+ {
+ mediaType.Example = jsonString.Parse();
+ }
+ }
+ }
+ }
+ }
+ }
+ 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;
+ }
+ }
+
+ [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.TryGetXmlComment(propertyInfo.DeclaringType, propertyInfo.Name, out var propertyComment))
+ {
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ }
+ if (XmlCommentCache.TryGetXmlComment(context.JsonTypeInfo.Type, (string?)null, 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/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs
index aa9137dd68b8..e2150a6ea5be 100644
--- 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
@@ -256,7 +256,7 @@ type as a cref attribute.
In generic classes and methods, you'll often want to reference the
generic type, or the type parameter.", null, null, false, null, null, null));
_cache.Add(new MemberKey(typeof(global::ParamsAndParamRefs), MemberType.Type, null, null, []), new XmlComment(@"This shows examples of typeparamref and typeparam tags", null, null, null, null, false, null, null, null));
- _cache.Add(new MemberKey(typeof(global::ExampleClass), MemberType.Property, "Label", null, []), new XmlComment(null, null, @" The ExampleClass.Label is a
+ _cache.Add(new MemberKey(typeof(global::ExampleClass), MemberType.Property, "Label", null, []), new XmlComment(null, null, @" The string? ExampleClass.Label is a
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
@@ -268,6 +268,12 @@ Note that there isn't a way to provide a ""cref"" to
{
Console.WriteLine(c);
}```"], [new XmlParameterComment(@"left", @"The left operand of the addition.", null, false), new XmlParameterComment(@"right", @"The right operand of the addition.", null, false)], null));
+ _cache.Add(new MemberKey(typeof(global::ExampleClass), MemberType.Method, "AddAsync", typeof(global::System.Threading.Tasks.Task), [typeof(global::System.Int32), typeof(global::System.Int32)]), new XmlComment(@"This method is an example of a method that
+returns an awaitable item.", null, null, null, null, false, null, null, null));
+ _cache.Add(new MemberKey(typeof(global::ExampleClass), MemberType.Method, "DoNothingAsync", typeof(global::System.Threading.Tasks.Task), []), new XmlComment(@"This method is an example of a method that
+returns a Task which should map to a void return type.", null, null, null, null, false, null, null, null));
+ _cache.Add(new MemberKey(typeof(global::ExampleClass), MemberType.Method, "AddNumbers", typeof(global::System.Int32), [typeof(global::System.Int32[])]), new XmlComment(@"This method is an example of a method that consumes
+an params array.", null, null, null, null, false, null, null, null));
_cache.Add(new MemberKey(typeof(global::ITestInterface), MemberType.Method, "Method", typeof(global::System.Int32), [typeof(global::System.Int32)]), new XmlComment(@"This method is part of the test interface.", null, @"This content would be inherited by classes
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));
@@ -276,7 +282,7 @@ that implement this interface when the
_cache.Add(new MemberKey(typeof(global::InheritAllButRemarks), MemberType.Method, "MyParentMethod", typeof(global::System.Boolean), [typeof(global::System.Boolean)]), new XmlComment(@"In this example, this summary is visible on all the methods.", null, @"The remarks can be inherited by other methods
using the xpath expression.", @"A boolean", null, false, null, null, null));
_cache.Add(new MemberKey(typeof(global::InheritAllButRemarks), MemberType.Method, "MyChildMethod", typeof(global::System.Boolean), []), new XmlComment(@"In this example, this summary is visible on all the methods.", null, null, @"A boolean", null, false, null, null, null));
- _cache.Add(new MemberKey(typeof(global::ParamsAndParamRefs), MemberType.Method, "GetGenericValue", typeof(object), [typeof(object)]), new XmlComment(@"The GetGenericValue method.", null, @"This sample shows how to specify the ParamsAndParamRefs.GetGenericValue<T>(T)
+ _cache.Add(new MemberKey(typeof(global::ParamsAndParamRefs), MemberType.Method, "GetGenericValue", typeof(object), [typeof(object)]), new XmlComment(@"The GetGenericValue method.", null, @"This sample shows how to specify the T ParamsAndParamRefs.GetGenericValue<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));