Skip to content
Merged
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
41 changes: 30 additions & 11 deletions src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -449,17 +449,7 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
schema.Example = jsonString.Parse();
}
}
}
// Apply comments from the type
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
Expand All @@ -468,6 +458,34 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
schema.Example = jsonString.Parse();
}
}

if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
// Apply comments from the property
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
if (schema.Metadata is null
|| !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
|| string.IsNullOrEmpty(schemaId as string))
{
// Inlined schema
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
schema.Example = jsonString.Parse();
}
}
else
{
// Schema Reference
schema.Metadata["x-ref-description"] = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
schema.Metadata["x-ref-example"] = jsonString.Parse()!;
}
}
}
}
return Task.CompletedTask;
}
}
Expand Down Expand Up @@ -507,6 +525,7 @@ file static class GeneratedServiceCollectionExtensions
{{GenerateAddOpenApiInterceptions(groupedAddOpenApiInvocations)}}
}
}

""";

internal static string GetAddOpenApiInterceptor(AddOpenApiOverloadVariant overloadVariant) => overloadVariant switch
Expand Down
17 changes: 16 additions & 1 deletion src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -21,6 +23,19 @@ public static IOpenApiSchema AddOpenApiSchemaByReference(this OpenApiDocument do
document.Workspace ??= new();
var location = document.BaseUri + "/components/schemas/" + schemaId;
document.Workspace.RegisterComponentForDocument(document, schema, location);
return new OpenApiSchemaReference(schemaId, document);

object? description = null;
object? example = null;
if (schema is OpenApiSchema actualSchema)
{
actualSchema.Metadata?.TryGetValue(OpenApiConstants.RefDescriptionAnnotation, out description);
actualSchema.Metadata?.TryGetValue(OpenApiConstants.RefExampleAnnotation, out example);
}

return new OpenApiSchemaReference(schemaId, document)
{
Description = description as string,
Examples = example is JsonNode exampleJson ? [exampleJson] : null,
};
}
}
2 changes: 2 additions & 0 deletions src/OpenApi/src/Services/OpenApiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ internal static class OpenApiConstants
internal const string DescriptionId = "x-aspnetcore-id";
internal const string SchemaId = "x-schema-id";
internal const string RefId = "x-ref-id";
internal const string RefDescriptionAnnotation = "x-ref-description";
internal const string RefExampleAnnotation = "x-ref-example";
internal const string DefaultOpenApiResponseKey = "default";
// Since there's a finite set of HTTP methods that can be included in a given
// OpenApiPaths, we can pre-allocate an array of these methods and use a direct
Expand Down
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 Down Expand Up @@ -175,6 +175,7 @@ internal class User : IUser
/// <inheritdoc/>
public string Name { get; set; }
}

""";
var generator = new XmlCommentGenerator();
await SnapshotTestHelper.Verify(source, generator, out var compilation);
Expand Down Expand Up @@ -260,4 +261,161 @@ await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
Assert.Equal("The user's display name.", user.Properties["name"].Description);
});
}

[Fact]
public async Task XmlCommentsOnPropertiesShouldApplyToSchemaReferences()
{
var source = """
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options => {
var prevCreateSchemaReferenceId = options.CreateSchemaReferenceId;
options.CreateSchemaReferenceId = (x) => x.Type == typeof(ModelInline) ? null : prevCreateSchemaReferenceId(x);
});

var app = builder.Build();

app.MapPost("/example", (RootModel model) => { });

app.Run();

/// <summary>
/// Comment on class ModelWithSummary.
/// </summary>
/// <example>
/// { "street": "ModelWithSummaryClass" }
/// </example>
public class ModelWithSummary
{
public string Street { get; set; }
}

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

/// <summary>
/// Comment on class ModelInline.
/// </summary>
/// <example>
/// { "street": "ModelInlineClass" }
/// </example>
public class ModelInline
{
public string Street { get; set; }
}

/// <summary>
/// Comment on class RootModel.
/// </summary>
/// <example>
/// { }
/// </example>
public class RootModel
{
public ModelWithSummary NoPropertyComment { get; set; }

/// <summary>
/// Comment on property ModelWithSummary1.
/// </summary>
/// <example>
/// { "street": "ModelWithSummary1Prop" }
/// </example>
public ModelWithSummary ModelWithSummary1 { get; set; }

/// <summary>
/// Comment on property ModelWithSummary2.
/// </summary>
/// <example>
/// { "street": "ModelWithSummary2Prop" }
/// </example>
public ModelWithSummary ModelWithSummary2 { get; set; }

/// <summary>
/// Comment on property ModelWithoutSummary1.
/// </summary>
/// <example>
/// { "street": "ModelWithoutSummary1Prop" }
/// </example>
public ModelWithoutSummary ModelWithoutSummary1 { get; set; }

/// <summary>
/// Comment on property ModelWithoutSummary2.
/// </summary>
/// <example>
/// { "street": "ModelWithoutSummary2Prop" }
/// </example>
public ModelWithoutSummary ModelWithoutSummary2 { get; set; }

/// <summary>
/// Comment on property ModelInline1.
/// </summary>
/// <example>
/// { "street": "ModelInline1Prop" }
/// </example>
public ModelInline ModelInline1 { get; set; }

/// <summary>
/// Comment on property ModelInline2.
/// </summary>
/// <example>
/// { "street": "ModelInline2Prop" }
/// </example>
public ModelInline ModelInline2 { get; set; }
}
""";
var generator = new XmlCommentGenerator();
await SnapshotTestHelper.Verify(source, generator, out var compilation);
await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
{
var path = document.Paths["/example"].Operations[HttpMethod.Post];
var exampleOperationBodySchema = path.RequestBody.Content["application/json"].Schema;
Assert.Equal("Comment on class RootModel.", exampleOperationBodySchema.Description);

var rootModelSchema = document.Components.Schemas["RootModel"];
Assert.Equal("Comment on class RootModel.", rootModelSchema.Description);

var modelWithSummary = document.Components.Schemas["ModelWithSummary"];
Assert.Equal("Comment on class ModelWithSummary.", modelWithSummary.Description);
Assert.True(JsonNode.DeepEquals(JsonNode.Parse("""{ "street": "ModelWithSummaryClass" }"""), modelWithSummary.Example));

var modelWithoutSummary = document.Components.Schemas["ModelWithoutSummary"];
Assert.Null(modelWithoutSummary.Description);

Assert.DoesNotContain("ModelInline", document.Components.Schemas.Keys);

// Check RootModel properties
var noPropertyCommentProp = Assert.IsType<OpenApiSchemaReference>(rootModelSchema.Properties["noPropertyComment"]);
Assert.Null(noPropertyCommentProp.Reference.Description);

var modelWithSummary1Prop = Assert.IsType<OpenApiSchemaReference>(rootModelSchema.Properties["modelWithSummary1"]);
Assert.Equal("Comment on property ModelWithSummary1.", modelWithSummary1Prop.Description);
Assert.True(JsonNode.DeepEquals(JsonNode.Parse("""{ "street": "ModelWithSummary1Prop" }"""), modelWithSummary1Prop.Examples[0]));

var modelWithSummary2Prop = Assert.IsType<OpenApiSchemaReference>(rootModelSchema.Properties["modelWithSummary2"]);
Assert.Equal("Comment on property ModelWithSummary2.", modelWithSummary2Prop.Description);
Assert.True(JsonNode.DeepEquals(JsonNode.Parse("""{ "street": "ModelWithSummary2Prop" }"""), modelWithSummary2Prop.Examples[0]));

var modelWithoutSummary1Prop = Assert.IsType<OpenApiSchemaReference>(rootModelSchema.Properties["modelWithoutSummary1"]);
Assert.Equal("Comment on property ModelWithoutSummary1.", modelWithoutSummary1Prop.Description);
Assert.True(JsonNode.DeepEquals(JsonNode.Parse("""{ "street": "ModelWithoutSummary1Prop" }"""), modelWithoutSummary1Prop.Examples[0]));

var modelWithoutSummary2Prop = Assert.IsType<OpenApiSchemaReference>(rootModelSchema.Properties["modelWithoutSummary2"]);
Assert.Equal("Comment on property ModelWithoutSummary2.", modelWithoutSummary2Prop.Description);
Assert.True(JsonNode.DeepEquals(JsonNode.Parse("""{ "street": "ModelWithoutSummary2Prop" }"""), modelWithoutSummary2Prop.Examples[0]));

var modelInline1Prop = Assert.IsType<OpenApiSchema>(rootModelSchema.Properties["modelInline1"]);
Assert.Equal("Comment on property ModelInline1.", modelInline1Prop.Description);
Assert.True(JsonNode.DeepEquals(JsonNode.Parse("""{ "street": "ModelInline1Prop" }"""), modelInline1Prop.Example));

var modelInline2Prop = Assert.IsType<OpenApiSchema>(rootModelSchema.Properties["modelInline2"]);
Assert.Equal("Comment on property ModelInline2.", modelInline2Prop.Description);
Assert.True(JsonNode.DeepEquals(JsonNode.Parse("""{ "street": "ModelInline2Prop" }"""), modelInline2Prop.Example));
});
}
}
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 @@ -431,17 +431,7 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
schema.Example = jsonString.Parse();
}
}
}
// Apply comments from the type
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
Expand All @@ -450,6 +440,34 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
schema.Example = jsonString.Parse();
}
}

if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
// Apply comments from the property
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
if (schema.Metadata is null
|| !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
|| string.IsNullOrEmpty(schemaId as string))
{
// Inlined schema
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
schema.Example = jsonString.Parse();
}
}
else
{
// Schema Reference
schema.Metadata["x-ref-description"] = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
schema.Metadata["x-ref-example"] = jsonString.Parse()!;
}
}
}
}
return Task.CompletedTask;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,17 +460,7 @@ private static OpenApiParameter UnwrapOpenApiParameter(IOpenApiParameter sourceP
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
schema.Example = jsonString.Parse();
}
}
}
// Apply comments from the type
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
Expand All @@ -479,6 +469,34 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
schema.Example = jsonString.Parse();
}
}

if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
// Apply comments from the property
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
if (schema.Metadata is null
|| !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
|| string.IsNullOrEmpty(schemaId as string))
{
// Inlined schema
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
schema.Example = jsonString.Parse();
}
}
else
{
// Schema Reference
schema.Metadata["x-ref-description"] = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
schema.Metadata["x-ref-example"] = jsonString.Parse()!;
}
}
}
}
return Task.CompletedTask;
}
}
Expand Down
Loading
Loading