diff --git a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs index 82864654e936..fe0a8b0188b1 100644 --- a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs +++ b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs @@ -386,9 +386,15 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform } if (methodComment.Parameters is { Count: > 0}) { + var requestBodyParameterName = context.Description.ParameterDescriptions + .FirstOrDefault(parameterDescription => + parameterDescription.Source == Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource.Body || + parameterDescription.Source == Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource.FormFile || + parameterDescription.Source == Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource.Form) + ?.ParameterDescriptor?.Name; + 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) { @@ -400,12 +406,12 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform } targetOperationParameter.Deprecated = parameterComment.Deprecated; } - else + else if (requestBodyParameterName is not null && string.Equals(parameterComment.Name, requestBodyParameterName, StringComparison.Ordinal)) { var requestBody = operation.RequestBody; if (requestBody is not null) { - requestBody.Description = parameterComment.Description; + requestBody.Description ??= parameterComment.Description; if (parameterComment.Example is { } jsonString) { var content = requestBody?.Content?.Values; diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/OperationTests.MinimalApis.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/OperationTests.MinimalApis.cs index dad4380e2f9a..13d3d4f5ea9a 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/OperationTests.MinimalApis.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/OperationTests.MinimalApis.cs @@ -530,4 +530,64 @@ await SnapshotTestHelper.VerifyOpenApi(compilation, document => Assert.Equal("Property with only value documentation.", valueOnlyParam2.Description); }); } + + [Fact] + public async Task RequestBodyDescriptionUsesFromBodyParameterCommentNotLastParameter() + { + // Regression test for issue #65805 + // When an endpoint has [FromBody] parameter followed by other parameters like [FromServices] or CancellationToken, + // the request body description should use the [FromBody] parameter's XML comment, not the last parameter's. + var source = """ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; + +var builder = WebApplication.CreateBuilder(); +builder.Services.AddOpenApi(); +var app = builder.Build(); + +app.MapPost("/test", TestEndpoint.PostData); +app.Run(); + +public static class TestEndpoint +{ + /// + /// Process some sample input. + /// + /// Sample data provided by the user. + /// Logger for diagnostics and tracing. + /// Injected cancellation token. + /// The number the user supplied. + public static async Task> PostData( + [FromBody] SampleData data, + [FromServices] ILogger logger, + CancellationToken cancellation) + { + ArgumentNullException.ThrowIfNull(data); + logger.LogInformation("User supplied {Number} and {Text}", data.Number, data.Text); + await Task.Delay(1, cancellation).ConfigureAwait(false); + return TypedResults.Ok(data.Number); + } +} + +public record SampleData(int Number, string Text); +"""; + var generator = new XmlCommentGenerator(); + await SnapshotTestHelper.Verify(source, generator, out var compilation); + await SnapshotTestHelper.VerifyOpenApi(compilation, document => + { + var postOperation = document.Paths["/test"].Operations[HttpMethod.Post]; + var requestBody = postOperation.RequestBody; + + Assert.NotNull(requestBody); + Assert.Equal("Sample data provided by the user.", requestBody.Description); + Assert.NotEqual("Injected cancellation token.", requestBody.Description); + }); + } }