Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit d72b3c2

Browse files
Skip IResult in metadata if it implements IEndpointMetadataProvider (#63157)
1 parent 8d2a495 commit d72b3c2

7 files changed

+161
-5
lines changed

‎src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,10 @@ internal static Dictionary<int, ApiResponseType> ReadResponseMetadata(
231231

232232
foreach (var metadata in responseMetadata)
233233
{
234-
// `IResult` metadata inserted for awaitable types should
235-
// not be considered for response metadata.
236-
if (typeof(IResult).IsAssignableFrom(metadata.Type))
234+
// Skip IResult types that implement IEndpointMetadataProvider (built-in framework types like TypedResults)
235+
// since they handle their own metadata population. Custom IResult implementations that don't implement
236+
// IEndpointMetadataProvider should be included in response metadata for API documentation.
237+
if (typeof(IResult).IsAssignableFrom(metadata.Type) && typeof(IEndpointMetadataProvider).IsAssignableFrom(metadata.Type))
237238
{
238239
continue;
239240
}

‎src/Mvc/Mvc.ApiExplorer/test/ApiResponseTypeProviderTest.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -811,7 +811,7 @@ public void GetApiResponseTypes_HandlesActionWithMultipleContentTypesAndProduces
811811
}
812812

813813
[Fact]
814-
public void GetApiResponseTypes_ReturnNoResponseTypes_IfActionWithIResultReturnType()
814+
public void GetApiResponseTypes_ReturnNoResponseTypes_IfActionWithBuiltIResultReturnType()
815815
{
816816
// Arrange
817817
var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.GetIResult));
@@ -824,6 +824,23 @@ public void GetApiResponseTypes_ReturnNoResponseTypes_IfActionWithIResultReturnT
824824
Assert.False(result.Any());
825825
}
826826

827+
[Fact]
828+
public void GetApiResponseTypes_ReturnResponseType_IfActionHasCustomIResultReturnTypeInMetadata()
829+
{
830+
// Arrange
831+
var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.GetCustomIResult));
832+
actionDescriptor.EndpointMetadata = [new ProducesResponseTypeMetadata(200, typeof(MyResponse))];
833+
var provider = new ApiResponseTypeProvider(new EmptyModelMetadataProvider(), new ActionResultTypeMapper(), new MvcOptions());
834+
835+
// Act
836+
var result = provider.GetApiResponseTypes(actionDescriptor);
837+
838+
// Assert
839+
var response = Assert.Single(result);
840+
Assert.Equal(typeof(MyResponse), response.Type);
841+
Assert.Equal(200, response.StatusCode);
842+
}
843+
827844
private static ApiResponseTypeProvider GetProvider()
828845
{
829846
var mvcOptions = new MvcOptions
@@ -871,6 +888,18 @@ public class TestController
871888
public ActionResult<DerivedModel> PutModel(string userId, DerivedModel model) => null;
872889

873890
public IResult GetIResult(int id) => null;
891+
892+
public MyResponse GetCustomIResult() => new MyResponse { Content = "Test Content" };
893+
}
894+
895+
public class MyResponse : IResult
896+
{
897+
public required string Content { get; set; }
898+
899+
public Task ExecuteAsync(HttpContext httpContext)
900+
{
901+
return httpContext.Response.WriteAsJsonAsync(this);
902+
}
874903
}
875904

876905
private class TestOutputFormatter : OutputFormatter

‎src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,32 @@ async Task<Results<Created<InferredJsonClass>, ProblemHttpResult>> () =>
628628
Assert.Empty(badRequestResponseType.ApiResponseFormats);
629629
}
630630

631+
[Fact]
632+
public void ResponseProducesMetadataWithIResultImplementor()
633+
{
634+
var apiDescription = GetApiDescription(
635+
[ProducesResponseType(typeof(CustomIResultImplementor), StatusCodes.Status200OK)] () => new CustomIResultImplementor { Content = "Hello, World!" });
636+
637+
var okResponseType = Assert.Single(apiDescription.SupportedResponseTypes);
638+
639+
Assert.Equal(200, okResponseType.StatusCode);
640+
Assert.Equal(typeof(CustomIResultImplementor), okResponseType.Type);
641+
Assert.Equal(typeof(CustomIResultImplementor), okResponseType.ModelMetadata?.ModelType);
642+
643+
var okResponseFormat = Assert.Single(okResponseType.ApiResponseFormats);
644+
Assert.Equal("application/json", okResponseFormat.MediaType);
645+
}
646+
647+
public class CustomIResultImplementor : IResult
648+
{
649+
public required string Content { get; set; }
650+
651+
public Task ExecuteAsync(HttpContext httpContext)
652+
{
653+
return httpContext.Response.WriteAsJsonAsync(this);
654+
}
655+
}
656+
631657
[Fact]
632658
public void AddsFromRouteParameterAsPath()
633659
{

‎src/OpenApi/sample/Endpoints/MapSchemasEndpoints.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,20 @@ public static IEndpointRouteBuilder MapSchemasEndpoints(this IEndpointRouteBuild
3939
schemas.MapPost("/child", (ChildObject child) => Results.Ok(child));
4040
schemas.MapPatch("/json-patch", (JsonPatchDocument patchDoc) => Results.NoContent());
4141
schemas.MapPatch("/json-patch-generic", (JsonPatchDocument<ParentObject> patchDoc) => Results.NoContent());
42-
42+
schemas.MapGet("/custom-iresult", () => new CustomIResultImplementor { Content = "Hello world!" })
43+
.Produces<CustomIResultImplementor>(200);
4344
return endpointRouteBuilder;
4445
}
4546

47+
public class CustomIResultImplementor : IResult
48+
{
49+
public required string Content { get; set; }
50+
public Task ExecuteAsync(HttpContext httpContext)
51+
{
52+
return Task.CompletedTask;
53+
}
54+
}
55+
4656
public sealed class Category
4757
{
4858
public required string Name { get; set; }

‎src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,25 @@
551551
}
552552
}
553553
}
554+
},
555+
"/schemas-by-ref/custom-iresult": {
556+
"get": {
557+
"tags": [
558+
"Sample"
559+
],
560+
"responses": {
561+
"200": {
562+
"description": "OK",
563+
"content": {
564+
"application/json": {
565+
"schema": {
566+
"$ref": "#/components/schemas/CustomIResultImplementor"
567+
}
568+
}
569+
}
570+
}
571+
}
572+
}
554573
}
555574
},
556575
"components": {
@@ -631,6 +650,17 @@
631650
}
632651
}
633652
},
653+
"CustomIResultImplementor": {
654+
"required": [
655+
"content"
656+
],
657+
"type": "object",
658+
"properties": {
659+
"content": {
660+
"type": "string"
661+
}
662+
}
663+
},
634664
"Item": {
635665
"type": "object",
636666
"properties": {

‎src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,25 @@
551551
}
552552
}
553553
}
554+
},
555+
"/schemas-by-ref/custom-iresult": {
556+
"get": {
557+
"tags": [
558+
"Sample"
559+
],
560+
"responses": {
561+
"200": {
562+
"description": "OK",
563+
"content": {
564+
"application/json": {
565+
"schema": {
566+
"$ref": "#/components/schemas/CustomIResultImplementor"
567+
}
568+
}
569+
}
570+
}
571+
}
572+
}
554573
}
555574
},
556575
"components": {
@@ -631,6 +650,17 @@
631650
}
632651
}
633652
},
653+
"CustomIResultImplementor": {
654+
"required": [
655+
"content"
656+
],
657+
"type": "object",
658+
"properties": {
659+
"content": {
660+
"type": "string"
661+
}
662+
}
663+
},
634664
"Item": {
635665
"type": "object",
636666
"properties": {

‎src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,25 @@
10771077
}
10781078
}
10791079
},
1080+
"/schemas-by-ref/custom-iresult": {
1081+
"get": {
1082+
"tags": [
1083+
"Sample"
1084+
],
1085+
"responses": {
1086+
"200": {
1087+
"description": "OK",
1088+
"content": {
1089+
"application/json": {
1090+
"schema": {
1091+
"$ref": "#/components/schemas/CustomIResultImplementor"
1092+
}
1093+
}
1094+
}
1095+
}
1096+
}
1097+
}
1098+
},
10801099
"/responses/200-add-xml": {
10811100
"get": {
10821101
"tags": [
@@ -1410,6 +1429,17 @@
14101429
}
14111430
}
14121431
},
1432+
"CustomIResultImplementor": {
1433+
"required": [
1434+
"content"
1435+
],
1436+
"type": "object",
1437+
"properties": {
1438+
"content": {
1439+
"type": "string"
1440+
}
1441+
}
1442+
},
14131443
"IFormFile": {
14141444
"type": "string",
14151445
"format": "binary"

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /