Skip to content

Fix/openapi schema xml comments ordering #62213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand Down
5 changes: 4 additions & 1 deletion src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();

Expand All @@ -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();

Expand Down Expand Up @@ -175,6 +179,60 @@ internal class User : IUser
/// <inheritdoc/>
public string Name { get; set; }
}

/// <summary>
/// An address.
/// </summary>
public class AddressWithSummary
{
public string Street { get; set; }
}

public class AddressWithoutSummary
{
public string Street { get; set; }
}

/// <summary>
/// An address.
/// </summary>
public class AddressNested
{
public string Street { get; set; }
}

public class Company
{
/// <summary>
/// Billing address.
/// </summary>
public AddressWithSummary BillingAddressClassWithSummary { get; set; }

/// <summary>
/// Billing address.
/// </summary>
public AddressWithoutSummary BillingAddressClassWithoutSummary { get; set; }

/// <summary>
/// Billing address.
/// </summary>
public AddressNested BillingAddressNested { get; set; }

/// <summary>
/// Visiting address.
/// </summary>
public AddressWithSummary VisitingAddressClassWithSummary { get; set; }

/// <summary>
/// Visiting address.
/// </summary>
public AddressWithoutSummary VisitingAddressClassWithoutSummary { get; set; }

/// <summary>
/// Visiting address.
/// </summary>
public AddressNested VisitingAddressNested { get; set; }
}
""";
var generator = new XmlCommentGenerator();
await SnapshotTestHelper.Verify(source, generator, out var compilation);
Expand Down Expand Up @@ -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);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//HintName: OpenApiXmlCommentSupport.generated.cs
//HintName: OpenApiXmlCommentSupport.generated.cs
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
Expand Down Expand Up @@ -78,6 +78,8 @@ private static Dictionary<string, XmlComment> 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));
Expand All @@ -102,6 +104,12 @@ private static Dictionary<string, XmlComment> 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;
}
Expand Down Expand Up @@ -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))
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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<OpenApiOptions> configureOptions)
{
return services.AddOpenApi("v1", options =>
{
options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
options.AddOperationTransformer(new XmlCommentOperationTransformer());
configureOptions(options);
});
}

}
}
}
Loading