Skip to content

Commit 9cdd601

Browse files
[OpenAPI] Get parameter description with [FromQuery] (#62986)
* [OpenAPI] Get description with [FromQuery] Get the description from the associated object's property when `[FromQuery]` is applied to a property of an object used as a `[FromQuery]` parameter. Resolves #61297. * [OpenAPI] Get default value with [FromQuery] Get the default value from the associated object's property when `[FromQuery]` is applied to a property of an object used as a `[FromQuery]` parameter. Resolves #61934.
1 parent f8c007b commit 9cdd601

File tree

4 files changed

+126
-6
lines changed

4 files changed

+126
-6
lines changed

src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,11 @@ internal static void ApplyParameterInfo(this JsonNode schema, ApiParameterDescri
329329
var attributes = validations.OfType<ValidationAttribute>();
330330
schema.ApplyValidationAttributes(attributes);
331331
}
332+
if (parameterDescription.ModelMetadata is Mvc.ModelBinding.Metadata.DefaultModelMetadata { Attributes.PropertyAttributes.Count: > 0 } metadata &&
333+
metadata.Attributes.PropertyAttributes.OfType<DefaultValueAttribute>().LastOrDefault() is { } metadataDefaultValueAttribute)
334+
{
335+
schema.ApplyDefaultValue(metadataDefaultValueAttribute.Value, jsonTypeInfo);
336+
}
332337
if (parameterDescription.ParameterDescriptor is IParameterInfoParameterDescriptor { ParameterInfo: { } parameterInfo })
333338
{
334339
if (parameterInfo.HasDefaultValue)

src/OpenApi/src/Services/OpenApiDocumentService.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -506,11 +506,22 @@ private static bool IsRequired(ApiParameterDescription parameter)
506506
}
507507

508508
// Apply [Description] attributes on the parameter to the top-level OpenApiParameter object and not the schema.
509-
private static string? GetParameterDescriptionFromAttribute(ApiParameterDescription parameter) =>
510-
parameter.ParameterDescriptor is IParameterInfoParameterDescriptor { ParameterInfo: { } parameterInfo } &&
511-
parameterInfo.GetCustomAttributes().OfType<DescriptionAttribute>().LastOrDefault() is { } descriptionAttribute ?
512-
descriptionAttribute.Description :
513-
null;
509+
private static string? GetParameterDescriptionFromAttribute(ApiParameterDescription parameter)
510+
{
511+
if (parameter.ParameterDescriptor is IParameterInfoParameterDescriptor { ParameterInfo: { } parameterInfo } &&
512+
parameterInfo.GetCustomAttributes<DescriptionAttribute>().LastOrDefault() is { } parameterDescription)
513+
{
514+
return parameterDescription.Description;
515+
}
516+
517+
if (parameter.ModelMetadata is Mvc.ModelBinding.Metadata.DefaultModelMetadata { Attributes.PropertyAttributes.Count: > 0 } metadata &&
518+
metadata.Attributes.PropertyAttributes.OfType<DescriptionAttribute>().LastOrDefault() is { } propertyDescription)
519+
{
520+
return propertyDescription.Description;
521+
}
522+
523+
return null;
524+
}
514525

515526
private async Task<OpenApiRequestBody?> GetRequestBodyAsync(OpenApiDocument document, ApiDescription description, IServiceProvider scopedServiceProvider, IOpenApiSchemaTransformer[] schemaTransformers, CancellationToken cancellationToken)
516527
{

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ public ControllerActionDescriptor CreateActionDescriptor(string methodName = nul
229229

230230
action.AttributeRouteInfo = new()
231231
{
232-
Template = action.MethodInfo.GetCustomAttribute<RouteAttribute>()?.Template,
232+
Template = action.MethodInfo.GetCustomAttribute<RouteAttribute>()?.Template ?? string.Empty,
233233
Name = action.MethodInfo.GetCustomAttribute<RouteAttribute>()?.Name,
234234
Order = action.MethodInfo.GetCustomAttribute<RouteAttribute>()?.Order ?? 0,
235235
};

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,86 @@ await VerifyOpenApiDocument(builder, document =>
509509
});
510510
}
511511

512+
[Fact]
513+
public async Task GetOpenApiParameters_HandlesAsParametersParametersWithDescriptionAttribute()
514+
{
515+
// Arrange
516+
var builder = CreateBuilder();
517+
518+
// Act
519+
builder.MapGet("/api", ([AsParameters] FromQueryModel model) => { });
520+
521+
// Assert
522+
await VerifyOpenApiDocument(builder, document =>
523+
{
524+
var operation = document.Paths["/api"].Operations[HttpMethod.Get];
525+
Assert.Contains(operation.Parameters, actualMemory => actualMemory.Name == "id" && actualMemory.Description == "The ID of the entity");
526+
});
527+
}
528+
529+
[Fact]
530+
public async Task GetOpenApiParameters_HandlesFromQueryParametersWithDescriptionAttribute()
531+
{
532+
// Arrange
533+
var actionDescriptor = CreateActionDescriptor(nameof(TestFromQueryController.GetWithFromQueryDto), typeof(TestFromQueryController));
534+
535+
// Assert
536+
await VerifyOpenApiDocument(actionDescriptor, document =>
537+
{
538+
var operation = document.Paths["/"].Operations[HttpMethod.Get];
539+
Assert.Contains(operation.Parameters, actualMemory => actualMemory.Name == "id" && actualMemory.Description == "The ID of the entity");
540+
});
541+
}
542+
543+
[Fact]
544+
public async Task GetOpenApiParameters_HandlesAsParametersParametersWithDefaultValueAttribute()
545+
{
546+
// Arrange
547+
var builder = CreateBuilder();
548+
549+
// Act
550+
builder.MapGet("/api", ([AsParameters] FromQueryModel model) => { });
551+
552+
// Assert
553+
await VerifyOpenApiDocument(builder, document =>
554+
{
555+
var operation = document.Paths["/api"].Operations[HttpMethod.Get];
556+
Assert.Contains(
557+
operation.Parameters,
558+
actualMemory =>
559+
{
560+
return actualMemory.Name == "limit" &&
561+
actualMemory.Schema != null &&
562+
actualMemory.Schema.Default != null &&
563+
actualMemory.Schema.Default.GetValueKind() == JsonValueKind.Number &&
564+
actualMemory.Schema.Default.GetValue<int>() == 20;
565+
});
566+
});
567+
}
568+
569+
[Fact]
570+
public async Task GetOpenApiParameters_HandlesFromQueryParametersWithDefaultValueAttribute()
571+
{
572+
// Arrange
573+
var actionDescriptor = CreateActionDescriptor(nameof(TestFromQueryController.GetWithFromQueryDto), typeof(TestFromQueryController));
574+
575+
// Assert
576+
await VerifyOpenApiDocument(actionDescriptor, document =>
577+
{
578+
var operation = document.Paths["/"].Operations[HttpMethod.Get];
579+
Assert.Contains(
580+
operation.Parameters,
581+
actualMemory =>
582+
{
583+
return actualMemory.Name == "limit" &&
584+
actualMemory.Schema != null &&
585+
actualMemory.Schema.Default != null &&
586+
actualMemory.Schema.Default.GetValueKind() == JsonValueKind.Number &&
587+
actualMemory.Schema.Default.GetValue<int>() == 20;
588+
});
589+
});
590+
}
591+
512592
[Route("/api/{id}/{date}")]
513593
private void AcceptsParametersInModel(RouteParamsContainer model) { }
514594

@@ -809,4 +889,28 @@ public override void Write(Utf8JsonWriter writer, EnumArrayType value, JsonSeria
809889
writer.WriteEndObject();
810890
}
811891
}
892+
893+
[ApiController]
894+
[Route("[controller]/[action]")]
895+
private class TestFromQueryController : ControllerBase
896+
{
897+
[HttpGet]
898+
public Task<IActionResult> GetWithFromQueryDto([FromQuery] FromQueryModel query)
899+
{
900+
return Task.FromResult<IActionResult>(Ok());
901+
}
902+
}
903+
904+
[Description("A query model.")]
905+
private record FromQueryModel
906+
{
907+
[Description("The ID of the entity")]
908+
[FromQuery(Name = "id")]
909+
public int Id { get; set; }
910+
911+
[Description("The maximum number of results")]
912+
[FromQuery(Name = "limit")]
913+
[DefaultValue(20)]
914+
public int Limit { get; set; }
915+
}
812916
}

0 commit comments

Comments
 (0)