diff --git a/src/Workleap.Extensions.OpenAPI.Tests/Workleap.Extensions.OpenAPI.Tests.csproj b/src/Workleap.Extensions.OpenAPI.Tests/Workleap.Extensions.OpenAPI.Tests.csproj index 0c1105d..313a7e6 100644 --- a/src/Workleap.Extensions.OpenAPI.Tests/Workleap.Extensions.OpenAPI.Tests.csproj +++ b/src/Workleap.Extensions.OpenAPI.Tests/Workleap.Extensions.OpenAPI.Tests.csproj @@ -13,7 +13,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Workleap.Extensions.OpenAPI/TypedResult/ExtractSchemaTypeResultFilter.cs b/src/Workleap.Extensions.OpenAPI/TypedResult/ExtractSchemaTypeResultFilter.cs index 40d610c..32f7022 100644 --- a/src/Workleap.Extensions.OpenAPI/TypedResult/ExtractSchemaTypeResultFilter.cs +++ b/src/Workleap.Extensions.OpenAPI/TypedResult/ExtractSchemaTypeResultFilter.cs @@ -1,3 +1,4 @@ +using System.Net.Mime; using System.Reflection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -9,7 +10,7 @@ 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 const string DefaultContentType = "application/json"; + private const string DefaultContentType = MediaTypeNames.Application.Json; public void Apply(OpenApiOperation operation, OperationFilterContext context) { @@ -24,6 +25,13 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) // when the ProducesResponseType attribute is present. if (operation.Responses.TryGetValue(responseMetadata.HttpCode.ToString(), out var existingResponse)) { + // If no content type is specified, three will be added by default: application/json, text/plain, and text/json. + // In this case we want to enforce the proper content type associated with the method's return type. + if (IsDefaultContentTypes(existingResponse.Content)) + { + existingResponse.Content.Clear(); + } + var canEnrichContent = !existingResponse.Content.Any() && responseMetadata.SchemaType != null; if (!canEnrichContent) @@ -59,6 +67,11 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) } } + private static bool IsDefaultContentTypes(IDictionary contentTypes) => + contentTypes.ContainsKey(MediaTypeNames.Application.Json) && + contentTypes.ContainsKey(MediaTypeNames.Text.Plain) && + contentTypes.ContainsKey("text/json"); + internal static IEnumerable GetResponsesMetadata(Type returnType) { // Unwrap Task<> to get the return type @@ -146,11 +159,9 @@ internal static HashSet ExtractResponseCodesFromAttributes(IEnumerable, BadRequest, NotFound - else - { - return new(statusCode, resultType.GenericTypeArguments.First()); - } + return new(statusCode, resultType.GenericTypeArguments.First()); } private static int? ExtractStatusCodeFromType(Type resultType) diff --git a/src/Workleap.Extensions.OpenAPI/Workleap.Extensions.OpenAPI.csproj b/src/Workleap.Extensions.OpenAPI/Workleap.Extensions.OpenAPI.csproj index 46e158a..1c0d6f3 100644 --- a/src/Workleap.Extensions.OpenAPI/Workleap.Extensions.OpenAPI.csproj +++ b/src/Workleap.Extensions.OpenAPI/Workleap.Extensions.OpenAPI.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/tests/WebApi.OpenAPI.SystemTest/ExtractTypeResult/TypedResultNoProducesController.cs b/src/tests/WebApi.OpenAPI.SystemTest/ExtractTypeResult/TypedResultNoProducesController.cs deleted file mode 100644 index be78c24..0000000 --- a/src/tests/WebApi.OpenAPI.SystemTest/ExtractTypeResult/TypedResultNoProducesController.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore.Http.HttpResults; -using Microsoft.AspNetCore.Mvc; - -namespace WebApi.OpenAPI.SystemTest.ExtractTypeResult; - -public class TypedResultNoProducesController -{ - [HttpGet] - [Route("/useApplicationJsonContentType")] - [ProducesResponseType(StatusCodes.Status200OK, "text/plain")] - public Ok TypedResultUseApplicationJsonContentType() - { - return TypedResults.Ok("example"); - } -} \ No newline at end of file diff --git a/src/tests/WebApi.OpenAPI.SystemTest/ExtractTypeResult/TypedResultProperContentTypeController.cs b/src/tests/WebApi.OpenAPI.SystemTest/ExtractTypeResult/TypedResultProperContentTypeController.cs new file mode 100644 index 0000000..a51b91c --- /dev/null +++ b/src/tests/WebApi.OpenAPI.SystemTest/ExtractTypeResult/TypedResultProperContentTypeController.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace WebApi.OpenAPI.SystemTest.ExtractTypeResult; + +public class TypedResultProperContentTypeController +{ + [HttpGet] + [EndpointName("OkNoContentType")] + [Route("/useApplicationJsonContentTypeWithOk")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Ok GivenOkTypedResultAndNoContenTypeThenContentTypeApplicationJson() + { + return TypedResults.Ok(); + } + + [HttpGet] + [EndpointName("TemplatedOkNoContentType")] + [Route("/useApplicationJsonContentTypeWithOk")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Ok GivenTemplatedOkTypedResultAndNoContenTypeThenContentTypeApplicationJson() + { + return TypedResults.Ok("example"); + } + + [HttpGet] + [EndpointName("ResultsNoContentType")] + [Route("/useApplicationJsonContentTypeWithResultsType")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, "text/plain")] + [ProducesResponseType(StatusCodes.Status404NotFound, "text/plain")] + public Results, BadRequest, NotFound> GivenResultsTypeAndNoContentTypeThenContentTypeApplicationJson() + { + return TypedResults.Ok("example"); + } + + [HttpGet] + [EndpointName("OkContentTypeTextPlain")] + [Route("/overwriteContenTypeWithProduceAttributeTextPlainForOk")] + [ProducesResponseType(StatusCodes.Status200OK, "text/plain")] + public Ok GivenOkTypedResultAndContentTypeThenKeepContentType() + { + return TypedResults.Ok(); + } + + [HttpGet] + [EndpointName("TemplatedOkContentTypeTextPlain")] + [Route("/overwriteContenTypeWithProduceAttributeTextPlainForOk")] + [ProducesResponseType(StatusCodes.Status200OK, "text/plain")] + public Ok GivenTemplatedOkTypedResultAndContentTypeThenKeepContentType() + { + return TypedResults.Ok("example"); + } + + [HttpGet] + [EndpointName("ResultsContentTypeTextPlain")] + [Route("/overwriteContenTypeWithProduceAttributeTextPlainForResultsType")] + [ProducesResponseType(StatusCodes.Status200OK, "text/plain")] + [ProducesResponseType(StatusCodes.Status400BadRequest, "text/plain")] + [ProducesResponseType(StatusCodes.Status404NotFound, "text/plain")] + public Results, BadRequest, NotFound> GivenResultsTypeAndContentTypeThenKeepContentType() + { + return TypedResults.Ok("example"); + } +} \ No newline at end of file diff --git a/src/tests/WebApi.OpenAPI.SystemTest/WebApi.OpenAPI.SystemTest.csproj b/src/tests/WebApi.OpenAPI.SystemTest/WebApi.OpenAPI.SystemTest.csproj index 09f8f37..6613e65 100644 --- a/src/tests/WebApi.OpenAPI.SystemTest/WebApi.OpenAPI.SystemTest.csproj +++ b/src/tests/WebApi.OpenAPI.SystemTest/WebApi.OpenAPI.SystemTest.csproj @@ -10,8 +10,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/tests/WebApi.OpenAPI.SystemTest/openapi-v1.yaml b/src/tests/WebApi.OpenAPI.SystemTest/openapi-v1.yaml index 5c446f3..a0f3142 100644 --- a/src/tests/WebApi.OpenAPI.SystemTest/openapi-v1.yaml +++ b/src/tests/WebApi.OpenAPI.SystemTest/openapi-v1.yaml @@ -25,7 +25,7 @@ paths: operationId: GetExplicitOperationIdInName responses: '200': - description: Success + description: OK /explicitOperationIdInSwagger: get: tags: @@ -33,7 +33,7 @@ paths: operationId: GetExplicitOperationIdInSwagger responses: '200': - description: Success + description: OK /noOperationId: get: tags: @@ -41,7 +41,7 @@ paths: operationId: GetNotOperationId responses: '200': - description: Success + description: OK /recordClassRequiredType: get: tags: @@ -49,7 +49,7 @@ paths: operationId: GetRecordClassRequiredType responses: '200': - description: Success + description: OK content: application/json: schema: @@ -61,7 +61,7 @@ paths: operationId: GetClassRequiredType responses: '200': - description: Success + description: OK content: application/json: schema: @@ -75,7 +75,6 @@ paths: - name: id in: path required: true - style: simple schema: type: integer format: int32 @@ -103,7 +102,6 @@ paths: - name: id in: query required: true - style: form schema: type: integer format: int32 @@ -130,7 +128,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -149,13 +146,12 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 responses: '200': - description: Success + description: OK content: application/json: schema: @@ -180,7 +176,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -207,7 +202,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -230,7 +224,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -261,7 +254,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -284,7 +276,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -315,7 +306,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -332,13 +322,12 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 responses: '200': - description: Success + description: OK /withSwaggerResponseAnnotation: get: tags: @@ -359,7 +348,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -382,7 +370,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -413,7 +400,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -444,13 +430,12 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 responses: '200': - description: Success + description: OK '202': description: Accepted content: @@ -481,10 +466,19 @@ paths: application/json: schema: type: string - /useApplicationJsonContentType: + /useApplicationJsonContentTypeWithOk: get: tags: - - TypedResultNoProduces + - TypedResultProperContentType + operationId: OkNoContentType + responses: + '200': + description: OK + /useApplicationJsonContentTypeWithOk: + get: + tags: + - TypedResultProperContentType + operationId: TemplatedOkNoContentType responses: '200': description: OK @@ -492,6 +486,78 @@ paths: application/json: schema: type: string + /useApplicationJsonContentTypeWithResultsType: + get: + tags: + - TypedResultProperContentType + operationId: ResultsNoContentType + responses: + '200': + description: OK + content: + application/json: + schema: + type: string + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + '404': + description: Not Found + content: + text/plain: + schema: + type: string + /overwriteContenTypeWithProduceAttributeTextPlainForOk: + get: + tags: + - TypedResultProperContentType + operationId: OkContentTypeTextPlain + responses: + '200': + description: OK + content: + text/plain: + schema: + type: string + /overwriteContenTypeWithProduceAttributeTextPlainForOk: + get: + tags: + - TypedResultProperContentType + operationId: TemplatedOkContentTypeTextPlain + responses: + '200': + description: OK + content: + text/plain: + schema: + type: string + /overwriteContenTypeWithProduceAttributeTextPlainForResultsType: + get: + tags: + - TypedResultProperContentType + operationId: ResultsContentTypeTextPlain + responses: + '200': + description: OK + content: + text/plain: + schema: + type: string + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + '404': + description: Not Found + content: + text/plain: + schema: + type: string components: schemas: OperationEnum: diff --git a/src/tests/Workleap.Extensions.OpenAPI.Analyzers.Tests/Workleap.Extensions.OpenAPI.Analyzers.Tests.csproj b/src/tests/Workleap.Extensions.OpenAPI.Analyzers.Tests/Workleap.Extensions.OpenAPI.Analyzers.Tests.csproj index e559d78..c93e00e 100644 --- a/src/tests/Workleap.Extensions.OpenAPI.Analyzers.Tests/Workleap.Extensions.OpenAPI.Analyzers.Tests.csproj +++ b/src/tests/Workleap.Extensions.OpenAPI.Analyzers.Tests/Workleap.Extensions.OpenAPI.Analyzers.Tests.csproj @@ -12,7 +12,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/tests/expected-openapi-document.yaml b/src/tests/expected-openapi-document.yaml index 5c446f3..a0f3142 100644 --- a/src/tests/expected-openapi-document.yaml +++ b/src/tests/expected-openapi-document.yaml @@ -25,7 +25,7 @@ paths: operationId: GetExplicitOperationIdInName responses: '200': - description: Success + description: OK /explicitOperationIdInSwagger: get: tags: @@ -33,7 +33,7 @@ paths: operationId: GetExplicitOperationIdInSwagger responses: '200': - description: Success + description: OK /noOperationId: get: tags: @@ -41,7 +41,7 @@ paths: operationId: GetNotOperationId responses: '200': - description: Success + description: OK /recordClassRequiredType: get: tags: @@ -49,7 +49,7 @@ paths: operationId: GetRecordClassRequiredType responses: '200': - description: Success + description: OK content: application/json: schema: @@ -61,7 +61,7 @@ paths: operationId: GetClassRequiredType responses: '200': - description: Success + description: OK content: application/json: schema: @@ -75,7 +75,6 @@ paths: - name: id in: path required: true - style: simple schema: type: integer format: int32 @@ -103,7 +102,6 @@ paths: - name: id in: query required: true - style: form schema: type: integer format: int32 @@ -130,7 +128,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -149,13 +146,12 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 responses: '200': - description: Success + description: OK content: application/json: schema: @@ -180,7 +176,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -207,7 +202,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -230,7 +224,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -261,7 +254,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -284,7 +276,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -315,7 +306,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -332,13 +322,12 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 responses: '200': - description: Success + description: OK /withSwaggerResponseAnnotation: get: tags: @@ -359,7 +348,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -382,7 +370,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -413,7 +400,6 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 @@ -444,13 +430,12 @@ paths: parameters: - name: id in: query - style: form schema: type: integer format: int32 responses: '200': - description: Success + description: OK '202': description: Accepted content: @@ -481,10 +466,19 @@ paths: application/json: schema: type: string - /useApplicationJsonContentType: + /useApplicationJsonContentTypeWithOk: get: tags: - - TypedResultNoProduces + - TypedResultProperContentType + operationId: OkNoContentType + responses: + '200': + description: OK + /useApplicationJsonContentTypeWithOk: + get: + tags: + - TypedResultProperContentType + operationId: TemplatedOkNoContentType responses: '200': description: OK @@ -492,6 +486,78 @@ paths: application/json: schema: type: string + /useApplicationJsonContentTypeWithResultsType: + get: + tags: + - TypedResultProperContentType + operationId: ResultsNoContentType + responses: + '200': + description: OK + content: + application/json: + schema: + type: string + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + '404': + description: Not Found + content: + text/plain: + schema: + type: string + /overwriteContenTypeWithProduceAttributeTextPlainForOk: + get: + tags: + - TypedResultProperContentType + operationId: OkContentTypeTextPlain + responses: + '200': + description: OK + content: + text/plain: + schema: + type: string + /overwriteContenTypeWithProduceAttributeTextPlainForOk: + get: + tags: + - TypedResultProperContentType + operationId: TemplatedOkContentTypeTextPlain + responses: + '200': + description: OK + content: + text/plain: + schema: + type: string + /overwriteContenTypeWithProduceAttributeTextPlainForResultsType: + get: + tags: + - TypedResultProperContentType + operationId: ResultsContentTypeTextPlain + responses: + '200': + description: OK + content: + text/plain: + schema: + type: string + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + '404': + description: Not Found + content: + text/plain: + schema: + type: string components: schemas: OperationEnum: