Skip to content

Commit 6dc454f

Browse files
authored
Fix up handling for awaitable IResults in ApiExplorer (#57068)
1 parent a9111ad commit 6dc454f

File tree

3 files changed

+154
-6
lines changed

3 files changed

+154
-6
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,13 @@ internal static Dictionary<int, ApiResponseType> ReadResponseMetadata(
228228

229229
foreach (var metadata in responseMetadata)
230230
{
231+
// `IResult` metadata inserted for awaitable types should
232+
// not be considered for response metadata.
233+
if (typeof(IResult).IsAssignableFrom(metadata.Type))
234+
{
235+
continue;
236+
}
237+
231238
var statusCode = metadata.StatusCode;
232239

233240
var apiResponseType = new ApiResponseType

src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -324,12 +324,6 @@ private static void AddSupportedResponseTypes(
324324
{
325325
var responseType = returnType;
326326

327-
// Can't determine anything about IResults yet that's not from extra metadata. IResult<T> could help here.
328-
if (typeof(IResult).IsAssignableFrom(responseType))
329-
{
330-
responseType = typeof(void);
331-
}
332-
333327
// We support attributes (which implement the IApiResponseMetadataProvider) interface
334328
// and types added via the extension methods (which implement IProducesResponseTypeMetadata).
335329
var responseProviderMetadata = endpointMetadata.GetOrderedMetadata<IApiResponseMetadataProvider>();
@@ -338,6 +332,14 @@ private static void AddSupportedResponseTypes(
338332
var defaultErrorType = errorMetadata?.Type ?? typeof(void);
339333
var contentTypes = new MediaTypeCollection();
340334

335+
// If the return type is an IResult or an awaitable IResult, then we should treat it as a void return type
336+
// since we can't infer anything without additional metadata.
337+
if (typeof(IResult).IsAssignableFrom(responseType) ||
338+
producesResponseMetadata.Any(metadata => typeof(IResult).IsAssignableFrom(metadata.Type)))
339+
{
340+
responseType = typeof(void);
341+
}
342+
341343
var responseProviderMetadataTypes = ApiResponseTypeProvider.ReadResponseMetadata(
342344
responseProviderMetadata, responseType, defaultErrorType, contentTypes, out var errorSetByDefault);
343345
var producesResponseMetadataTypes = ApiResponseTypeProvider.ReadResponseMetadata(producesResponseMetadata, responseType);

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

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Security.Claims;
99
using Microsoft.AspNetCore.Builder;
1010
using Microsoft.AspNetCore.Http;
11+
using Microsoft.AspNetCore.Http.HttpResults;
1112
using Microsoft.AspNetCore.Http.Metadata;
1213
using Microsoft.AspNetCore.Mvc.Abstractions;
1314
using Microsoft.AspNetCore.Mvc.Infrastructure;
@@ -273,6 +274,144 @@ public void AddsMultipleResponseFormatsFromMetadataWithIResult()
273274
Assert.Empty(badRequestResponseType.ApiResponseFormats);
274275
}
275276

277+
[Fact]
278+
public void AddsMultipleResponseFormatsForTypedResults()
279+
{
280+
var apiDescription = GetApiDescription(Results<Created<InferredJsonClass>, BadRequest> () =>
281+
TypedResults.Created("https://example.com", new InferredJsonClass()));
282+
283+
Assert.Equal(2, apiDescription.SupportedResponseTypes.Count);
284+
285+
var createdResponseType = apiDescription.SupportedResponseTypes[0];
286+
287+
Assert.Equal(201, createdResponseType.StatusCode);
288+
Assert.Equal(typeof(InferredJsonClass), createdResponseType.Type);
289+
Assert.Equal(typeof(InferredJsonClass), createdResponseType.ModelMetadata?.ModelType);
290+
291+
var createdResponseFormat = Assert.Single(createdResponseType.ApiResponseFormats);
292+
Assert.Equal("application/json", createdResponseFormat.MediaType);
293+
294+
var badRequestResponseType = apiDescription.SupportedResponseTypes[1];
295+
296+
Assert.Equal(400, badRequestResponseType.StatusCode);
297+
Assert.Equal(typeof(void), badRequestResponseType.Type);
298+
Assert.Equal(typeof(void), badRequestResponseType.ModelMetadata?.ModelType);
299+
300+
Assert.Empty(badRequestResponseType.ApiResponseFormats);
301+
}
302+
303+
[Fact]
304+
public void AddsResponseFormatsForTypedResultWithoutReturnType()
305+
{
306+
var apiDescription = GetApiDescription(() => TypedResults.Created("https://example.com", new InferredJsonClass()));
307+
308+
Assert.Equal(1, apiDescription.SupportedResponseTypes.Count);
309+
310+
var createdResponseType = apiDescription.SupportedResponseTypes[0];
311+
312+
Assert.Equal(201, createdResponseType.StatusCode);
313+
Assert.Equal(typeof(InferredJsonClass), createdResponseType.Type);
314+
Assert.Equal(typeof(InferredJsonClass), createdResponseType.ModelMetadata?.ModelType);
315+
316+
var createdResponseFormat = Assert.Single(createdResponseType.ApiResponseFormats);
317+
Assert.Equal("application/json", createdResponseFormat.MediaType);
318+
}
319+
320+
[Fact]
321+
public void HandlesTypedResultsWithoutIEndpointMetadataProviderImplementation()
322+
{
323+
// TypedResults for ProblemDetails doesn't implement IEndpointMetadataProvider
324+
var apiDescription = GetApiDescription(() => TypedResults.Problem());
325+
326+
Assert.Equal(1, apiDescription.SupportedResponseTypes.Count);
327+
328+
var responseType = apiDescription.SupportedResponseTypes[0];
329+
330+
Assert.Equal(200, responseType.StatusCode);
331+
Assert.Equal(typeof(void), responseType.Type);
332+
Assert.Equal(typeof(void), responseType.ModelMetadata?.ModelType);
333+
}
334+
335+
[Fact]
336+
public void AddsResponseFormatsForAwaitableTypedResultWithoutReturnType()
337+
{
338+
var apiDescription = GetApiDescription(() =>
339+
Task.FromResult(TypedResults.Created("https://example.com", new InferredJsonClass())));
340+
341+
Assert.Equal(1, apiDescription.SupportedResponseTypes.Count);
342+
343+
var createdResponseType = apiDescription.SupportedResponseTypes[0];
344+
345+
Assert.Equal(201, createdResponseType.StatusCode);
346+
Assert.Equal(typeof(InferredJsonClass), createdResponseType.Type);
347+
Assert.Equal(typeof(InferredJsonClass), createdResponseType.ModelMetadata?.ModelType);
348+
349+
var createdResponseFormat = Assert.Single(createdResponseType.ApiResponseFormats);
350+
Assert.Equal("application/json", createdResponseFormat.MediaType);
351+
}
352+
353+
// Coverage for https://github.com/dotnet/aspnetcore/issues/56975
354+
[Fact]
355+
public void AddsMultipleResponseFormatsFromMetadataWithAwaitableIResult()
356+
{
357+
var apiDescription = GetApiDescription(
358+
[ProducesResponseType(typeof(InferredJsonClass), StatusCodes.Status201Created)]
359+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
360+
() => Task.FromResult(Results.Ok(new InferredJsonClass())));
361+
362+
Assert.Equal(2, apiDescription.SupportedResponseTypes.Count);
363+
364+
var createdResponseType = apiDescription.SupportedResponseTypes[0];
365+
366+
Assert.Equal(201, createdResponseType.StatusCode);
367+
Assert.Equal(typeof(InferredJsonClass), createdResponseType.Type);
368+
Assert.Equal(typeof(InferredJsonClass), createdResponseType.ModelMetadata?.ModelType);
369+
370+
var createdResponseFormat = Assert.Single(createdResponseType.ApiResponseFormats);
371+
Assert.Equal("application/json", createdResponseFormat.MediaType);
372+
373+
var badRequestResponseType = apiDescription.SupportedResponseTypes[1];
374+
375+
Assert.Equal(400, badRequestResponseType.StatusCode);
376+
Assert.Equal(typeof(void), badRequestResponseType.Type);
377+
Assert.Equal(typeof(void), badRequestResponseType.ModelMetadata?.ModelType);
378+
379+
Assert.Empty(badRequestResponseType.ApiResponseFormats);
380+
}
381+
382+
[Fact]
383+
public void AddsMultipleResponseFormatsFromMetadataWithAwaitableResultType()
384+
{
385+
var apiDescription = GetApiDescription(
386+
[ProducesResponseType(typeof(InferredJsonClass), StatusCodes.Status201Created)]
387+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
388+
async Task<Results<Created<InferredJsonClass>, ProblemHttpResult>> () => {
389+
await Task.CompletedTask;
390+
return Random.Shared.Next() % 2 == 0
391+
? TypedResults.Created<InferredJsonClass>("/", new InferredJsonClass())
392+
: TypedResults.Problem();
393+
});
394+
395+
Assert.Equal(2, apiDescription.SupportedResponseTypes.Count);
396+
397+
var createdResponseType = apiDescription.SupportedResponseTypes[0];
398+
399+
Assert.Equal(201, createdResponseType.StatusCode);
400+
Assert.Equal(typeof(InferredJsonClass), createdResponseType.Type);
401+
Assert.Equal(typeof(InferredJsonClass), createdResponseType.ModelMetadata?.ModelType);
402+
403+
var createdResponseFormat = Assert.Single(createdResponseType.ApiResponseFormats);
404+
Assert.Equal("application/json", createdResponseFormat.MediaType);
405+
406+
var badRequestResponseType = apiDescription.SupportedResponseTypes[1];
407+
408+
Assert.Equal(400, badRequestResponseType.StatusCode);
409+
Assert.Equal(typeof(void), badRequestResponseType.Type);
410+
Assert.Equal(typeof(void), badRequestResponseType.ModelMetadata?.ModelType);
411+
412+
Assert.Empty(badRequestResponseType.ApiResponseFormats);
413+
}
414+
276415
[Fact]
277416
public void AddsFromRouteParameterAsPath()
278417
{

0 commit comments

Comments
 (0)