diff --git a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
index cf8bb5e875eb..24e3c75b38e8 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,33 @@ 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))
+ {
+ var isInlinedSchema = schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string);
+ if(isInlinedSchema)
+ {
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ else
+ {
+ 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 +524,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..6e194e8788f2 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,16 @@ 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;
+ ((OpenApiSchema)schema).Metadata.TryGetValue("x-ref-description", out description);
+ ((OpenApiSchema)schema).Metadata.TryGetValue("x-ref-example", out example);
+
+ return new OpenApiSchemaReference(schemaId, document)
+ {
+ Description = description as string,
+ Examples = example is JsonNode exampleJsonNode ? [exampleJsonNode] : null,
+ };
}
}
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 82ae70006c10..ead167aef011 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/CompletenessTests.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/CompletenessTests.cs
@@ -516,9 +516,6 @@ await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
Assert.Equal("This class validates the behavior for mapping\ngeneric types to open generics for use in\ntypeof expressions.", genericParent.Description, ignoreLineEndingDifferences: true);
Assert.Equal("This property is a nullable value type.", genericParent.Properties["id"].Description);
Assert.Equal("This property is a nullable reference type.", genericParent.Properties["name"].Description);
- Assert.Equal("This property is a generic type containing a tuple.", genericParent.Properties["taskOfTupleProp"].Description);
- Assert.Equal("This property is a tuple with a generic type inside.", genericParent.Properties["tupleWithGenericProp"].Description);
- Assert.Equal("This property is a tuple with a nested generic type inside.", genericParent.Properties["tupleWithNestedGenericProp"].Description);
path = document.Paths["/params-and-param-refs"].Operations[HttpMethod.Post];
var paramsAndParamRefs = path.RequestBody.Content["application/json"].Schema;
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..0f03c8ec9dba 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;
@@ -19,7 +19,10 @@ public async Task SupportsXmlCommentsOnSchemas()
var builder = WebApplication.CreateBuilder();
-builder.Services.AddOpenApi();
+builder.Services.AddOpenApi(options => {
+ var prevCreateSchemaReferenceId = options.CreateSchemaReferenceId;
+ options.CreateSchemaReferenceId = (x) => x.Type == typeof(AddressNested) ? null : prevCreateSchemaReferenceId(x);
+});
var app = builder.Build();
@@ -31,6 +34,7 @@ public async Task SupportsXmlCommentsOnSchemas()
app.MapPost("/todo-with-description", (TodoWithDescription todo) => { });
app.MapPost("/type-with-examples", (TypeWithExamples typeWithExamples) => { });
app.MapPost("/user", (User user) => { });
+app.MapPost("/company", (Company company) => { });
app.Run();
@@ -175,6 +179,60 @@ internal class User : IUser
///
public string Name { get; set; }
}
+
+///
+/// An address.
+///
+public class AddressWithSummary
+{
+ public string Street { get; set; }
+}
+
+public class AddressWithoutSummary
+{
+ public string Street { get; set; }
+}
+
+///
+/// An address.
+///
+public class AddressNested
+{
+ public string Street { get; set; }
+}
+
+public class Company
+{
+ ///
+ /// Billing address.
+ ///
+ public AddressWithSummary BillingAddressClassWithSummary { get; set; }
+
+ ///
+ /// Billing address.
+ ///
+ public AddressWithoutSummary BillingAddressClassWithoutSummary { get; set; }
+
+ ///
+ /// Billing address.
+ ///
+ public AddressNested BillingAddressNested { get; set; }
+
+ ///
+ /// Visiting address.
+ ///
+ public AddressWithSummary VisitingAddressClassWithSummary { get; set; }
+
+ ///
+ /// Visiting address.
+ ///
+ public AddressWithoutSummary VisitingAddressClassWithoutSummary { get; set; }
+
+ ///
+ /// Visiting address.
+ ///
+ public AddressNested VisitingAddressNested { get; set; }
+}
""";
var generator = new XmlCommentGenerator();
await SnapshotTestHelper.Verify(source, generator, out var compilation);
@@ -258,6 +316,118 @@ await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
var user = path.RequestBody.Content["application/json"].Schema;
Assert.Equal("The unique identifier for the user.", user.Properties["id"].Description);
Assert.Equal("The user's display name.", user.Properties["name"].Description);
+
+ path = document.Paths["/company"].Operations[HttpMethod.Post];
+ var company = path.RequestBody.Content["application/json"].Schema;
+ Assert.Equal("Billing address.", company.Properties["billingAddressNested"].Description);
+ Assert.Equal("Visiting address.", company.Properties["visitingAddressNested"].Description);
+
+ var addressWithSummary = document.Components.Schemas["AddressWithSummary"];
+ Assert.Equal("An address.", addressWithSummary.Description);
+
+ var addressWithoutSummary = document.Components.Schemas["AddressWithoutSummary"];
+ Assert.Null(addressWithoutSummary.Description);
+ });
+ }
+
+ [Fact]
+ public async Task XmlCommentsOnPropertiesShouldNotApplyToReferencedSchemas()
+ {
+ 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.
+///
+public class ModelWithSummary
+{
+ public string Street { get; set; }
+}
+
+public class ModelWithoutSummary
+{
+ public string Street { get; set; }
+}
+
+///
+/// Comment on class ModelInline.
+///
+public class ModelInline
+{
+ public string Street { get; set; }
+}
+
+///
+/// Comment on class RootModel.
+///
+public class RootModel
+{
+ ///
+ /// Comment on property FirstModelWithSummary.
+ ///
+ public ModelWithSummary FirstModelWithSummary { get; set; }
+
+ ///
+ /// Comment on property SecondModelWithSummary.
+ ///
+ public ModelWithSummary SecondModelWithSummary { get; set; }
+
+ ///
+ /// Comment on property FirstModelWithoutSummary.
+ ///
+ public ModelWithoutSummary FirstModelWithoutSummary { get; set; }
+
+ ///
+ /// Comment on property SecondModelWithoutSummary.
+ ///
+ public ModelWithoutSummary SecondModelWithoutSummary { get; set; }
+
+ ///
+ /// Comment on property FirstModelInline.
+ ///
+ public ModelInline FirstModelInline { get; set; }
+
+ ///
+ /// Comment on property SecondModelInline.
+ ///
+ public ModelInline SecondModelInline { 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);
+
+ var modelWithoutSummary = document.Components.Schemas["ModelWithoutSummary"];
+ Assert.Null(modelWithoutSummary.Description);
+
+ Assert.DoesNotContain("ModelInline", document.Components.Schemas.Keys);
+ Assert.Equal("Comment on property FirstModelInline.", rootModelSchema.Properties["firstModelInline"].Description);
+ Assert.Equal("Comment on property SecondModelInline.", rootModelSchema.Properties["secondModelInline"].Description);
});
}
}
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 4693176aedc3..956313ed214c 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs
@@ -200,6 +200,14 @@ void OnEntryPointExit(Exception exception)
using var writer = new FormattingStreamWriter(stream, CultureInfo.InvariantCulture) { AutoFlush = true };
var targetMethod = serviceType.GetMethod("GenerateAsync", [typeof(string), typeof(TextWriter)]) ?? throw new InvalidOperationException("Could not resolve GenerateAsync method.");
targetMethod.Invoke(service, ["v1", writer]);
+
+ var openApiString = Encoding.UTF8.GetString(stream.ToArray());
+ await Verifier.Verify(openApiString)
+ .UseTextForParameters("openapi.json")
+ .UseDirectory(SkipOnHelixAttribute.OnHelix()
+ ? Path.Combine(Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"), "snapshots")
+ : "snapshots");
+
stream.Position = 0;
var (document, _) = await OpenApiDocument.LoadAsync(stream, "json");
verifyFunc(document);
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..03a02f3ae801 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
@@ -431,8 +431,22 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ // 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();
+ }
+ }
+
+ var isInlinedSchema = schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string);
+ if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
+ // Apply comments from the property
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
@@ -442,14 +456,6 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
}
}
}
- 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();
- }
- }
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..238841727ee5 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,8 +460,22 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ // 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();
+ }
+ }
+
+ var isInlinedSchema = schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string);
+ if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
+ // Apply comments from the property
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
@@ -471,14 +485,6 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
}
}
}
- 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();
- }
- }
return Task.CompletedTask;
}
}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts_openapi.json.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts_openapi.json.verified.txt
new file mode 100644
index 000000000000..a7f44be82a50
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts_openapi.json.verified.txt
@@ -0,0 +1,387 @@
+{
+ "openapi": "3.1.1",
+ "info": {
+ "title": "testhost | v1",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/todo": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/project": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Project"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/board": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BoardItem"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/project-record": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProjectRecord"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/todo-with-description": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TodoWithDescription"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/type-with-examples": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TypeWithExamples"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/external-method": {
+ "post": {
+ "tags": [
+ "Endpoints"
+ ],
+ "summary": "An external method.",
+ "parameters": [
+ {
+ "name": "name",
+ "in": "query",
+ "description": "The name of the tester. Defaults to \"Tester\".",
+ "schema": {
+ "type": "string",
+ "default": "Tester"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "BoardItem": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "description": "The identifier of the board item. Defaults to \"name\"."
+ }
+ },
+ "description": "An item on the board."
+ },
+ "Project": {
+ "required": [
+ "name",
+ "description"
+ ],
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "description": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ },
+ "description": "The project that contains Todo items."
+ },
+ "ProjectRecord": {
+ "required": [
+ "name",
+ "description"
+ ],
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "description": "The name of the project."
+ },
+ "description": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "description": "The description of the project."
+ }
+ },
+ "description": "The project that contains Todo items."
+ },
+ "Todo": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32"
+ },
+ "name": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "description": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ },
+ "description": "This is a todo item."
+ },
+ "TodoWithDescription": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "description": "The identifier of the todo.",
+ "format": "int32"
+ },
+ "name": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "description": "The name of the todo."
+ },
+ "description": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "description": "Another description of the todo."
+ }
+ }
+ },
+ "TypeWithExamples": {
+ "type": "object",
+ "properties": {
+ "booleanType": {
+ "type": "boolean",
+ "example": true
+ },
+ "integerType": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32",
+ "example": 42
+ },
+ "longType": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int64",
+ "example": 1234567890123456789
+ },
+ "doubleType": {
+ "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$",
+ "type": [
+ "number",
+ "string"
+ ],
+ "format": "double",
+ "example": 3.14
+ },
+ "floatType": {
+ "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$",
+ "type": [
+ "number",
+ "string"
+ ],
+ "format": "float",
+ "example": 3.14
+ },
+ "dateTimeType": {
+ "type": "string",
+ "format": "date-time",
+ "example": "2022-01-01T00:00:00Z"
+ },
+ "dateOnlyType": {
+ "type": "string",
+ "format": "date",
+ "example": "2022-01-01"
+ },
+ "stringType": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "example": "Hello, World!"
+ },
+ "guidType": {
+ "type": "string",
+ "format": "uuid",
+ "example": "2d8f1eac-b5c6-4e29-8c62-4d9d75ef3d3d"
+ },
+ "timeOnlyType": {
+ "type": "string",
+ "format": "time",
+ "example": "12:30:45"
+ },
+ "timeSpanType": {
+ "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$",
+ "type": "string",
+ "example": "P3DT4H5M"
+ },
+ "byteType": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "uint8",
+ "example": 255
+ },
+ "decimalType": {
+ "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
+ "type": [
+ "number",
+ "string"
+ ],
+ "format": "double",
+ "example": 3.14159265359
+ },
+ "uriType": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "format": "uri",
+ "example": "https://example.com"
+ }
+ }
+ }
+ }
+ },
+ "tags": [
+ {
+ "name": "testhost"
+ },
+ {
+ "name": "Endpoints"
+ }
+ ]
+}
\ 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 69e1bbcab4b4..7140007d7e51 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,8 +552,22 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ // 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();
+ }
+ }
+
+ var isInlinedSchema = schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string);
+ if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
+ // Apply comments from the property
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
@@ -563,14 +577,6 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
}
}
}
- 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();
- }
- }
return Task.CompletedTask;
}
}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas_openapi.json.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas_openapi.json.verified.txt
new file mode 100644
index 000000000000..cf9ff5deda2f
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas_openapi.json.verified.txt
@@ -0,0 +1,517 @@
+{
+ "openapi": "3.1.1",
+ "info": {
+ "title": "testhost | v1",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/example-class": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ExampleClass"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/person": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Person"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/derived-class": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/DerivedClass"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/main-class": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MainClass"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/test-interface": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ITestInterface"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/implementing-class": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ImplementingClass"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/inherit-only-returns": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InheritOnlyReturns"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/inherit-all-but-remarks": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InheritAllButRemarks"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/generic-class": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/GenericClassOfstring"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/generic-parent": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/GenericParent"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/params-and-param-refs": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ParamsAndParamRefs"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "AggregateException": {
+ "type": [
+ "null",
+ "object"
+ ],
+ "properties": {
+ "innerExceptions": {
+ "type": [
+ "null",
+ "array"
+ ],
+ "items": {
+ "$ref": "#/components/schemas/Exception"
+ }
+ },
+ "message": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "targetSite": {
+ "$ref": "#/components/schemas/MethodBase"
+ },
+ "data": {
+ "type": [
+ "null",
+ "object"
+ ]
+ },
+ "innerException": {
+ "$ref": "#/components/schemas/Exception"
+ },
+ "helpLink": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "source": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "hResult": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32"
+ },
+ "stackTrace": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ }
+ },
+ "DerivedClass": {
+ "type": "object",
+ "description": "A summary about this class."
+ },
+ "ExampleClass": {
+ "type": "object",
+ "properties": {
+ "label": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "description": "The `Label` property represents a label\r\nfor this instance."
+ }
+ },
+ "description": "Every class and member should have a one sentence\r\nsummary describing its purpose."
+ },
+ "Exception": {
+ "type": [
+ "null",
+ "object"
+ ],
+ "properties": {
+ "targetSite": {
+ "$ref": "#/components/schemas/MethodBase"
+ },
+ "message": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "data": {
+ "$ref": "#/components/schemas/GenericParent/properties/taskOfTupleProp/properties/exception/properties/innerExceptions/items/properties/data"
+ },
+ "innerException": {
+ "$ref": "#/components/schemas/Exception"
+ },
+ "helpLink": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "source": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "hResult": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32"
+ },
+ "stackTrace": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ }
+ },
+ "GenericClassOfstring": {
+ "type": "object",
+ "description": "This is a generic class."
+ },
+ "GenericParent": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "null",
+ "integer",
+ "string"
+ ],
+ "description": "This property is a nullable value type.",
+ "format": "int32"
+ },
+ "name": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "description": "This property is a nullable reference type."
+ },
+ "taskOfTupleProp": {
+ "$ref": "#/components/schemas/TaskOfValueTupleOfintAndstring"
+ },
+ "tupleWithGenericProp": {
+ "$ref": "#/components/schemas/ValueTupleOfintAndDictionaryOfintAndstring"
+ },
+ "tupleWithNestedGenericProp": {
+ "$ref": "#/components/schemas/ValueTupleOfintAndDictionaryOfintAndDictionaryOfstringAndint"
+ }
+ },
+ "description": "This class validates the behavior for mapping\r\ngeneric types to open generics for use in\r\ntypeof expressions."
+ },
+ "ImplementingClass": {
+ "type": "object",
+ "description": "This interface would describe all the methods in\r\nits contract."
+ },
+ "InheritAllButRemarks": {
+ "type": "object",
+ "description": "This class shows an example of sharing comments across methods."
+ },
+ "InheritOnlyReturns": {
+ "type": "object",
+ "description": "This class shows hows you can \"inherit\" the doc\r\ncomments from one method in another method."
+ },
+ "ITestInterface": {
+ "type": "object",
+ "description": "This interface would describe all the methods in\r\nits contract."
+ },
+ "MainClass": {
+ "type": "object",
+ "description": "A summary about this class."
+ },
+ "MethodBase": { },
+ "ParamsAndParamRefs": {
+ "type": "object",
+ "description": "This shows examples of typeparamref and typeparam tags"
+ },
+ "Person": {
+ "required": [
+ "firstName",
+ "lastName"
+ ],
+ "type": "object",
+ "properties": {
+ "firstName": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "description": "This tag will apply to the primary constructor parameter."
+ },
+ "lastName": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "description": "This tag will apply to the primary constructor parameter."
+ }
+ },
+ "description": "This is an example of a positional record."
+ },
+ "TaskCreationOptions": {
+ "type": "integer"
+ },
+ "TaskOfValueTupleOfintAndstring": {
+ "type": [
+ "null",
+ "object"
+ ],
+ "properties": {
+ "result": {
+ "$ref": "#/components/schemas/ValueTupleOfintAndstring"
+ },
+ "id": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32"
+ },
+ "exception": {
+ "$ref": "#/components/schemas/AggregateException"
+ },
+ "status": {
+ "$ref": "#/components/schemas/TaskStatus"
+ },
+ "isCanceled": {
+ "type": "boolean"
+ },
+ "isCompleted": {
+ "type": "boolean"
+ },
+ "isCompletedSuccessfully": {
+ "type": "boolean"
+ },
+ "creationOptions": {
+ "$ref": "#/components/schemas/TaskCreationOptions"
+ },
+ "asyncState": { },
+ "isFaulted": {
+ "type": "boolean"
+ }
+ }
+ },
+ "TaskStatus": {
+ "type": "integer"
+ },
+ "ValueTupleOfintAndDictionaryOfintAndDictionaryOfstringAndint": {
+ "type": "object"
+ },
+ "ValueTupleOfintAndDictionaryOfintAndstring": {
+ "type": "object"
+ },
+ "ValueTupleOfintAndstring": {
+ "type": "object"
+ }
+ }
+ },
+ "tags": [
+ {
+ "name": "testhost"
+ }
+ ]
+}
\ No newline at end of file
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..3d4e20a30388 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,8 +435,22 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ // 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();
+ }
+ }
+
+ var isInlinedSchema = schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string);
+ if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
+ // Apply comments from the property
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
@@ -446,14 +460,6 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
}
}
}
- 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();
- }
- }
return Task.CompletedTask;
}
}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers_openapi.json.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers_openapi.json.verified.txt
new file mode 100644
index 000000000000..593d74fa4008
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers_openapi.json.verified.txt
@@ -0,0 +1,210 @@
+{
+ "openapi": "3.1.1",
+ "info": {
+ "title": "testhost | v1",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/Test": {
+ "get": {
+ "tags": [
+ "Test"
+ ],
+ "summary": "A summary of the action.",
+ "description": "A description of the action.",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "application/json": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "text/json": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/Test2": {
+ "get": {
+ "tags": [
+ "Test2"
+ ],
+ "parameters": [
+ {
+ "name": "name",
+ "in": "query",
+ "description": "The name of the person.",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns the greeting.",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "application/json": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "text/json": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "Test2"
+ ],
+ "requestBody": {
+ "description": "The todo to insert into the database.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ },
+ "text/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ },
+ "application/*+json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "application/json": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "text/json": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/Test2/HelloByInt": {
+ "get": {
+ "tags": [
+ "Test2"
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "query",
+ "description": "The id associated with the request.",
+ "schema": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "application/json": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "text/json": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "Todo": {
+ "required": [
+ "id",
+ "title",
+ "completed"
+ ],
+ "type": "object",
+ "properties": {
+ "id": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32"
+ },
+ "title": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "completed": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "tags": [
+ {
+ "name": "Test"
+ },
+ {
+ "name": "Test2"
+ }
+ ]
+}
\ No newline at end of file
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..36d7783f8dd2 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,8 +453,22 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ // 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();
+ }
+ }
+
+ var isInlinedSchema = schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string);
+ if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
+ // Apply comments from the property
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
@@ -464,14 +478,6 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
}
}
}
- 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();
- }
- }
return Task.CompletedTask;
}
}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis_openapi.json.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis_openapi.json.verified.txt
new file mode 100644
index 000000000000..1cb9a70236ed
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis_openapi.json.verified.txt
@@ -0,0 +1,604 @@
+{
+ "openapi": "3.1.1",
+ "info": {
+ "title": "testhost | v1",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/1": {
+ "get": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "summary": "A summary of the action.",
+ "description": "A description of the action.",
+ "responses": {
+ "200": {
+ "description": "Returns the greeting.",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/2": {
+ "get": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "parameters": [
+ {
+ "name": "name",
+ "in": "query",
+ "description": "The name of the person.",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns the greeting.",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/3": {
+ "get": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "parameters": [
+ {
+ "name": "name",
+ "in": "query",
+ "description": "The name of the person.",
+ "schema": {
+ "type": "string"
+ },
+ "example": "Testy McTester"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns the greeting.",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/4": {
+ "get": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "responses": {
+ "404": {
+ "description": "Indicates that the value was not found.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/5": {
+ "get": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "responses": {
+ "404": {
+ "description": "Indicates that the value was not found.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "Indicates that the value is even.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "201": {
+ "description": "Indicates that the value is less than 50."
+ }
+ }
+ }
+ },
+ "/6": {
+ "post": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "summary": "Creates a new user.",
+ "description": "Sample request:\r\n POST /6\r\n {\r\n \"username\": \"johndoe\",\r\n \"email\": \"john@example.com\"\r\n }",
+ "requestBody": {
+ "description": "The user information.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ },
+ "example": {
+ "username": "johndoe",
+ "email": "john@example.com"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/7": {
+ "put": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "summary": "Updates an existing record.",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "query",
+ "description": "Legacy ID parameter - use uuid instead.",
+ "deprecated": true,
+ "schema": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32"
+ }
+ },
+ {
+ "name": "uuid",
+ "in": "query",
+ "description": "Unique identifier for the record.",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/8": {
+ "get": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "summary": "A summary of Get8.",
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/9": {
+ "get": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "summary": "A summary of Get9.",
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/10": {
+ "get": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "summary": "A summary of Get10.",
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/11": {
+ "get": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "summary": "A summary of Get11.",
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/12": {
+ "get": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "summary": "A summary of Get12.",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/13": {
+ "get": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "summary": "A summary of Get13.",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/14": {
+ "get": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "summary": "A summary of Get14.",
+ "responses": {
+ "200": {
+ "description": "Returns the greeting.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HolderOfstring"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/15": {
+ "get": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "summary": "A summary of Get15.",
+ "responses": {
+ "200": {
+ "description": "Returns the greeting.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HolderOfstring"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/16": {
+ "post": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "summary": "A summary of Post16.",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Example"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/17": {
+ "get": {
+ "tags": [
+ "RouteHandlerExtensionMethods"
+ ],
+ "summary": "A summary of Get17.",
+ "parameters": [
+ {
+ "name": "args",
+ "in": "query",
+ "schema": {
+ "type": "array",
+ "items": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32"
+ }
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "AggregateException": {
+ "type": [
+ "null",
+ "object"
+ ],
+ "properties": {
+ "innerExceptions": {
+ "type": [
+ "null",
+ "array"
+ ],
+ "items": {
+ "$ref": "#/components/schemas/Exception"
+ }
+ },
+ "message": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "targetSite": {
+ "$ref": "#/components/schemas/MethodBase"
+ },
+ "data": {
+ "type": [
+ "null",
+ "object"
+ ]
+ },
+ "innerException": {
+ "$ref": "#/components/schemas/Exception"
+ },
+ "helpLink": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "source": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "hResult": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32"
+ },
+ "stackTrace": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ }
+ },
+ "Example": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32"
+ },
+ "id": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32"
+ },
+ "exception": {
+ "$ref": "#/components/schemas/AggregateException"
+ },
+ "status": {
+ "$ref": "#/components/schemas/TaskStatus"
+ },
+ "isCanceled": {
+ "type": "boolean"
+ },
+ "isCompleted": {
+ "type": "boolean"
+ },
+ "isCompletedSuccessfully": {
+ "type": "boolean"
+ },
+ "creationOptions": {
+ "$ref": "#/components/schemas/TaskCreationOptions"
+ },
+ "asyncState": { },
+ "isFaulted": {
+ "type": "boolean"
+ }
+ }
+ },
+ "Exception": {
+ "type": [
+ "null",
+ "object"
+ ],
+ "properties": {
+ "targetSite": {
+ "$ref": "#/components/schemas/MethodBase"
+ },
+ "message": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "data": {
+ "$ref": "#/components/schemas/Example/properties/exception/properties/innerExceptions/items/properties/data"
+ },
+ "innerException": {
+ "$ref": "#/components/schemas/Exception"
+ },
+ "helpLink": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "source": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "hResult": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32"
+ },
+ "stackTrace": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ }
+ },
+ "HolderOfstring": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ }
+ },
+ "MethodBase": { },
+ "TaskCreationOptions": {
+ "type": "integer"
+ },
+ "TaskStatus": {
+ "type": "integer"
+ },
+ "User": {
+ "type": "object",
+ "properties": {
+ "username": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "email": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "tags": [
+ {
+ "name": "RouteHandlerExtensionMethods"
+ }
+ ]
+}
\ 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.verified.cs
index 33e3e561e958..08204c622668 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
@@ -76,6 +76,8 @@ private static Dictionary GenerateCacheEntries()
cache.Add(@"T:ProjectBoard.ProtectedInternalElement", new XmlComment(@"Can find this XML comment.", null, null, null, null, false, null, null, null));
cache.Add(@"T:ProjectRecord", new XmlComment(@"The project that contains Todo items.", null, null, null, null, false, null, [new XmlParameterComment(@"Name", @"The name of the project.", null, false), new XmlParameterComment(@"Description", @"The description of the project.", null, false)], null));
cache.Add(@"T:User", new XmlComment(null, null, null, null, null, false, null, null, null));
+ cache.Add(@"T:AddressWithSummary", new XmlComment(@"An address.", null, null, null, null, false, null, null, null));
+ cache.Add(@"T:AddressNested", new XmlComment(@"An address.", null, null, null, null, false, null, null, null));
cache.Add(@"P:ProjectBoard.ProtectedInternalElement.Name", new XmlComment(@"The unique identifier for the element.", null, null, null, null, false, null, null, null));
cache.Add(@"P:ProjectRecord.Name", new XmlComment(@"The name of the project.", null, null, null, null, false, null, null, null));
cache.Add(@"P:ProjectRecord.Description", new XmlComment(@"The description of the project.", null, null, null, null, false, null, null, null));
@@ -100,6 +102,12 @@ private static Dictionary GenerateCacheEntries()
cache.Add(@"P:IUser.Name", new XmlComment(@"The user's display name.", null, null, null, null, false, null, null, null));
cache.Add(@"P:User.Id", new XmlComment(@"The unique identifier for the user.", null, null, null, null, false, null, null, null));
cache.Add(@"P:User.Name", new XmlComment(@"The user's display name.", null, null, null, null, false, null, null, null));
+ cache.Add(@"P:Company.BillingAddressClassWithSummary", new XmlComment(@"Billing address.", null, null, null, null, false, null, null, null));
+ cache.Add(@"P:Company.BillingAddressClassWithoutSummary", new XmlComment(@"Billing address.", null, null, null, null, false, null, null, null));
+ cache.Add(@"P:Company.BillingAddressNested", new XmlComment(@"Billing address.", null, null, null, null, false, null, null, null));
+ cache.Add(@"P:Company.VisitingAddressClassWithSummary", new XmlComment(@"Visiting address.", null, null, null, null, false, null, null, null));
+ cache.Add(@"P:Company.VisitingAddressClassWithoutSummary", new XmlComment(@"Visiting address.", null, null, null, null, false, null, null, null));
+ cache.Add(@"P:Company.VisitingAddressNested", new XmlComment(@"Visiting address.", null, null, null, null, false, null, null, null));
return cache;
}
@@ -461,8 +469,22 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ // 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();
+ }
+ }
+
+ var isInlinedSchema = schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string);
+ if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
+ // Apply comments from the property
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
@@ -472,14 +494,6 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
}
}
}
- 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();
- }
- }
return Task.CompletedTask;
}
}
@@ -517,12 +531,13 @@ file static class JsonNodeExtensions
file static class GeneratedServiceCollectionExtensions
{
[InterceptsLocation]
- public static IServiceCollection AddOpenApi(this IServiceCollection services)
+ 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/SchemaTests.SupportsXmlCommentsOnSchemas_openapi.json.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas_openapi.json.verified.txt
new file mode 100644
index 000000000000..3c7877f96ec0
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas_openapi.json.verified.txt
@@ -0,0 +1,532 @@
+{
+ "openapi": "3.1.1",
+ "info": {
+ "title": "testhost | v1",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/todo": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/project": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Project"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/board": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BoardItem"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/protected-internal-element": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProtectedInternalElement"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/project-record": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProjectRecord"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/todo-with-description": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TodoWithDescription"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/type-with-examples": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TypeWithExamples"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/user": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/company": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Company"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "AddressWithoutSummary": {
+ "type": [
+ "null",
+ "object"
+ ],
+ "properties": {
+ "street": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ }
+ },
+ "AddressWithSummary": {
+ "type": [
+ "null",
+ "object"
+ ],
+ "properties": {
+ "street": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ },
+ "description": "An address."
+ },
+ "BoardItem": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ },
+ "description": "An item on the board."
+ },
+ "Company": {
+ "type": "object",
+ "properties": {
+ "billingAddressClassWithSummary": {
+ "$ref": "#/components/schemas/AddressWithSummary"
+ },
+ "billingAddressClassWithoutSummary": {
+ "$ref": "#/components/schemas/AddressWithoutSummary"
+ },
+ "billingAddressNested": {
+ "type": [
+ "null",
+ "object"
+ ],
+ "properties": {
+ "street": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ },
+ "description": "Billing address."
+ },
+ "visitingAddressClassWithSummary": {
+ "$ref": "#/components/schemas/AddressWithSummary"
+ },
+ "visitingAddressClassWithoutSummary": {
+ "$ref": "#/components/schemas/AddressWithoutSummary"
+ },
+ "visitingAddressNested": {
+ "type": [
+ "null",
+ "object"
+ ],
+ "properties": {
+ "street": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ },
+ "description": "Visiting address."
+ }
+ }
+ },
+ "Project": {
+ "required": [
+ "name",
+ "description"
+ ],
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "description": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ },
+ "description": "The project that contains Todo items."
+ },
+ "ProjectRecord": {
+ "required": [
+ "name",
+ "description"
+ ],
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "description": "The name of the project."
+ },
+ "description": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "description": "The description of the project."
+ }
+ },
+ "description": "The project that contains Todo items."
+ },
+ "ProtectedInternalElement": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "description": "The unique identifier for the element."
+ }
+ },
+ "description": "Can find this XML comment."
+ },
+ "Todo": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32"
+ },
+ "name": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "description": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ },
+ "description": "This is a todo item."
+ },
+ "TodoWithDescription": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "description": "The identifier of the todo.",
+ "format": "int32"
+ },
+ "name": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "description": "The name of the todo."
+ },
+ "description": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "description": "Another description of the todo."
+ }
+ }
+ },
+ "TypeWithExamples": {
+ "type": "object",
+ "properties": {
+ "booleanType": {
+ "type": "boolean",
+ "example": true
+ },
+ "integerType": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32",
+ "example": 42
+ },
+ "longType": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int64",
+ "example": 1234567890123456789
+ },
+ "doubleType": {
+ "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$",
+ "type": [
+ "number",
+ "string"
+ ],
+ "format": "double",
+ "example": 3.14
+ },
+ "floatType": {
+ "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$",
+ "type": [
+ "number",
+ "string"
+ ],
+ "format": "float",
+ "example": 3.14
+ },
+ "dateTimeType": {
+ "type": "string",
+ "format": "date-time",
+ "example": "2022-01-01T00:00:00Z"
+ },
+ "dateOnlyType": {
+ "type": "string",
+ "format": "date",
+ "example": "2022-01-01"
+ },
+ "stringType": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "example": "Hello, World!"
+ },
+ "guidType": {
+ "type": "string",
+ "format": "uuid",
+ "example": "2d8f1eac-b5c6-4e29-8c62-4d9d75ef3d3d"
+ },
+ "timeOnlyType": {
+ "type": "string",
+ "format": "time",
+ "example": "12:30:45"
+ },
+ "timeSpanType": {
+ "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$",
+ "type": "string",
+ "example": "P3DT4H5M"
+ },
+ "byteType": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "uint8",
+ "example": 255
+ },
+ "decimalType": {
+ "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
+ "type": [
+ "number",
+ "string"
+ ],
+ "format": "double",
+ "example": 3.14159265359
+ },
+ "uriType": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "format": "uri",
+ "example": "https://example.com"
+ }
+ }
+ },
+ "User": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "description": "The unique identifier for the user.",
+ "format": "int32"
+ },
+ "name": {
+ "type": [
+ "null",
+ "string"
+ ],
+ "description": "The user's display name."
+ }
+ }
+ }
+ }
+ },
+ "tags": [
+ {
+ "name": "testhost"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.XmlCommentsOnPropertiesShouldNotApplyToReferencedSchemas#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.XmlCommentsOnPropertiesShouldNotApplyToReferencedSchemas#OpenApiXmlCommentSupport.generated.verified.cs
new file mode 100644
index 000000000000..50f490a82aa4
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.XmlCommentsOnPropertiesShouldNotApplyToReferencedSchemas#OpenApiXmlCommentSupport.generated.verified.cs
@@ -0,0 +1,527 @@
+//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, null, null, null));
+ cache.Add(@"T:ModelInline", new XmlComment(@"Comment on class ModelInline.", null, null, null, null, false, null, null, null));
+ cache.Add(@"T:RootModel", new XmlComment(@"Comment on class RootModel.", null, null, null, null, false, null, null, null));
+ cache.Add(@"P:RootModel.FirstModelWithSummary", new XmlComment(@"Comment on property FirstModelWithSummary.", null, null, null, null, false, null, null, null));
+ cache.Add(@"P:RootModel.SecondModelWithSummary", new XmlComment(@"Comment on property SecondModelWithSummary.", null, null, null, null, false, null, null, null));
+ cache.Add(@"P:RootModel.FirstModelWithoutSummary", new XmlComment(@"Comment on property FirstModelWithoutSummary.", null, null, null, null, false, null, null, null));
+ cache.Add(@"P:RootModel.SecondModelWithoutSummary", new XmlComment(@"Comment on property SecondModelWithoutSummary.", null, null, null, null, false, null, null, null));
+ cache.Add(@"P:RootModel.FirstModelInline", new XmlComment(@"Comment on property FirstModelInline.", null, null, null, null, false, null, null, null));
+ cache.Add(@"P:RootModel.SecondModelInline", new XmlComment(@"Comment on property SecondModelInline.", null, null, null, null, false, null, 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))
+ {
+ var isInlinedSchema = schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string);
+ if(isInlinedSchema)
+ {
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ else
+ {
+ 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/SchemaTests.XmlCommentsOnPropertiesShouldNotApplyToReferencedSchemas_openapi.json.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.XmlCommentsOnPropertiesShouldNotApplyToReferencedSchemas_openapi.json.verified.txt
new file mode 100644
index 000000000000..6a784a54cf0c
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.XmlCommentsOnPropertiesShouldNotApplyToReferencedSchemas_openapi.json.verified.txt
@@ -0,0 +1,120 @@
+{
+ "openapi": "3.1.1",
+ "info": {
+ "title": "testhost | v1",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/example": {
+ "post": {
+ "tags": [
+ "testhost"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RootModel"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "ModelWithoutSummary": {
+ "type": [
+ "null",
+ "object"
+ ],
+ "properties": {
+ "street": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ }
+ },
+ "ModelWithSummary": {
+ "type": [
+ "null",
+ "object"
+ ],
+ "properties": {
+ "street": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ },
+ "description": "Comment on class ModelWithSummary."
+ },
+ "RootModel": {
+ "type": "object",
+ "properties": {
+ "firstModelWithSummary": {
+ "description": "Comment on property FirstModelWithSummary.",
+ "$ref": "#/components/schemas/ModelWithSummary"
+ },
+ "secondModelWithSummary": {
+ "description": "Comment on property SecondModelWithSummary.",
+ "$ref": "#/components/schemas/ModelWithSummary"
+ },
+ "firstModelWithoutSummary": {
+ "description": "Comment on property FirstModelWithoutSummary.",
+ "$ref": "#/components/schemas/ModelWithoutSummary"
+ },
+ "secondModelWithoutSummary": {
+ "description": "Comment on property SecondModelWithoutSummary.",
+ "$ref": "#/components/schemas/ModelWithoutSummary"
+ },
+ "firstModelInline": {
+ "type": [
+ "null",
+ "object"
+ ],
+ "properties": {
+ "street": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ },
+ "description": "Comment on property FirstModelInline."
+ },
+ "secondModelInline": {
+ "type": [
+ "null",
+ "object"
+ ],
+ "properties": {
+ "street": {
+ "type": [
+ "null",
+ "string"
+ ]
+ }
+ },
+ "description": "Comment on property SecondModelInline."
+ }
+ },
+ "description": "Comment on class RootModel."
+ }
+ }
+ },
+ "tags": [
+ {
+ "name": "testhost"
+ }
+ ]
+}
\ No newline at end of file
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..105a54d461e8 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,8 +432,22 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
- if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
+ // 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();
+ }
+ }
+
+ var isInlinedSchema = schema.Metadata is null
+ || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
+ || string.IsNullOrEmpty(schemaId as string);
+ if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
+ // Apply comments from the property
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
@@ -443,14 +457,6 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
}
}
}
- 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();
- }
- }
return Task.CompletedTask;
}
}
@@ -498,4 +504,4 @@ public static IServiceCollection AddOpenApi(this IServiceCollection services)
}
}
-}
\ No newline at end of file
+}
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats_openapi.json.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats_openapi.json.verified.txt
new file mode 100644
index 000000000000..b252dafd093f
--- /dev/null
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats_openapi.json.verified.txt
@@ -0,0 +1,43 @@
+{
+ "openapi": "3.1.1",
+ "info": {
+ "title": "testhost | v1",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/test-method": {
+ "post": {
+ "tags": [
+ "TestApi"
+ ],
+ "summary": "This method should have its XML comment merged properly.",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "query",
+ "description": "The identifier for the test.",
+ "required": true,
+ "schema": {
+ "pattern": "^-?(?:0|[1-9]\\d*)$",
+ "type": [
+ "integer",
+ "string"
+ ],
+ "format": "int32"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A task representing the asynchronous operation."
+ }
+ }
+ }
+ }
+ },
+ "tags": [
+ {
+ "name": "TestApi"
+ }
+ ]
+}
\ No newline at end of file