diff --git a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs index 771f09d181c3..003df4f0689f 100644 --- a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs +++ b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs @@ -423,6 +423,14 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP { public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) { + if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment)) + { + schema.Description = typeComment.Summary; + if (typeComment.Examples?.FirstOrDefault() is { } jsonString) + { + schema.Example = jsonString.Parse(); + } + } if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) { if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment)) @@ -434,14 +442,6 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext } } } - if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment)) - { - schema.Description = typeComment.Summary; - if (typeComment.Examples?.FirstOrDefault() is { } jsonString) - { - schema.Example = jsonString.Parse(); - } - } return Task.CompletedTask; } } diff --git a/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs b/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs index 777913ccc567..919675d580ab 100644 --- a/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs +++ b/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs @@ -97,8 +97,8 @@ internal static void ApplyValidationAttributes(this JsonNode schema, IEnumerable ? CultureInfo.InvariantCulture : CultureInfo.CurrentCulture; - var minString = rangeAttribute.Minimum.ToString(); - var maxString = rangeAttribute.Maximum.ToString(); + var minString = string.Format(targetCulture, "{0}", rangeAttribute.Minimum); + var maxString = string.Format(targetCulture, "{0}", rangeAttribute.Maximum); if (decimal.TryParse(minString, NumberStyles.Any, targetCulture, out var minDecimal)) { diff --git a/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs b/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs index cb3b6b26abfc..0a0ee73ca841 100644 --- a/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs @@ -25,6 +25,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 290100cdbfc3..1f8065c1a756 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SchemaTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; @@ -19,7 +19,10 @@ public async Task SupportsXmlCommentsOnSchemas() var builder = WebApplication.CreateBuilder(); -builder.Services.AddOpenApi(); +builder.Services.AddOpenApi(options => { + var prevCreateSchemaReferenceId = options.CreateSchemaReferenceId; + options.CreateSchemaReferenceId = (x) => x.Type == typeof(AddressNested) ? null : prevCreateSchemaReferenceId(x); +}); var app = builder.Build(); @@ -31,6 +34,7 @@ public async Task SupportsXmlCommentsOnSchemas() app.MapPost("/todo-with-description", (TodoWithDescription todo) => { }); app.MapPost("/type-with-examples", (TypeWithExamples typeWithExamples) => { }); app.MapPost("/user", (User user) => { }); +app.MapPost("/company", (Company company) => { }); app.Run(); @@ -175,6 +179,60 @@ internal class User : IUser /// public string Name { get; set; } } + +/// +/// An address. +/// +public class AddressWithSummary +{ + public string Street { get; set; } +} + +public class AddressWithoutSummary +{ + public string Street { get; set; } +} + +/// +/// An address. +/// +public class AddressNested +{ + public string Street { get; set; } +} + +public class Company +{ + /// + /// Billing address. + /// + public AddressWithSummary BillingAddressClassWithSummary { get; set; } + + /// + /// Billing address. + /// + public AddressWithoutSummary BillingAddressClassWithoutSummary { get; set; } + + /// + /// Billing address. + /// + public AddressNested BillingAddressNested { get; set; } + + /// + /// Visiting address. + /// + public AddressWithSummary VisitingAddressClassWithSummary { get; set; } + + /// + /// Visiting address. + /// + public AddressWithoutSummary VisitingAddressClassWithoutSummary { get; set; } + + /// + /// Visiting address. + /// + public AddressNested VisitingAddressNested { get; set; } +} """; var generator = new XmlCommentGenerator(); await SnapshotTestHelper.Verify(source, generator, out var compilation); @@ -258,6 +316,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); }); } } 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 c2d09bdb5971..3f321dd217df 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Globalization; using System.Reflection; using System.Runtime.Loader; using System.Text; @@ -16,6 +17,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Writers; namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests; @@ -197,8 +199,7 @@ void OnEntryPointExit(Exception exception) var service = services.GetService(serviceType) ?? throw new InvalidOperationException("Could not resolve IDocumentProvider service."); using var stream = new MemoryStream(); - var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); - using var writer = new StreamWriter(stream, encoding, bufferSize: 1024, leaveOpen: true); + 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]); stream.Position = 0; 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 dd6515efdb21..43e6a5f68f57 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 @@ -1,4 +1,4 @@ -//HintName: OpenApiXmlCommentSupport.generated.cs +//HintName: OpenApiXmlCommentSupport.generated.cs //------------------------------------------------------------------------------ // // This code was generated by a tool. @@ -78,6 +78,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)); @@ -102,6 +104,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; } @@ -435,6 +443,14 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP { public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) { + if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment)) + { + schema.Description = typeComment.Summary; + if (typeComment.Examples?.FirstOrDefault() is { } jsonString) + { + schema.Example = jsonString.Parse(); + } + } if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo }) { if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment)) @@ -446,14 +462,6 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext } } } - if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment)) - { - schema.Description = typeComment.Summary; - if (typeComment.Examples?.FirstOrDefault() is { } jsonString) - { - schema.Example = jsonString.Parse(); - } - } return Task.CompletedTask; } } @@ -490,14 +498,15 @@ 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); }); } } -} \ No newline at end of file +}