[{"data":1,"prerenderedAt":280},["ShallowReactive",2],{"/2025/11/15-openapi-and-dotnet-webapi-minimal-api-to-download-file":3},{"id":4,"title":5,"body":6,"date":271,"description":38,"extension":272,"meta":273,"navigation":274,"path":275,"robots":276,"seo":277,"stem":278,"__hash__":279},"posts/2025/11/15 openapi-and-dotnet-webapi-minimal-api-to-download-file.md","15 Openapi And Dotnet Webapi Minimal Api To Download File",{"type":7,"value":8,"toc":268},"minimark",[9,18,21,25,39,41,52,60,62,70,78,80,104,106,117,125,127,130,138,140,148,156,159,167,187,189,195,219,221,228,236,238,241,248,250,258,260],[10,11,13],"post-title",{":date":12},"date",[14,15,17],"h1",{"id":16},"openapi-and-net-web-api-minimal-api-to-download-file","OpenAPI and .NET Web API Minimal Api to Download File",[19,20],"br",{},[22,23,24],"p",{},"I have an endpoint to download file in my .NET Web API application. When I tried to generate client-side code to download file based on the OpenAPI document, I'm suprised to find that it returns void instead of blob or byte array. So, I checked the OpenAPI document and found out that the response is indeed without type, hence void like the following:",[26,27,28],"code-block",{},[29,30,35],"pre",{"className":31,"code":33,"language":34},[32],"language-text","\"responses\": {\n  \"200\": {\n    \"description\": \"OK\"\n  }\n}\n","text",[36,37,33],"code",{"__ignoreMap":38},"",[19,40],{},[22,42,43,44,51],{},"I'm using .NET 9 with built-in OpenAPI support and one of the suggestions is to use ",[36,45,50],{"className":46},[47,48,49],"bg-gray-200","p-2","rounded","TypedResults.File()"," similar to:",[26,53,54],{},[29,55,58],{"className":56,"code":57,"language":34},[32],"TypedResults.File(fileStream, contentType: \"application/octet-stream\", fileDownloadName: \"file.txt\");\n",[36,59,57],{"__ignoreMap":38},[19,61],{},[22,63,64,65,69],{},"However, it didn't pick up the type as well. I remember in .NET 8, I had to use ",[36,66,68],{"className":67},[47,48,49],"IOperationFilter"," to generate the expected OpenAPI document response which, at least, should look like:",[26,71,72],{},[29,73,76],{"className":74,"code":75,"language":34},[32],"\"responses\": {\n  \"200\": {\n    \"description\": \"OK\",\n    \"content\": {\n      \"application/octet-stream\": {\n        \"schema\": {\n          \"type\": \"string\",\n          \"format\": \"binary\"\n        }\n      }\n    }\n  }\n}\n",[36,77,75],{"__ignoreMap":38},[19,79],{},[22,81,82,83,86,87,91,92,103],{},"Perhaps there's .NET 9 equivalent to ",[36,84,68],{"className":85},[47,48,49]," and I found ",[36,88,90],{"className":89},[47,48,49],"AddOperationTransformer"," from ",[93,94,97],"span",{"className":95},[96],"text-blue-600",[98,99,100],"a",{"href":100,"rel":101},"https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/customize-openapi?view=aspnetcore-9.0#use-operation-transformers",[102],"nofollow",". However, it needs to be applied globally and requires more code to filter it if we want it only for a certain endpoint.",[19,105],{},[22,107,108,109,116],{},"Fortunately, per endpoint application is addressed in .NET 10: ",[93,110,112],{"className":111},[96],[98,113,114],{"href":114,"rel":115},"https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/customize-openapi?view=aspnetcore-10.0#use-operation-transformers",[102]," and in .NET 10, I can then apply it for a specific endpoint:",[26,118,119],{},[29,120,123],{"className":121,"code":122,"language":34},[32],"app.MapGet(...)\n    .AddOpenApiOperationTransformer((operation, context, cancellationToken) =>\n    {\n        if (operation.Responses?.TryGetValue(StatusCodes.Status200OK.ToString(), out var okResponse) ?? false)\n        {\n            okResponse.Content?.TryAdd(\"application/octet-stream\", \n                new OpenApiMediaType\n                {\n                    Schema = new OpenApiSchema\n                    {\n                        Type = JsonSchemaType.String,\n                        Format = \"binary\"\n                    }\n                });\n        }\n        return Task.CompletedTask;\n    });\n",[36,124,122],{"__ignoreMap":38},[19,126],{},[22,128,129],{},"Alternatively, we can clear existing generated response:",[26,131,132],{},[29,133,136],{"className":134,"code":135,"language":34},[32],"app.MapGet(...)\n    .AddOpenApiOperationTransformer((operation, context, cancellationToken) =>\n    {\n        var binaryOkResponse = new OpenApiResponse\n        {\n            Content = new Dictionary\u003Cstring, OpenApiMediaType>() {\n                {\n                    \"application/octet-stream\",\n                    new OpenApiMediaType\n                    {\n                        Schema = new OpenApiSchema\n                        {\n                            Type = JsonSchemaType.String,\n                            Format = \"binary\"\n                        }\n                    }\n                }\n            }\n        };\n\n        operation.Responses?.Clear();\n        operation.Responses?.Add(StatusCodes.Status200OK.ToString(), binaryOkResponse);\n\n        return Task.CompletedTask;\n    });\n",[36,137,135],{"__ignoreMap":38},[19,139],{},[22,141,142,143,147],{},"Although it produces the expected response in the OpenAPI document, it feels like forced/hardcoded instead of generating it from the actual response type. So another option is to use ",[36,144,146],{"className":145},[47,48,49],"Produces()",". This should work with .NET 9 as well:",[26,149,150],{},[29,151,154],{"className":152,"code":153,"language":34},[32],"app.MapGet(...)\n    .Produces\u003Cbyte[]>(StatusCodes.Status200OK, contentType: \"application/octet-stream\");\n",[36,155,153],{"__ignoreMap":38},[22,157,158],{},"And the above will produces the following response:",[26,160,161],{},[29,162,165],{"className":163,"code":164,"language":34},[32],"\"responses\": {\n  \"200\": {\n    \"description\": \"OK\",\n    \"content\": {\n      \"application/octet-stream\": {\n        \"schema\": {\n          \"type\": \"string\",\n          \"format\": \"byte\"\n        }\n      }\n    }\n  }\n}\n",[36,166,164],{"__ignoreMap":38},[22,168,169,170,174,175,178,179,186],{},"Notice that the format is byte instead of binary. Basically, ",[171,172,173],"strong",{},"byte"," is used when we want to return json along with the file content. To return only the file itself without json, use ",[171,176,177],{},"binary",". To read more on binary vs byte, please check ",[93,180,182],{"className":181},[96],[98,183,184],{"href":184,"rel":185},"https://swagger.io/docs/specification/v3_0/describing-responses/#response-that-returns-a-file:~:text=The%20user%20name.-,Response%20That%20Returns%20a%20File,-An%20API%20operation",[102],".",[19,188],{},[22,190,191,192,186],{},"Unfortunately, I can't find a way to generate response with format set to binary using ",[36,193,146],{"className":194},[47,48,49],[196,197,198,209],"ul",{},[199,200,201,202,186],"li",{},".NET 9 type and format: ",[93,203,205],{"className":204},[96],[98,206,207],{"href":207,"rel":208},"https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/include-metadata?view=aspnetcore-9.0&tabs=minimal-apis#type-and-format",[102],[199,210,211,212,186],{},".NET 10 type and format: ",[93,213,215],{"className":214},[96],[98,216,217],{"href":217,"rel":218},"https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/include-metadata?view=aspnetcore-10.0&tabs=minimal-apis#type-and-format",[102],[19,220],{},[22,222,223,224,227],{},"Since I still return ",[36,225,50],{"className":226},[47,48,49],", I tried to return the same type with:",[26,229,230],{},[29,231,234],{"className":232,"code":233,"language":34},[32],"app.MapGet(...)\n    .Produces\u003CFileStreamHttpResult>(StatusCodes.Status200OK, contentType: \"application/octet-stream\");\n",[36,235,233],{"__ignoreMap":38},[19,237],{},[22,239,240],{},"Problem with the above is my client-side code generator creates a lot of unnecessary code, so I tried the following:",[26,242,243],{},[29,244,246],{"className":245,"code":153,"language":34},[32],[36,247,153],{"__ignoreMap":38},[19,249],{},[22,251,252,253,257],{},"It produces a better client-side code but somehow, the return value on the client-side code is string instead of blob. Apparently, I need the response format to be binary instead of byte. Since there's no way to do that with Produces, I revert to using ",[36,254,256],{"className":255},[47,48,49],"AddOpenApiOperationTransformer"," which works for my case where generated code returns blob.",[19,259],{},[22,261,262,263,267],{},"Although the server returns ",[36,264,266],{"className":265},[47,48,49],"FileStreamHttpResult"," which is a complex object, Scalar correctly return binary in the body and thus the client correctly extracts blob from the call. One final note is the generated OpenAPI document response can be different from the actual working of the endpoint and in this case, it's our responsibility to keep them in sync.",{"title":38,"searchDepth":269,"depth":269,"links":270},2,[],"2025-11-15T00:00:00.000Z","md",{},true,"/2025/11/15-openapi-and-dotnet-webapi-minimal-api-to-download-file",null,{"title":5,"description":38},"2025/11/15 openapi-and-dotnet-webapi-minimal-api-to-download-file","--nqfYiTT3K2zuMiU62DhZRd0wCnY4Dr4Rh9h29bwyQ",1785167452531]