diff --git a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
index cf8bb5e875eb..ecd59881e3f2 100644
--- a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
+++ b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
@@ -449,17 +449,7 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
- {
- 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)
- {
- schema.Example = jsonString.Parse();
- }
- }
- }
+ // Apply comments from the type
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
@@ -468,6 +458,34 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
schema.Example = jsonString.Parse();
}
}
+
+ if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ {
+ // Apply comments from the property
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
+ {
+ if (schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string))
+ {
+ // Inlined schema
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ else
+ {
+ // Schema Reference
+ schema.Metadata["x-ref-description"] = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Metadata["x-ref-example"] = jsonString.Parse()!;
+ }
+ }
+ }
+ }
return Task.CompletedTask;
}
}
@@ -507,6 +525,7 @@ file static class GeneratedServiceCollectionExtensions
{{GenerateAddOpenApiInterceptions(groupedAddOpenApiInvocations)}}
}
}
+
""";
internal static string GetAddOpenApiInterceptor(AddOpenApiOverloadVariant overloadVariant) => overloadVariant switch
diff --git a/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs b/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs
index c53465f8226c..c09bd50dc67b 100644
--- a/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs
+++ b/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs
@@ -1,6 +1,8 @@
// 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;
+
namespace Microsoft.AspNetCore.OpenApi;
internal static class OpenApiDocumentExtensions
@@ -21,6 +23,19 @@ public static IOpenApiSchema AddOpenApiSchemaByReference(this OpenApiDocument do
document.Workspace ??= new();
var location = document.BaseUri + "/components/schemas/" + schemaId;
document.Workspace.RegisterComponentForDocument(document, schema, location);
- return new OpenApiSchemaReference(schemaId, document);
+
+ object? description = null;
+ object? example = null;
+ if (schema is OpenApiSchema actualSchema)
+ {
+ actualSchema.Metadata?.TryGetValue(OpenApiConstants.RefDescriptionAnnotation, out description);
+ actualSchema.Metadata?.TryGetValue(OpenApiConstants.RefExampleAnnotation, out example);
+ }
+
+ return new OpenApiSchemaReference(schemaId, document)
+ {
+ Description = description as string,
+ Examples = example is JsonNode exampleJson ? [exampleJson] : null,
+ };
}
}
diff --git a/src/OpenApi/src/Services/OpenApiConstants.cs b/src/OpenApi/src/Services/OpenApiConstants.cs
index 8c34704cf2b6..b32e5bb6e936 100644
--- a/src/OpenApi/src/Services/OpenApiConstants.cs
+++ b/src/OpenApi/src/Services/OpenApiConstants.cs
@@ -13,6 +13,8 @@ internal static class OpenApiConstants
internal const string DescriptionId = "x-aspnetcore-id";
internal const string SchemaId = "x-schema-id";
internal const string RefId = "x-ref-id";
+ internal const string RefDescriptionAnnotation = "x-ref-description";
+ internal const string RefExampleAnnotation = "x-ref-example";
internal const string DefaultOpenApiResponseKey = "default";
// Since there's a finite set of HTTP methods that can be included in a given
// OpenApiPaths, we can pre-allocate an array of these methods and use a direct
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs
index 290100cdbfc3..629b1322c376 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs
@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// 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;
@@ -175,6 +175,7 @@ internal class User : IUser
///
public string Name { get; set; }
}
+
""";
var generator = new XmlCommentGenerator();
await SnapshotTestHelper.Verify(source, generator, out var compilation);
@@ -260,4 +261,161 @@ await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
Assert.Equal("The user's display name.", user.Properties["name"].Description);
});
}
+
+ [Fact]
+ public async Task XmlCommentsOnPropertiesShouldApplyToSchemaReferences()
+ {
+ var source = """
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+
+var builder = WebApplication.CreateBuilder();
+
+builder.Services.AddOpenApi(options => {
+ var prevCreateSchemaReferenceId = options.CreateSchemaReferenceId;
+ options.CreateSchemaReferenceId = (x) => x.Type == typeof(ModelInline) ? null : prevCreateSchemaReferenceId(x);
+});
+
+var app = builder.Build();
+
+app.MapPost("/example", (RootModel model) => { });
+
+app.Run();
+
+///
+/// Comment on class ModelWithSummary.
+///
+///
+/// { "street": "ModelWithSummaryClass" }
+///
+public class ModelWithSummary
+{
+ public string Street { get; set; }
+}
+
+public class ModelWithoutSummary
+{
+ public string Street { get; set; }
+}
+
+///
+/// Comment on class ModelInline.
+///
+///
+/// { "street": "ModelInlineClass" }
+///
+public class ModelInline
+{
+ public string Street { get; set; }
+}
+
+///
+/// Comment on class RootModel.
+///
+///
+/// { }
+///
+public class RootModel
+{
+ public ModelWithSummary NoPropertyComment { get; set; }
+
+ ///
+ /// Comment on property ModelWithSummary1.
+ ///
+ ///
+ /// { "street": "ModelWithSummary1Prop" }
+ ///
+ public ModelWithSummary ModelWithSummary1 { get; set; }
+
+ ///
+ /// Comment on property ModelWithSummary2.
+ ///
+ ///
+ /// { "street": "ModelWithSummary2Prop" }
+ ///
+ public ModelWithSummary ModelWithSummary2 { get; set; }
+
+ ///
+ /// Comment on property ModelWithoutSummary1.
+ ///
+ ///
+ /// { "street": "ModelWithoutSummary1Prop" }
+ ///
+ public ModelWithoutSummary ModelWithoutSummary1 { get; set; }
+
+ ///
+ /// Comment on property ModelWithoutSummary2.
+ ///
+ ///
+ /// { "street": "ModelWithoutSummary2Prop" }
+ ///
+ public ModelWithoutSummary ModelWithoutSummary2 { get; set; }
+
+ ///
+ /// Comment on property ModelInline1.
+ ///
+ ///
+ /// { "street": "ModelInline1Prop" }
+ ///
+ public ModelInline ModelInline1 { get; set; }
+
+ ///
+ /// Comment on property ModelInline2.
+ ///
+ ///
+ /// { "street": "ModelInline2Prop" }
+ ///
+ public ModelInline ModelInline2 { get; set; }
+}
+""";
+ var generator = new XmlCommentGenerator();
+ await SnapshotTestHelper.Verify(source, generator, out var compilation);
+ await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
+ {
+ var path = document.Paths["/example"].Operations[HttpMethod.Post];
+ var exampleOperationBodySchema = path.RequestBody.Content["application/json"].Schema;
+ Assert.Equal("Comment on class RootModel.", exampleOperationBodySchema.Description);
+
+ var rootModelSchema = document.Components.Schemas["RootModel"];
+ Assert.Equal("Comment on class RootModel.", rootModelSchema.Description);
+
+ var modelWithSummary = document.Components.Schemas["ModelWithSummary"];
+ Assert.Equal("Comment on class ModelWithSummary.", modelWithSummary.Description);
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse("""{ "street": "ModelWithSummaryClass" }"""), modelWithSummary.Example));
+
+ var modelWithoutSummary = document.Components.Schemas["ModelWithoutSummary"];
+ Assert.Null(modelWithoutSummary.Description);
+
+ Assert.DoesNotContain("ModelInline", document.Components.Schemas.Keys);
+
+ // Check RootModel properties
+ var noPropertyCommentProp = Assert.IsType(rootModelSchema.Properties["noPropertyComment"]);
+ Assert.Null(noPropertyCommentProp.Reference.Description);
+
+ var modelWithSummary1Prop = Assert.IsType(rootModelSchema.Properties["modelWithSummary1"]);
+ Assert.Equal("Comment on property ModelWithSummary1.", modelWithSummary1Prop.Description);
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse("""{ "street": "ModelWithSummary1Prop" }"""), modelWithSummary1Prop.Examples[0]));
+
+ var modelWithSummary2Prop = Assert.IsType(rootModelSchema.Properties["modelWithSummary2"]);
+ Assert.Equal("Comment on property ModelWithSummary2.", modelWithSummary2Prop.Description);
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse("""{ "street": "ModelWithSummary2Prop" }"""), modelWithSummary2Prop.Examples[0]));
+
+ var modelWithoutSummary1Prop = Assert.IsType(rootModelSchema.Properties["modelWithoutSummary1"]);
+ Assert.Equal("Comment on property ModelWithoutSummary1.", modelWithoutSummary1Prop.Description);
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse("""{ "street": "ModelWithoutSummary1Prop" }"""), modelWithoutSummary1Prop.Examples[0]));
+
+ var modelWithoutSummary2Prop = Assert.IsType(rootModelSchema.Properties["modelWithoutSummary2"]);
+ Assert.Equal("Comment on property ModelWithoutSummary2.", modelWithoutSummary2Prop.Description);
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse("""{ "street": "ModelWithoutSummary2Prop" }"""), modelWithoutSummary2Prop.Examples[0]));
+
+ var modelInline1Prop = Assert.IsType(rootModelSchema.Properties["modelInline1"]);
+ Assert.Equal("Comment on property ModelInline1.", modelInline1Prop.Description);
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse("""{ "street": "ModelInline1Prop" }"""), modelInline1Prop.Example));
+
+ var modelInline2Prop = Assert.IsType(rootModelSchema.Properties["modelInline2"]);
+ Assert.Equal("Comment on property ModelInline2.", modelInline2Prop.Description);
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse("""{ "street": "ModelInline2Prop" }"""), modelInline2Prop.Example));
+ });
+ }
}
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.verified.cs
index dea5968bdf63..4f143854b812 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.verified.cs
@@ -1,4 +1,4 @@
-//HintName: OpenApiXmlCommentSupport.generated.cs
+//HintName: OpenApiXmlCommentSupport.generated.cs
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
@@ -431,17 +431,7 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
- {
- 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)
- {
- schema.Example = jsonString.Parse();
- }
- }
- }
+ // Apply comments from the type
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
@@ -450,6 +440,34 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
schema.Example = jsonString.Parse();
}
}
+
+ if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ {
+ // Apply comments from the property
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
+ {
+ if (schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string))
+ {
+ // Inlined schema
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ else
+ {
+ // Schema Reference
+ schema.Metadata["x-ref-description"] = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Metadata["x-ref-example"] = jsonString.Parse()!;
+ }
+ }
+ }
+ }
return Task.CompletedTask;
}
}
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
index fe18f3d0de2c..7b4426da5bee 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.verified.cs
@@ -460,17 +460,7 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
- {
- 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)
- {
- schema.Example = jsonString.Parse();
- }
- }
- }
+ // Apply comments from the type
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
@@ -479,6 +469,34 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
schema.Example = jsonString.Parse();
}
}
+
+ if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ {
+ // Apply comments from the property
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
+ {
+ if (schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string))
+ {
+ // Inlined schema
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ else
+ {
+ // Schema Reference
+ schema.Metadata["x-ref-description"] = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Metadata["x-ref-example"] = jsonString.Parse()!;
+ }
+ }
+ }
+ }
return Task.CompletedTask;
}
}
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 69e1bbcab4b4..979f2251418d 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
@@ -552,17 +552,7 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
- {
- 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)
- {
- schema.Example = jsonString.Parse();
- }
- }
- }
+ // Apply comments from the type
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
@@ -571,6 +561,34 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
schema.Example = jsonString.Parse();
}
}
+
+ if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ {
+ // Apply comments from the property
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
+ {
+ if (schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string))
+ {
+ // Inlined schema
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ else
+ {
+ // Schema Reference
+ schema.Metadata["x-ref-description"] = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Metadata["x-ref-example"] = jsonString.Parse()!;
+ }
+ }
+ }
+ }
return Task.CompletedTask;
}
}
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
index 4baa534fd17c..e1505e95c046 100644
--- 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
@@ -435,17 +435,7 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
- {
- 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)
- {
- schema.Example = jsonString.Parse();
- }
- }
- }
+ // Apply comments from the type
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
@@ -454,6 +444,34 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
schema.Example = jsonString.Parse();
}
}
+
+ if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ {
+ // Apply comments from the property
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
+ {
+ if (schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string))
+ {
+ // Inlined schema
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ else
+ {
+ // Schema Reference
+ schema.Metadata["x-ref-description"] = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Metadata["x-ref-example"] = jsonString.Parse()!;
+ }
+ }
+ }
+ }
return Task.CompletedTask;
}
}
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
index fc1fbec8ab34..b0c6faedc4ee 100644
--- 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
@@ -453,17 +453,7 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
- {
- 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)
- {
- schema.Example = jsonString.Parse();
- }
- }
- }
+ // Apply comments from the type
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
@@ -472,6 +462,34 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
schema.Example = jsonString.Parse();
}
}
+
+ if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ {
+ // Apply comments from the property
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
+ {
+ if (schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string))
+ {
+ // Inlined schema
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ else
+ {
+ // Schema Reference
+ schema.Metadata["x-ref-description"] = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Metadata["x-ref-example"] = jsonString.Parse()!;
+ }
+ }
+ }
+ }
return Task.CompletedTask;
}
}
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.verified.cs
index 33e3e561e958..bf889e0c50be 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.verified.cs
@@ -461,17 +461,7 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
- {
- 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)
- {
- schema.Example = jsonString.Parse();
- }
- }
- }
+ // Apply comments from the type
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
@@ -480,6 +470,34 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
schema.Example = jsonString.Parse();
}
}
+
+ if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ {
+ // Apply comments from the property
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
+ {
+ if (schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string))
+ {
+ // Inlined schema
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ else
+ {
+ // Schema Reference
+ schema.Metadata["x-ref-description"] = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Metadata["x-ref-example"] = jsonString.Parse()!;
+ }
+ }
+ }
+ }
return Task.CompletedTask;
}
}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.XmlCommentsOnPropertiesShouldApplyToSchemaReferences#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.XmlCommentsOnPropertiesShouldApplyToSchemaReferences#OpenApiXmlCommentSupport.generated.verified.cs
new file mode 100644
index 000000000000..e60c10ce68f7
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.XmlCommentsOnPropertiesShouldApplyToSchemaReferences#OpenApiXmlCommentSupport.generated.verified.cs
@@ -0,0 +1,528 @@
+//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:ModelWithSummary", new XmlComment(@"Comment on class ModelWithSummary.", null, null, null, null, false, [@"{ ""street"": ""ModelWithSummaryClass"" }"], null, null));
+ cache.Add(@"T:ModelInline", new XmlComment(@"Comment on class ModelInline.", null, null, null, null, false, [@"{ ""street"": ""ModelInlineClass"" }"], null, null));
+ cache.Add(@"T:RootModel", new XmlComment(@"Comment on class RootModel.", null, null, null, null, false, [@"{ }"], null, null));
+ cache.Add(@"P:RootModel.ModelWithSummary1", new XmlComment(@"Comment on property ModelWithSummary1.", null, null, null, null, false, [@"{ ""street"": ""ModelWithSummary1Prop"" }"], null, null));
+ cache.Add(@"P:RootModel.ModelWithSummary2", new XmlComment(@"Comment on property ModelWithSummary2.", null, null, null, null, false, [@"{ ""street"": ""ModelWithSummary2Prop"" }"], null, null));
+ cache.Add(@"P:RootModel.ModelWithoutSummary1", new XmlComment(@"Comment on property ModelWithoutSummary1.", null, null, null, null, false, [@"{ ""street"": ""ModelWithoutSummary1Prop"" }"], null, null));
+ cache.Add(@"P:RootModel.ModelWithoutSummary2", new XmlComment(@"Comment on property ModelWithoutSummary2.", null, null, null, null, false, [@"{ ""street"": ""ModelWithoutSummary2Prop"" }"], null, null));
+ cache.Add(@"P:RootModel.ModelInline1", new XmlComment(@"Comment on property ModelInline1.", null, null, null, null, false, [@"{ ""street"": ""ModelInline1Prop"" }"], null, null));
+ cache.Add(@"P:RootModel.ModelInline2", new XmlComment(@"Comment on property ModelInline2.", null, null, null, null, false, [@"{ ""street"": ""ModelInline2Prop"" }"], null, null));
+
+ return cache;
+ }
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.OpenApi.SourceGenerators, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ 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('+', '.');
+ }
+
+ ///
+ /// 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")]
+ 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(DocumentationCommentIdHelper.NormalizeDocId(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)
+ {
+ // Apply comments from the type
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
+ {
+ schema.Description = typeComment.Summary;
+ if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+
+ if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ {
+ // Apply comments from the property
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
+ {
+ if (schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string))
+ {
+ // Inlined schema
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ else
+ {
+ // Schema Reference
+ schema.Metadata["x-ref-description"] = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Metadata["x-ref-example"] = jsonString.Parse()!;
+ }
+ }
+ }
+ }
+ 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 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, Action configureOptions)
+ {
+ return services.AddOpenApi("v1", options =>
+ {
+ options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
+ options.AddOperationTransformer(new XmlCommentOperationTransformer());
+ configureOptions(options);
+ });
+ }
+
+ }
+}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.verified.cs
index fb09682caf38..a2a2af1855b3 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.verified.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.verified.cs
@@ -432,17 +432,7 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
- {
- 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)
- {
- schema.Example = jsonString.Parse();
- }
- }
- }
+ // Apply comments from the type
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
@@ -451,6 +441,34 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
schema.Example = jsonString.Parse();
}
}
+
+ if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ {
+ // Apply comments from the property
+ if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
+ {
+ if (schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string))
+ {
+ // Inlined schema
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ else
+ {
+ // Schema Reference
+ schema.Metadata["x-ref-description"] = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Metadata["x-ref-example"] = jsonString.Parse()!;
+ }
+ }
+ }
+ }
return Task.CompletedTask;
}
}
@@ -498,4 +516,4 @@ public static IServiceCollection AddOpenApi(this IServiceCollection services)
}
}
-}
\ No newline at end of file
+}