diff --git a/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs b/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs index bde53bfd7f2e..8692f0b7dfc6 100644 --- a/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs +++ b/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs @@ -309,6 +309,11 @@ internal static void ApplyParameterInfo(this JsonNode schema, ApiParameterDescri var attributes = validations.OfType(); schema.ApplyValidationAttributes(attributes); } + if (parameterDescription.ModelMetadata is Mvc.ModelBinding.Metadata.DefaultModelMetadata { Attributes.PropertyAttributes.Count: > 0 } metadata && + metadata.Attributes.PropertyAttributes.OfType().LastOrDefault() is { } metadataDefaultValueAttribute) + { + schema.ApplyDefaultValue(metadataDefaultValueAttribute.Value, jsonTypeInfo); + } if (parameterDescription.ParameterDescriptor is IParameterInfoParameterDescriptor { ParameterInfo: { } parameterInfo }) { if (parameterInfo.HasDefaultValue) diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs index d8676530f00e..97ae0da5a96d 100644 --- a/src/OpenApi/src/Services/OpenApiDocumentService.cs +++ b/src/OpenApi/src/Services/OpenApiDocumentService.cs @@ -494,11 +494,22 @@ private static bool IsRequired(ApiParameterDescription parameter) } // Apply [Description] attributes on the parameter to the top-level OpenApiParameter object and not the schema. - private static string? GetParameterDescriptionFromAttribute(ApiParameterDescription parameter) => - parameter.ParameterDescriptor is IParameterInfoParameterDescriptor { ParameterInfo: { } parameterInfo } && - parameterInfo.GetCustomAttributes().OfType().LastOrDefault() is { } descriptionAttribute ? - descriptionAttribute.Description : - null; + private static string? GetParameterDescriptionFromAttribute(ApiParameterDescription parameter) + { + if (parameter.ParameterDescriptor is IParameterInfoParameterDescriptor { ParameterInfo: { } parameterInfo } && + parameterInfo.GetCustomAttributes().LastOrDefault() is { } parameterDescription) + { + return parameterDescription.Description; + } + + if (parameter.ModelMetadata is Mvc.ModelBinding.Metadata.DefaultModelMetadata { Attributes.PropertyAttributes.Count: > 0 } metadata && + metadata.Attributes.PropertyAttributes.OfType().LastOrDefault() is { } propertyDescription) + { + return propertyDescription.Description; + } + + return null; + } private async Task GetRequestBodyAsync(OpenApiDocument document, ApiDescription description, IServiceProvider scopedServiceProvider, IOpenApiSchemaTransformer[] schemaTransformers, CancellationToken cancellationToken) { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs index f18197120b86..9ab90d9f52a0 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs @@ -229,7 +229,7 @@ public ControllerActionDescriptor CreateActionDescriptor(string methodName = nul action.AttributeRouteInfo = new() { - Template = action.MethodInfo.GetCustomAttribute()?.Template, + Template = action.MethodInfo.GetCustomAttribute()?.Template ?? string.Empty, Name = action.MethodInfo.GetCustomAttribute()?.Name, Order = action.MethodInfo.GetCustomAttribute()?.Order ?? 0, }; diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs index 7929b27f0eca..ede9a0b3d1b2 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs @@ -509,6 +509,86 @@ await VerifyOpenApiDocument(builder, document => }); } + [Fact] + public async Task GetOpenApiParameters_HandlesAsParametersParametersWithDescriptionAttribute() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapGet("/api", ([AsParameters] FromQueryModel model) => { }); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api"].Operations[HttpMethod.Get]; + Assert.Contains(operation.Parameters, actualMemory => actualMemory.Name == "id" && actualMemory.Description == "The ID of the entity"); + }); + } + + [Fact] + public async Task GetOpenApiParameters_HandlesFromQueryParametersWithDescriptionAttribute() + { + // Arrange + var actionDescriptor = CreateActionDescriptor(nameof(TestFromQueryController.GetWithFromQueryDto), typeof(TestFromQueryController)); + + // Assert + await VerifyOpenApiDocument(actionDescriptor, document => + { + var operation = document.Paths["/"].Operations[HttpMethod.Get]; + Assert.Contains(operation.Parameters, actualMemory => actualMemory.Name == "id" && actualMemory.Description == "The ID of the entity"); + }); + } + + [Fact] + public async Task GetOpenApiParameters_HandlesAsParametersParametersWithDefaultValueAttribute() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapGet("/api", ([AsParameters] FromQueryModel model) => { }); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api"].Operations[HttpMethod.Get]; + Assert.Contains( + operation.Parameters, + actualMemory => + { + return actualMemory.Name == "limit" && + actualMemory.Schema != null && + actualMemory.Schema.Default != null && + actualMemory.Schema.Default.GetValueKind() == JsonValueKind.Number && + actualMemory.Schema.Default.GetValue() == 20; + }); + }); + } + + [Fact] + public async Task GetOpenApiParameters_HandlesFromQueryParametersWithDefaultValueAttribute() + { + // Arrange + var actionDescriptor = CreateActionDescriptor(nameof(TestFromQueryController.GetWithFromQueryDto), typeof(TestFromQueryController)); + + // Assert + await VerifyOpenApiDocument(actionDescriptor, document => + { + var operation = document.Paths["/"].Operations[HttpMethod.Get]; + Assert.Contains( + operation.Parameters, + actualMemory => + { + return actualMemory.Name == "limit" && + actualMemory.Schema != null && + actualMemory.Schema.Default != null && + actualMemory.Schema.Default.GetValueKind() == JsonValueKind.Number && + actualMemory.Schema.Default.GetValue() == 20; + }); + }); + } + [Route("/api/{id}/{date}")] private void AcceptsParametersInModel(RouteParamsContainer model) { } @@ -809,4 +889,28 @@ public override void Write(Utf8JsonWriter writer, EnumArrayType value, JsonSeria writer.WriteEndObject(); } } + + [ApiController] + [Route("[controller]/[action]")] + private class TestFromQueryController : ControllerBase + { + [HttpGet] + public Task GetWithFromQueryDto([FromQuery] FromQueryModel query) + { + return Task.FromResult(Ok()); + } + } + + [Description("A query model.")] + private record FromQueryModel + { + [Description("The ID of the entity")] + [FromQuery(Name = "id")] + public int Id { get; set; } + + [Description("The maximum number of results")] + [FromQuery(Name = "limit")] + [DefaultValue(20)] + public int Limit { get; set; } + } }