From cfab5dc03583ca2275f10bc9fc96597064f9683f Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Mon, 2 Jun 2025 14:31:19 +0200 Subject: [PATCH 1/7] Update OpenAPI range formatting to format using the target culture and Update tests to write in the InvariantCulture --- .../SchemaTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..61194e27fbbc 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; From 7595507dfba9e9bb82718180a1dc861a87a72224 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Mon, 2 Jun 2025 17:00:02 +0200 Subject: [PATCH 2/7] Updated schema reference XML Comment handling. Update handling of nested schemas and referenced schemas --- .../Extensions/OpenApiDocumentExtensions.cs | 5 +- .../SchemaTests.cs | 75 ++++++++++++++++++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs b/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs index c53465f8226c..fc96ad196397 100644 --- a/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs @@ -21,6 +21,9 @@ 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); + return new OpenApiSchemaReference(schemaId, document) + { + Description = schema.Description, + }; } } 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 61194e27fbbc..1f8065c1a756 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs @@ -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,21 @@ 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["billingAddressClassWithSummary"].Description); + Assert.Equal("Billing address.", company.Properties["billingAddressClassWithoutSummary"].Description); + Assert.Equal("Billing address.", company.Properties["billingAddressNested"].Description); + Assert.Equal("Visiting address.", company.Properties["visitingAddressClassWithSummary"].Description); + Assert.Equal("Visiting address.", company.Properties["visitingAddressClassWithoutSummary"].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(addressWithSummary.Description); }); } } From 8eb2713cc44d924ad0c86f39ed484d5cfd53c868 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Thu, 31 Jul 2025 16:37:59 +0200 Subject: [PATCH 3/7] Only set a schema property description when it's not a schema reference Update XmlCommentGenerator.Emitter.cs to skip applying property descriptions when it's a schema reference. Schema transformers get the full schema and not the schema reference. So when the description is updated it would update it for all locations. --- .../gen/XmlCommentGenerator.Emitter.cs | 25 +- .../Extensions/OpenApiDocumentExtensions.cs | 5 +- .../CompletenessTests.cs | 6 +- .../SchemaTests.cs | 107 +++- ...ApiXmlCommentSupport.generated.verified.cs | 24 +- ...ApiXmlCommentSupport.generated.verified.cs | 24 +- ...ApiXmlCommentSupport.generated.verified.cs | 24 +- ...ApiXmlCommentSupport.generated.verified.cs | 24 +- ...ApiXmlCommentSupport.generated.verified.cs | 24 +- ...ApiXmlCommentSupport.generated.verified.cs | 35 +- ...ApiXmlCommentSupport.generated.verified.cs | 516 ++++++++++++++++++ ...ApiXmlCommentSupport.generated.verified.cs | 26 +- 12 files changed, 754 insertions(+), 86 deletions(-) create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.XmlCommentsOnPropertiesShouldNotApplyToReferencedSchemas#OpenApiXmlCommentSupport.generated.verified.cs diff --git a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs index cf8bb5e875eb..268dce20983c 100644 --- a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs +++ b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs @@ -449,8 +449,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 isPropertyInlinedSchema = schema.Metadata is null + || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) + || string.IsNullOrEmpty(schemaId as string); + if (isPropertyInlinedSchema && 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; @@ -460,14 +474,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; } } @@ -507,6 +513,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 fc96ad196397..c53465f8226c 100644 --- a/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs @@ -21,9 +21,6 @@ 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) - { - Description = schema.Description, - }; + return new OpenApiSchemaReference(schemaId, document); } } 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..020b6ec61382 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,9 @@ 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); + //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 1f8065c1a756..d99c0e7f938f 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs @@ -319,18 +319,115 @@ await SnapshotTestHelper.VerifyOpenApi(compilation, document => path = document.Paths["/company"].Operations[HttpMethod.Post]; var company = path.RequestBody.Content["application/json"].Schema; - Assert.Equal("Billing address.", company.Properties["billingAddressClassWithSummary"].Description); - Assert.Equal("Billing address.", company.Properties["billingAddressClassWithoutSummary"].Description); Assert.Equal("Billing address.", company.Properties["billingAddressNested"].Description); - Assert.Equal("Visiting address.", company.Properties["visitingAddressClassWithSummary"].Description); - Assert.Equal("Visiting address.", company.Properties["visitingAddressClassWithoutSummary"].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(addressWithSummary.Description); + 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/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..71ca6f5d3fc5 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 isPropertyInlinedSchema = schema.Metadata is null + || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) + || string.IsNullOrEmpty(schemaId as string); + if (isPropertyInlinedSchema && 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..37157431f9dc 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 isPropertyInlinedSchema = schema.Metadata is null + || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) + || string.IsNullOrEmpty(schemaId as string); + if (isPropertyInlinedSchema && 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/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs index 69e1bbcab4b4..050c5a8d9b91 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 isPropertyInlinedSchema = schema.Metadata is null + || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) + || string.IsNullOrEmpty(schemaId as string); + if (isPropertyInlinedSchema && 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/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers#OpenApiXmlCommentSupport.generated.verified.cs index 4baa534fd17c..2e7a60909b32 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 isPropertyInlinedSchema = schema.Metadata is null + || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) + || string.IsNullOrEmpty(schemaId as string); + if (isPropertyInlinedSchema && 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.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis#OpenApiXmlCommentSupport.generated.verified.cs index fc1fbec8ab34..e448edfb42e6 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 isPropertyInlinedSchema = schema.Metadata is null + || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) + || string.IsNullOrEmpty(schemaId as string); + if (isPropertyInlinedSchema && 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/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs index 33e3e561e958..11c278ff80c0 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 isPropertyInlinedSchema = schema.Metadata is null + || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) + || string.IsNullOrEmpty(schemaId as string); + if (isPropertyInlinedSchema && 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.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..694d548d3cc0 --- /dev/null +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.XmlCommentsOnPropertiesShouldNotApplyToReferencedSchemas#OpenApiXmlCommentSupport.generated.verified.cs @@ -0,0 +1,516 @@ +//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(); + } + } + + var isPropertyInlinedSchema = schema.Metadata is null + || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) + || string.IsNullOrEmpty(schemaId as string); + if (isPropertyInlinedSchema && 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; + if (propertyComment.Examples?.FirstOrDefault() is { } jsonString) + { + schema.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..e2ccda84dcc0 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 isPropertyInlinedSchema = schema.Metadata is null + || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) + || string.IsNullOrEmpty(schemaId as string); + if (isPropertyInlinedSchema && 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 +} From d98b2bcd7cd14caf2aa0a82e27e1bb1cec0ba126 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Thu, 31 Jul 2025 16:38:35 +0200 Subject: [PATCH 4/7] Snapshot the generated openapi as part of the OpenApi source generator tests --- .../SnapshotTestHelper.cs | 8 + ...nAdditionalTexts_openapi.json.verified.txt | 387 +++++++++++ ...XmlTagsOnSchemas_openapi.json.verified.txt | 517 +++++++++++++++ ...sFromControllers_openapi.json.verified.txt | 210 ++++++ ...sFromMinimalApis_openapi.json.verified.txt | 604 ++++++++++++++++++ ...ommentsOnSchemas_openapi.json.verified.txt | 532 +++++++++++++++ ...eferencedSchemas_openapi.json.verified.txt | 116 ++++ ...ntationIdFormats_openapi.json.verified.txt | 43 ++ 8 files changed, 2417 insertions(+) create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/AdditionalTextsTests.CanHandleXmlForSchemasInAdditionalTexts_openapi.json.verified.txt create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/CompletenessTests.SupportsAllXmlTagsOnSchemas_openapi.json.verified.txt create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromControllers_openapi.json.verified.txt create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/OperationTests.SupportsXmlCommentsOnOperationsFromMinimalApis_openapi.json.verified.txt create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.SupportsXmlCommentsOnSchemas_openapi.json.verified.txt create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.XmlCommentsOnPropertiesShouldNotApplyToReferencedSchemas_openapi.json.verified.txt create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats_openapi.json.verified.txt 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/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_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_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_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_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_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..9f640c47e8e9 --- /dev/null +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.XmlCommentsOnPropertiesShouldNotApplyToReferencedSchemas_openapi.json.verified.txt @@ -0,0 +1,116 @@ +{ + "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": { + "$ref": "#/components/schemas/ModelWithSummary" + }, + "secondModelWithSummary": { + "$ref": "#/components/schemas/ModelWithSummary" + }, + "firstModelWithoutSummary": { + "$ref": "#/components/schemas/ModelWithoutSummary" + }, + "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_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 From b895d2d07d82ac76ec2ab838381c6f6b13e73786 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Thu, 31 Jul 2025 17:02:11 +0200 Subject: [PATCH 5/7] Uncomment and remove commented code --- .../CompletenessTests.cs | 3 --- .../SchemaTests.cs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) 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 020b6ec61382..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 d99c0e7f938f..0f03c8ec9dba 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs @@ -425,7 +425,7 @@ await SnapshotTestHelper.VerifyOpenApi(compilation, document => var modelWithoutSummary = document.Components.Schemas["ModelWithoutSummary"]; Assert.Null(modelWithoutSummary.Description); - //Assert.DoesNotContain("ModelInline", document.Components.Schemas.Keys); + 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); }); From 7b131bb1b951edec8a54a5b0fd3fd92158b48a50 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Thu, 31 Jul 2025 17:16:51 +0200 Subject: [PATCH 6/7] Rename emitted variable to isInlinedSchema --- src/OpenApi/gen/XmlCommentGenerator.Emitter.cs | 4 ++-- ...tAddOpenApi#OpenApiXmlCommentSupport.generated.verified.cs | 4 ++-- ...tionalTexts#OpenApiXmlCommentSupport.generated.verified.cs | 4 ++-- ...gsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs | 4 ++-- ...Controllers#OpenApiXmlCommentSupport.generated.verified.cs | 4 ++-- ...MinimalApis#OpenApiXmlCommentSupport.generated.verified.cs | 4 ++-- ...tsOnSchemas#OpenApiXmlCommentSupport.generated.verified.cs | 4 ++-- ...ncedSchemas#OpenApiXmlCommentSupport.generated.verified.cs | 4 ++-- ...onIdFormats#OpenApiXmlCommentSupport.generated.verified.cs | 4 ++-- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs index 268dce20983c..5b9bc6b48893 100644 --- a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs +++ b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs @@ -459,10 +459,10 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext } } - var isPropertyInlinedSchema = schema.Metadata is null + var isInlinedSchema = schema.Metadata is null || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) || string.IsNullOrEmpty(schemaId as string); - if (isPropertyInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) + if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) { // Apply comments from the property if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment)) 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 71ca6f5d3fc5..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 @@ -441,10 +441,10 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext } } - var isPropertyInlinedSchema = schema.Metadata is null + var isInlinedSchema = schema.Metadata is null || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) || string.IsNullOrEmpty(schemaId as string); - if (isPropertyInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) + if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) { // Apply comments from the property if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment)) 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 37157431f9dc..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 @@ -470,10 +470,10 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext } } - var isPropertyInlinedSchema = schema.Metadata is null + var isInlinedSchema = schema.Metadata is null || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) || string.IsNullOrEmpty(schemaId as string); - if (isPropertyInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) + if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) { // Apply comments from the property if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment)) 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 050c5a8d9b91..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 @@ -562,10 +562,10 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext } } - var isPropertyInlinedSchema = schema.Metadata is null + var isInlinedSchema = schema.Metadata is null || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) || string.IsNullOrEmpty(schemaId as string); - if (isPropertyInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) + if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) { // Apply comments from the property if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment)) 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 2e7a60909b32..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 @@ -445,10 +445,10 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext } } - var isPropertyInlinedSchema = schema.Metadata is null + var isInlinedSchema = schema.Metadata is null || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) || string.IsNullOrEmpty(schemaId as string); - if (isPropertyInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) + if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) { // Apply comments from the property if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment)) 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 e448edfb42e6..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 @@ -463,10 +463,10 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext } } - var isPropertyInlinedSchema = schema.Metadata is null + var isInlinedSchema = schema.Metadata is null || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) || string.IsNullOrEmpty(schemaId as string); - if (isPropertyInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) + if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) { // Apply comments from the property if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment)) 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 11c278ff80c0..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 @@ -479,10 +479,10 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext } } - var isPropertyInlinedSchema = schema.Metadata is null + var isInlinedSchema = schema.Metadata is null || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) || string.IsNullOrEmpty(schemaId as string); - if (isPropertyInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) + if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) { // Apply comments from the property if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment)) 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 index 694d548d3cc0..2e6423a035df 100644 --- 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 @@ -450,10 +450,10 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext } } - var isPropertyInlinedSchema = schema.Metadata is null + var isInlinedSchema = schema.Metadata is null || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) || string.IsNullOrEmpty(schemaId as string); - if (isPropertyInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) + if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) { // Apply comments from the property if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment)) 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 e2ccda84dcc0..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 @@ -442,10 +442,10 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext } } - var isPropertyInlinedSchema = schema.Metadata is null + var isInlinedSchema = schema.Metadata is null || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) || string.IsNullOrEmpty(schemaId as string); - if (isPropertyInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) + if (isInlinedSchema && context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) { // Apply comments from the property if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment)) From 4da2fb57bfa36af362ae416435dba9e710181f9c Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Sun, 3 Aug 2025 14:07:48 +0200 Subject: [PATCH 7/7] Openapi: Add metadata properties to be able to modify the openapi reference --- .../gen/XmlCommentGenerator.Emitter.cs | 25 +++++++++++++------ .../Extensions/OpenApiDocumentExtensions.cs | 14 ++++++++++- ...ApiXmlCommentSupport.generated.verified.cs | 25 +++++++++++++------ ...eferencedSchemas_openapi.json.verified.txt | 4 +++ 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs index 5b9bc6b48893..24e3c75b38e8 100644 --- a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs +++ b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs @@ -459,18 +459,29 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext } } - 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 }) + if (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; - if (propertyComment.Examples?.FirstOrDefault() is { } jsonString) + var isInlinedSchema = schema.Metadata is null + || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) + || string.IsNullOrEmpty(schemaId as string); + if(isInlinedSchema) { - schema.Example = jsonString.Parse(); + 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(); + } } } } 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/snapshots/SchemaTests.XmlCommentsOnPropertiesShouldNotApplyToReferencedSchemas#OpenApiXmlCommentSupport.generated.verified.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/snapshots/SchemaTests.XmlCommentsOnPropertiesShouldNotApplyToReferencedSchemas#OpenApiXmlCommentSupport.generated.verified.cs index 2e6423a035df..50f490a82aa4 100644 --- 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 @@ -450,18 +450,29 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext } } - 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 }) + if (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; - if (propertyComment.Examples?.FirstOrDefault() is { } jsonString) + var isInlinedSchema = schema.Metadata is null + || !schema.Metadata.TryGetValue("x-schema-id", out var schemaId) + || string.IsNullOrEmpty(schemaId as string); + if(isInlinedSchema) { - schema.Example = jsonString.Parse(); + 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(); + } } } } 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 index 9f640c47e8e9..6a784a54cf0c 100644 --- 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 @@ -62,15 +62,19 @@ "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": {