Skip to content

Commit

Permalink
[IDP-1424] Only return application/json content type for Typed respon…
Browse files Browse the repository at this point in the history
…ses (#16)

* [IDP-1424] Only return application/json content type for Typed responses

* Update readme
  • Loading branch information
heqianwang committed May 10, 2024
1 parent 80454ba commit cd1721b
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 33 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ public Ok<TypedResultExample> EnforceStronglyTypedResponse() //This is a strongl

### Limitations

We currently only support the extraction of the default media type and not any globally defined content types such as [`{ "application/json", "text/json", "text/plain", }`](https://learn.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-8.0)
[Given that HttpResults return types do not leverage the configured Formatters](https://learn.microsoft.com/en-us/aspnet/core/web-api/action-return-types?view=aspnetcore-8.0#httpresults-type), content negotiation is not supported for these endpoints and the produced Content-Type is decided by HttpResults implementation. For our use cases, it will be `application/json`.


## Building, releasing and versioning

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
Expand All @@ -8,11 +7,10 @@ namespace Workleap.Extensions.OpenAPI.TypedResult;
internal sealed class ExtractSchemaTypeResultFilter : IOperationFilter
{
// Based on this documentation: https://learn.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-8.0
private static readonly IReadOnlyList<string> DefaultContentTypes = new List<string>() { "application/json", "text/json", "text/plain", };
private const string DefaultContentType = "application/json";

public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var contentTypes = GetContentTypes(context);
foreach (var responseMetadata in GetResponsesMetadata(context.MethodInfo.ReturnType))
{
// If the response content is already set, we won't overwrite it. This is the case for minimal APIs and
Expand All @@ -33,41 +31,13 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)
if (responseMetadata.SchemaType != null)
{
var schema = context.SchemaGenerator.GenerateSchema(responseMetadata.SchemaType, context.SchemaRepository);

foreach (var contentType in contentTypes)
{
response.Content.Add(contentType, new OpenApiMediaType { Schema = schema });
}
response.Content.Add(DefaultContentType, new OpenApiMediaType { Schema = schema });
}

operation.Responses[responseMetadata.HttpCode.ToString()] = response;
}
}

private static IReadOnlyCollection<string> GetContentTypes(OperationFilterContext context)
{
var methodProducesAttribute = context.MethodInfo.GetCustomAttribute<Microsoft.AspNetCore.Mvc.ProducesAttribute>();
if (methodProducesAttribute != null)
{
return methodProducesAttribute.ContentTypes.ToList();
}

var controllerProducesAttribute = context.MethodInfo.DeclaringType?.GetCustomAttribute<Microsoft.AspNetCore.Mvc.ProducesAttribute>();
if (controllerProducesAttribute != null)
{
return controllerProducesAttribute.ContentTypes.ToList();
}

var endpointProducesAttribute = context.ApiDescription.ActionDescriptor.EndpointMetadata.OfType<Microsoft.AspNetCore.Mvc.ProducesAttribute>().FirstOrDefault();
if (endpointProducesAttribute != null)
{
return endpointProducesAttribute.ContentTypes.ToList();
}

// Fallback on default content types, not supporting globally defined content types
return DefaultContentTypes;
}

internal static IEnumerable<ResponseMetadata> GetResponsesMetadata(Type returnType)
{
// Unwrap Task<> to get the return type
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

namespace WebApi.OpenAPI.SystemTest.ExtractTypeResult;

public class TypedResultNoProducesController
{
[HttpGet]
[Route("/useApplicationJsonContentType")]
[ProducesResponseType<string>(StatusCodes.Status200OK, "text/plain")]
public Ok<string> TypedResultUseApplicationJsonContentType()
{
return TypedResults.Ok("example");
}
}
11 changes: 11 additions & 0 deletions src/tests/WebApi.OpenAPI.SystemTest/openapi-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,17 @@ paths:
application/json:
schema:
type: string
/useApplicationJsonContentType:
get:
tags:
- TypedResultNoProduces
responses:
'200':
description: '200'
content:
application/json:
schema:
type: string
components:
schemas:
ProblemDetails:
Expand Down
11 changes: 11 additions & 0 deletions src/tests/expected-openapi-document.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,17 @@ paths:
application/json:
schema:
type: string
/useApplicationJsonContentType:
get:
tags:
- TypedResultNoProduces
responses:
'200':
description: '200'
content:
application/json:
schema:
type: string
components:
schemas:
ProblemDetails:
Expand Down

0 comments on commit cd1721b

Please sign in to comment.