From cb2f8396caaee6429a4a88c34f02a2dc8f021c8b Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Fri, 28 Nov 2025 17:02:18 +0100 Subject: [PATCH 1/3] Support new HTTP result types in OpenAPI schema generation Added support for `FileStreamResult`, `FileContentHttpResult`, and `FileStreamHttpResult` in `OpenApiSchemaService`. These types are now represented as binary string schemas in OpenAPI documentation. Updated `JsonTypeInfoExtensionsTests` to include test cases for the new types, ensuring proper handling and naming in the schema. Added new test cases in `OpenApiSchemaServiceTests` to verify OpenAPI response handling for the new HTTP result types, validating schema generation with appropriate content types. These changes improve compatibility with new ASP.NET Core features and enhance the accuracy of API documentation. --- .../Services/Schemas/OpenApiSchemaService.cs | 7 +- .../Extensions/JsonTypeInfoExtensionsTests.cs | 3 + .../OpenApiSchemaService.ResponseSchemas.cs | 70 +++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs index 44bf7dbd3da5..97af32f3fbb3 100644 --- a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs +++ b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs @@ -12,6 +12,7 @@ using System.Text.Json.Schema; using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -58,9 +59,11 @@ internal sealed class OpenApiSchemaService( TransformSchemaNode = (context, schema) => { var type = context.TypeInfo.Type; - // Fix up schemas generated for IFormFile, IFormFileCollection, Stream, PipeReader and FileContentResult + // Fix up schemas generated for IFormFile, IFormFileCollection, Stream, PipeReader, + // FileContentResult, FileStreamResult, FileContentHttpResult and FileContentHttpResult // that appear as properties within complex types. - if (type == typeof(IFormFile) || type == typeof(Stream) || type == typeof(PipeReader) || type == typeof(Mvc.FileContentResult)) + if (type == typeof(IFormFile) || type == typeof(Stream) || type == typeof(PipeReader) + || type == typeof(Mvc.FileContentResult) || type == typeof(Mvc.FileStreamResult) || type == typeof(FileContentHttpResult) || type == typeof(FileStreamHttpResult)) { schema = new JsonObject { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonTypeInfoExtensionsTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonTypeInfoExtensionsTests.cs index a9ae3255708b..a71b4a6681d1 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonTypeInfoExtensionsTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonTypeInfoExtensionsTests.cs @@ -64,6 +64,9 @@ public class Baz [typeof(Stream), "Stream"], [typeof(PipeReader), "PipeReader"], [typeof(FileContentResult), "FileContentResult"], + [typeof(FileStreamResult), "FileStreamResult"], + [typeof(FileContentHttpResult), "FileContentHttpResult"], + [typeof(FileStreamHttpResult), "FileStreamHttpResult"], [typeof(Results, Ok>), "ResultsOfOkOfTodoWithDueDateAndOkOfTodo"], [typeof(Ok), "OkOfTodo"], [typeof(NotFound), "NotFoundOfTodoWithDueDate"], diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ResponseSchemas.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ResponseSchemas.cs index 05aaeb956848..37a83b464bf2 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ResponseSchemas.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ResponseSchemas.cs @@ -7,6 +7,7 @@ using System.Text.Json.Nodes; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; public partial class OpenApiSchemaServiceTests : OpenApiDocumentServiceTestBase @@ -1055,6 +1056,75 @@ await VerifyOpenApiDocument(builder, document => }); } + [Fact] + public async Task GetOpenApiResponse_HandlesFileStreamResultTypeResponse() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapPost("/filestreamresult", () => { return new FileStreamResult(new MemoryStream(), MediaTypeNames.Application.Octet); }) + .Produces(contentType: MediaTypeNames.Application.Octet); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/filestreamresult"].Operations[HttpMethod.Post]; + var responses = Assert.Single(operation.Responses); + var response = responses.Value; + Assert.True(response.Content.TryGetValue("application/octet-stream", out var mediaType)); + var schema = mediaType.Schema; + Assert.Equal(JsonSchemaType.String, schema.Type); + Assert.Equal("binary", schema.Format); + }); + } + + [Fact] + public async Task GetOpenApiResponse_HandlesFileContentHttpResultTypeResponse() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapPost("/filecontenthttpresult", () => { return TypedResults.File([], MediaTypeNames.Image.Png); }) + .Produces(contentType: MediaTypeNames.Image.Png); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/filecontenthttpresult"].Operations[HttpMethod.Post]; + var responses = Assert.Single(operation.Responses); + var response = responses.Value; + Assert.True(response.Content.TryGetValue("image/png", out var mediaType)); + var schema = mediaType.Schema; + Assert.Equal(JsonSchemaType.String, schema.Type); + Assert.Equal("binary", schema.Format); + }); + } + + [Fact] + public async Task GetOpenApiResponse_HandlesFileContentStreamResultTypeResponse() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapPost("/filestreamhttpresult", () => { return TypedResults.File(new MemoryStream(), MediaTypeNames.Application.Pdf); }) + .Produces(contentType: MediaTypeNames.Application.Pdf); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/filestreamhttpresult"].Operations[HttpMethod.Post]; + var responses = Assert.Single(operation.Responses); + var response = responses.Value; + Assert.True(response.Content.TryGetValue("application/pdf", out var mediaType)); + var schema = mediaType.Schema; + Assert.Equal(JsonSchemaType.String, schema.Type); + Assert.Equal("binary", schema.Format); + }); + } + [ApiController] [Produces("application/json")] public class TestController From cce993f56d413ee8aa6e3dbddf5e4b0a569a644b Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Fri, 28 Nov 2025 17:05:12 +0100 Subject: [PATCH 2/3] Fix a typo --- src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs index 97af32f3fbb3..05c56849d5cd 100644 --- a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs +++ b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs @@ -60,7 +60,7 @@ internal sealed class OpenApiSchemaService( { var type = context.TypeInfo.Type; // Fix up schemas generated for IFormFile, IFormFileCollection, Stream, PipeReader, - // FileContentResult, FileStreamResult, FileContentHttpResult and FileContentHttpResult + // FileContentResult, FileStreamResult, FileContentHttpResult and FileStreamHttpResult // that appear as properties within complex types. if (type == typeof(IFormFile) || type == typeof(Stream) || type == typeof(PipeReader) || type == typeof(Mvc.FileContentResult) || type == typeof(Mvc.FileStreamResult) || type == typeof(FileContentHttpResult) || type == typeof(FileStreamHttpResult)) From 82511f2b32704252b9ea12eea00ed503a93b2545 Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Fri, 28 Nov 2025 17:19:13 +0100 Subject: [PATCH 3/3] Apply code style suggestions --- src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs | 3 ++- .../OpenApiSchemaService.ResponseSchemas.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs index 05c56849d5cd..aeef62f85dc0 100644 --- a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs +++ b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs @@ -63,7 +63,8 @@ internal sealed class OpenApiSchemaService( // FileContentResult, FileStreamResult, FileContentHttpResult and FileStreamHttpResult // that appear as properties within complex types. if (type == typeof(IFormFile) || type == typeof(Stream) || type == typeof(PipeReader) - || type == typeof(Mvc.FileContentResult) || type == typeof(Mvc.FileStreamResult) || type == typeof(FileContentHttpResult) || type == typeof(FileStreamHttpResult)) + || type == typeof(Mvc.FileContentResult) || type == typeof(Mvc.FileStreamResult) + || type == typeof(FileContentHttpResult) || type == typeof(FileStreamHttpResult)) { schema = new JsonObject { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ResponseSchemas.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ResponseSchemas.cs index 37a83b464bf2..7cf014d29290 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ResponseSchemas.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ResponseSchemas.cs @@ -1103,7 +1103,7 @@ await VerifyOpenApiDocument(builder, document => } [Fact] - public async Task GetOpenApiResponse_HandlesFileContentStreamResultTypeResponse() + public async Task GetOpenApiResponse_HandlesFileStreamHttpResultTypeResponse() { // Arrange var builder = CreateBuilder();