Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
29 changes: 25 additions & 4 deletions src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -452,11 +452,20 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyDocId), out var propertyComment))
{
var parameter = operation.Parameters?.SingleOrDefault(p => p.Name == metadata.Name);
var description = propertyComment.Summary;
if (!string.IsNullOrEmpty(description) && !string.IsNullOrEmpty(propertyComment.Value))
{
description = $"{description}\n{propertyComment.Value}";
}
else if (string.IsNullOrEmpty(description))
{
description = propertyComment.Value;
}
if (parameter is null)
{
if (operation.RequestBody is not null)
{
operation.RequestBody.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
operation.RequestBody.Description = description;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
var content = operation.RequestBody.Content?.Values;
Expand All @@ -476,7 +485,7 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
var targetOperationParameter = UnwrapOpenApiParameter(parameter);
if (targetOperationParameter is not null)
{
targetOperationParameter.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
targetOperationParameter.Description = description;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
targetOperationParameter.Example = jsonString.Parse();
Expand Down Expand Up @@ -533,12 +542,21 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
// Apply comments from the property
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
var description = propertyComment.Summary;
if (!string.IsNullOrEmpty(description) && !string.IsNullOrEmpty(propertyComment.Value))
{
description = $"{description}\n{propertyComment.Value}";
}
else if (string.IsNullOrEmpty(description))
{
description = propertyComment.Value;
}
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!;
schema.Description = description;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
schema.Example = jsonString.Parse();
Expand All @@ -547,7 +565,10 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
else
{
// Schema Reference
schema.Metadata["x-ref-description"] = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
if (!string.IsNullOrEmpty(description))
{
schema.Metadata["x-ref-description"] = description;
}
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
schema.Metadata["x-ref-example"] = jsonString.Parse()!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ await SnapshotTestHelper.VerifyOpenApi(compilation, additionalAssemblies, docume
todo = path.RequestBody.Content["application/json"].Schema;
Assert.Equal("The identifier of the todo.", todo.Properties["id"].Description);
Assert.Equal("The name of the todo.", todo.Properties["name"].Description);
Assert.Equal("Another description of the todo.", todo.Properties["description"].Description);
Assert.Equal("A description of the todo.\nAnother description of the todo.", todo.Properties["description"].Description);

path = document.Paths["/type-with-examples"].Operations[HttpMethod.Post];
var typeWithExamples = path.RequestBody.Content["application/json"].Schema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public async Task SupportsAllXmlTagsOnSchemas()
app.MapPost("/generic-class", (GenericClass<string> generic) => { });
app.MapPost("/generic-parent", (GenericParent parent) => { });
app.MapPost("/params-and-param-refs", (ParamsAndParamRefs refs) => { });
app.MapPost("/xml-property-test", (XmlPropertyTestClass test) => { });


app.Run();
Expand Down Expand Up @@ -471,6 +472,37 @@ protected virtual void Dispose(bool disposing)
// No-op
}
}

/// <summary>
/// This class tests different XML comment scenarios for properties.
/// </summary>
public class XmlPropertyTestClass
{
/// <summary>
/// A property with only summary tag.
/// </summary>
public string SummaryOnly { get; set; }

/// <value>
/// A property with only value tag.
/// </value>
public string ValueOnly { get; set; }

/// <summary>
/// A property with both summary and value.
/// </summary>
/// <value>Additional value information.</value>
public string BothSummaryAndValue { get; set; }

/// <returns>This should be ignored for properties.</returns>
public string ReturnsOnly { get; set; }

/// <summary>
/// A property with summary and returns.
/// </summary>
/// <returns>This should be ignored for properties.</returns>
public string SummaryAndReturns { get; set; }
}
""";
var generator = new XmlCommentGenerator();
await SnapshotTestHelper.Verify(source, generator, out var compilation);
Expand All @@ -479,6 +511,7 @@ await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
var path = document.Paths["/example-class"].Operations[HttpMethod.Post];
var exampleClass = path.RequestBody.Content["application/json"].Schema;
Assert.Equal("Every class and member should have a one sentence\nsummary describing its purpose.", exampleClass.Description, ignoreLineEndingDifferences: true);
// Label property has only <value> tag -> uses value directly
Assert.Equal("The `Label` property represents a label\nfor this instance.", exampleClass.Properties["label"].Description, ignoreLineEndingDifferences: true);

path = document.Paths["/person"].Operations[HttpMethod.Post];
Expand Down Expand Up @@ -523,6 +556,26 @@ await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
path = document.Paths["/params-and-param-refs"].Operations[HttpMethod.Post];
var paramsAndParamRefs = path.RequestBody.Content["application/json"].Schema;
Assert.Equal("This shows examples of typeparamref and typeparam tags", paramsAndParamRefs.Description);

// Test new XML property documentation behavior
path = document.Paths["/xml-property-test"].Operations[HttpMethod.Post];
var xmlPropertyTest = path.RequestBody.Content["application/json"].Schema;
Assert.Equal("This class tests different XML comment scenarios for properties.", xmlPropertyTest.Description);

// Property with only <summary> -> uses summary directly
Assert.Equal("A property with only summary tag.", xmlPropertyTest.Properties["summaryOnly"].Description);

// Property with only <value> -> uses value directly
Assert.Equal("A property with only value tag.", xmlPropertyTest.Properties["valueOnly"].Description);

// Property with both <summary> and <value> -> combines with newline separator
Assert.Equal("A property with both summary and value.\nAdditional value information.", xmlPropertyTest.Properties["bothSummaryAndValue"].Description);

// Property with only <returns> -> should be null (ignored)
Assert.Null(xmlPropertyTest.Properties["returnsOnly"].Description);

// Property with <summary> and <returns> -> uses summary only, ignores returns
Assert.Equal("A property with summary and returns.", xmlPropertyTest.Properties["summaryAndReturns"].Description);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public async Task SupportsXmlCommentsOnOperationsFromMinimalApis()
app.MapPost("/19", RouteHandlerExtensionMethods.Post19);
app.MapGet("/20", RouteHandlerExtensionMethods.Get20);
app.MapGet("/21", RouteHandlerExtensionMethods.Get21);
app.MapGet("/22", RouteHandlerExtensionMethods.Get22);

app.Run();

Expand Down Expand Up @@ -249,6 +250,15 @@ public static IResult Get21([AsParameters] XmlDocPriorityParametersClass priorit
{
return TypedResults.Ok($"Processed parameters");
}

/// <summary>
/// Tests summary and value documentation priority on AsParameters properties.
/// </summary>
/// <param name="summaryValueParams">Parameters testing summary vs value priority.</param>
public static IResult Get22([AsParameters] SummaryValueParametersClass summaryValueParams)
{
return TypedResults.Ok($"Summary: {summaryValueParams.SummaryProperty}, Value: {summaryValueParams.ValueProperty}");
}
}

public class FirstParameters
Expand Down Expand Up @@ -371,6 +381,23 @@ public class XmlDocPriorityParametersClass
/// <value>Value-only description.</value>
public string? ValueOnlyProperty { get; set; }
}

public class SummaryValueParametersClass
{
/// <summary>
/// Property with only summary documentation.
/// </summary>
public string? SummaryProperty { get; set; }

/// <summary>
/// Property with summary that should be overridden by value.
/// </summary>
/// <value>Value description that should take precedence over summary.</value>
public string? ValueProperty { get; set; }

/// <value>Property with only value documentation.</value>
public string? ValueOnlyProperty { get; set; }
}
""";
var generator = new XmlCommentGenerator();
await SnapshotTestHelper.Verify(source, generator, out var compilation);
Expand Down Expand Up @@ -478,16 +505,29 @@ await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
Assert.Equal("Property with only summary documentation.", summaryOnlyParam.Description);

var summaryAndReturnsParam = path22.Parameters.First(p => p.Name == "SummaryAndReturnsProperty");
Assert.Equal("Returns-based description that should take precedence over summary.", summaryAndReturnsParam.Description);
Assert.Equal("Property with summary documentation that should be overridden.", summaryAndReturnsParam.Description);

var allThreeParam = path22.Parameters.First(p => p.Name == "AllThreeProperty");
Assert.Equal("Value-based description that should take highest precedence.", allThreeParam.Description);
Assert.Equal("Property with all three types of documentation.\nValue-based description that should take highest precedence.", allThreeParam.Description);

var returnsOnlyParam = path22.Parameters.First(p => p.Name == "ReturnsOnlyProperty");
Assert.Equal("Returns-only description.", returnsOnlyParam.Description);
Assert.Null(returnsOnlyParam.Description);

var valueOnlyParam = path22.Parameters.First(p => p.Name == "ValueOnlyProperty");
Assert.Equal("Value-only description.", valueOnlyParam.Description);

// Test summary and value documentation priority for AsParameters
var path23 = document.Paths["/22"].Operations[HttpMethod.Get];
Assert.Equal("Tests summary and value documentation priority on AsParameters properties.", path23.Summary);

var summaryParam = path23.Parameters.First(p => p.Name == "SummaryProperty");
Assert.Equal("Property with only summary documentation.", summaryParam.Description);

var valueParam = path23.Parameters.First(p => p.Name == "ValueProperty");
Assert.Equal("Property with summary that should be overridden by value.\nValue description that should take precedence over summary.", valueParam.Description);

var valueOnlyParam2 = path23.Parameters.First(p => p.Name == "ValueOnlyProperty");
Assert.Equal("Property with only value documentation.", valueOnlyParam2.Description);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,13 @@ await SnapshotTestHelper.VerifyOpenApi(compilation, document =>

path = document.Paths["/todo-with-description"].Operations[HttpMethod.Post];
todo = path.RequestBody.Content["application/json"].Schema;
// Test different XML comment scenarios for properties:
// Id: only <summary> tag -> uses summary directly
Assert.Equal("The identifier of the todo.", todo.Properties["id"].Description);
// Name: only <value> tag -> uses value directly
Assert.Equal("The name of the todo.", todo.Properties["name"].Description);
Assert.Equal("Another description of the todo.", todo.Properties["description"].Description);
// Description: both <summary> and <value> tags -> combines with newline separator
Assert.Equal("A description of the the todo.\nAnother description of the todo.", todo.Properties["description"].Description);

path = document.Paths["/type-with-examples"].Operations[HttpMethod.Post];
var typeWithExamples = path.RequestBody.Content["application/json"].Schema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,11 +434,20 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyDocId), out var propertyComment))
{
var parameter = operation.Parameters?.SingleOrDefault(p => p.Name == metadata.Name);
var description = propertyComment.Summary;
if (!string.IsNullOrEmpty(description) && !string.IsNullOrEmpty(propertyComment.Value))
{
description = $"{description}\n{propertyComment.Value}";
}
else if (string.IsNullOrEmpty(description))
{
description = propertyComment.Value;
}
if (parameter is null)
{
if (operation.RequestBody is not null)
{
operation.RequestBody.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
operation.RequestBody.Description = description;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
var content = operation.RequestBody.Content?.Values;
Expand All @@ -458,7 +467,7 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
var targetOperationParameter = UnwrapOpenApiParameter(parameter);
if (targetOperationParameter is not null)
{
targetOperationParameter.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
targetOperationParameter.Description = description;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
targetOperationParameter.Example = jsonString.Parse();
Expand Down Expand Up @@ -515,12 +524,21 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
// Apply comments from the property
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
var description = propertyComment.Summary;
if (!string.IsNullOrEmpty(description) && !string.IsNullOrEmpty(propertyComment.Value))
{
description = $"{description}\n{propertyComment.Value}";
}
else if (string.IsNullOrEmpty(description))
{
description = propertyComment.Value;
}
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!;
schema.Description = description;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
schema.Example = jsonString.Parse();
Expand All @@ -529,7 +547,10 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
else
{
// Schema Reference
schema.Metadata["x-ref-description"] = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary!;
if (!string.IsNullOrEmpty(description))
{
schema.Metadata["x-ref-description"] = description;
}
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
schema.Metadata["x-ref-example"] = jsonString.Parse()!;
Expand Down
Loading
Loading