From 97a4a2570f58008ced5e149964daea1a934517cd Mon Sep 17 00:00:00 2001 From: "b.sizov" Date: Sun, 15 Mar 2026 12:37:25 +0300 Subject: [PATCH 1/2] fix(binary): Add IFormFile support for binary in multipart requests - Added IFormFile generation for using binary in multipart/form-data - Added test for this case --- .../BinaryTests.cs | 58 +++++++++++++ ...f_object_in_CSharp_ASPNETCore.verified.txt | 81 +++++++++++++++++++ .../Models/CSharpOperationModel.cs | 37 ++++++--- 3 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 src/NSwag.CodeGeneration.CSharp.Tests/Snapshots/BinaryTests.When_body_is_binary_then_IFormFile_is_used_as_part_of_object_in_CSharp_ASPNETCore.verified.txt diff --git a/src/NSwag.CodeGeneration.CSharp.Tests/BinaryTests.cs b/src/NSwag.CodeGeneration.CSharp.Tests/BinaryTests.cs index 16e3e4860..cf3271c8d 100644 --- a/src/NSwag.CodeGeneration.CSharp.Tests/BinaryTests.cs +++ b/src/NSwag.CodeGeneration.CSharp.Tests/BinaryTests.cs @@ -55,6 +55,64 @@ public async Task When_body_is_binary_then_stream_is_used_as_parameter_in_CSharp CSharpCompiler.AssertCompile(code); } + [Fact] + public async Task When_body_is_binary_then_IFormFile_is_used_as_part_of_object_in_CSharp_ASPNETCore() + { + var yaml = @"openapi: 3.0.0 +servers: + - url: https://www.example.com/ +info: + version: '2.0.0' + title: 'Test API' +paths: + /files: + post: + tags: + - Files + summary: 'Add File' + operationId: addFile + responses: + '200': + description: 'something' + content: + application/json: + schema: + $ref: '#/components/schemas/FileToken' + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + name: + type: string +components: + schemas: + FileToken: + type: object + required: + - fileId + properties: + fileId: + type: string + format: uuid"; + + var document = await OpenApiYamlDocument.FromYamlAsync(yaml); + + // Act + CSharpControllerGeneratorSettings settings = new CSharpControllerGeneratorSettings(); + settings.ControllerTarget = CSharpControllerTarget.AspNetCore; + var codeGenerator = new CSharpControllerGenerator(document, settings); + var code = codeGenerator.GenerateFile(); + + // Assert + await VerifyHelper.Verify(code); + CSharpCompiler.AssertCompile(code); + } + [Fact] public async Task When_body_is_binary_then_IFormFile_is_used_as_parameter_in_CSharp_ASPNETCore() { diff --git a/src/NSwag.CodeGeneration.CSharp.Tests/Snapshots/BinaryTests.When_body_is_binary_then_IFormFile_is_used_as_part_of_object_in_CSharp_ASPNETCore.verified.txt b/src/NSwag.CodeGeneration.CSharp.Tests/Snapshots/BinaryTests.When_body_is_binary_then_IFormFile_is_used_as_part_of_object_in_CSharp_ASPNETCore.verified.txt new file mode 100644 index 000000000..31b1f49d4 --- /dev/null +++ b/src/NSwag.CodeGeneration.CSharp.Tests/Snapshots/BinaryTests.When_body_is_binary_then_IFormFile_is_used_as_part_of_object_in_CSharp_ASPNETCore.verified.txt @@ -0,0 +1,81 @@ + + +namespace MyNamespace +{ + using System = global::System; + + public interface IController + { + + + + + System.Threading.Tasks.Task AddFileAsync(Microsoft.AspNetCore.Http.IFormFile file, string name); + + } + + + public partial class Controller : Microsoft.AspNetCore.Mvc.ControllerBase + { + private IController _implementation; + + public Controller(IController implementation) + { + _implementation = implementation; + } + + [Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("files")] + public System.Threading.Tasks.Task AddFile(Microsoft.AspNetCore.Http.IFormFile file, string name) + { + + return _implementation.AddFileAsync(file, name); + } + + } + + public partial class FileToken + { + + [Newtonsoft.Json.JsonProperty("fileId", Required = Newtonsoft.Json.Required.Always)] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public System.Guid FileId { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties; + + [Newtonsoft.Json.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + public partial class FileParameter + { + public FileParameter(System.IO.Stream data) + : this (data, null, null) + { + } + + public FileParameter(System.IO.Stream data, string fileName) + : this (data, fileName, null) + { + } + + public FileParameter(System.IO.Stream data, string fileName, string contentType) + { + Data = data; + FileName = fileName; + ContentType = contentType; + } + + public System.IO.Stream Data { get; private set; } + + public string FileName { get; private set; } + + public string ContentType { get; private set; } + } + + +} diff --git a/src/NSwag.CodeGeneration.CSharp/Models/CSharpOperationModel.cs b/src/NSwag.CodeGeneration.CSharp/Models/CSharpOperationModel.cs index cb9cc8071..98bfa19ed 100644 --- a/src/NSwag.CodeGeneration.CSharp/Models/CSharpOperationModel.cs +++ b/src/NSwag.CodeGeneration.CSharp/Models/CSharpOperationModel.cs @@ -268,21 +268,24 @@ protected override string ResolveParameterType(OpenApiParameter parameter) "System.Collections.Generic.ICollection" : "System.Collections.Generic.ICollection"; } - else - { - return controllerSettings.ControllerTarget == CSharpControllerTarget.AspNetCore ? - "Microsoft.AspNetCore.Http.IFormFile" : - "System.Web.HttpPostedFileBase"; - } - } - else - { - return parameter.HasBinaryBodyWithMultipleMimeTypes ? "FileParameter" : "System.IO.Stream"; + + return controllerSettings.ControllerTarget == CSharpControllerTarget.AspNetCore ? + "Microsoft.AspNetCore.Http.IFormFile" : + "System.Web.HttpPostedFileBase"; } + + return parameter.HasBinaryBodyWithMultipleMimeTypes ? "FileParameter" : "System.IO.Stream"; } if (schema.Type == JsonObjectType.Array && (schema.Item?.IsBinary ?? false)) { + if (_settings is CSharpControllerGeneratorSettings controllerSettings) + { + return controllerSettings.ControllerTarget == CSharpControllerTarget.AspNetCore ? + "System.Collections.Generic.ICollection" : + "System.Collections.Generic.ICollection"; + } + return "System.Collections.Generic.IEnumerable"; } @@ -290,9 +293,23 @@ protected override string ResolveParameterType(OpenApiParameter parameter) { if (parameter.CollectionFormat == OpenApiParameterCollectionFormat.Multi && !schema.Type.HasFlag(JsonObjectType.Array)) { + if (_settings is CSharpControllerGeneratorSettings controllerSettings) + { + return controllerSettings.ControllerTarget == CSharpControllerTarget.AspNetCore ? + "System.Collections.Generic.ICollection" : + "System.Collections.Generic.ICollection"; + } + return "System.Collections.Generic.IEnumerable"; } + if (_settings is CSharpControllerGeneratorSettings controllerSettings1) + { + return controllerSettings1.ControllerTarget == CSharpControllerTarget.AspNetCore ? + "Microsoft.AspNetCore.Http.IFormFile" : + "System.Web.HttpPostedFileBase"; + } + return "FileParameter"; } From 3c7c3d494c625220c39342eda5799362bae16d8b Mon Sep 17 00:00:00 2001 From: "b.sizov" Date: Sun, 15 Mar 2026 13:00:46 +0300 Subject: [PATCH 2/2] refactor: Small refactor --- .../Models/CSharpOperationModel.cs | 55 +++++++++---------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/src/NSwag.CodeGeneration.CSharp/Models/CSharpOperationModel.cs b/src/NSwag.CodeGeneration.CSharp/Models/CSharpOperationModel.cs index 98bfa19ed..1fe7d0a8a 100644 --- a/src/NSwag.CodeGeneration.CSharp/Models/CSharpOperationModel.cs +++ b/src/NSwag.CodeGeneration.CSharp/Models/CSharpOperationModel.cs @@ -264,14 +264,10 @@ protected override string ResolveParameterType(OpenApiParameter parameter) { if (schema.Type == JsonObjectType.Array && schema.Item.IsBinary) { - return controllerSettings.ControllerTarget == CSharpControllerTarget.AspNetCore ? - "System.Collections.Generic.ICollection" : - "System.Collections.Generic.ICollection"; + return ResolveControllerBinaryCollectionType(controllerSettings); } - return controllerSettings.ControllerTarget == CSharpControllerTarget.AspNetCore ? - "Microsoft.AspNetCore.Http.IFormFile" : - "System.Web.HttpPostedFileBase"; + return ResolveControllerBinarySingleType(controllerSettings); } return parameter.HasBinaryBodyWithMultipleMimeTypes ? "FileParameter" : "System.IO.Stream"; @@ -279,38 +275,25 @@ protected override string ResolveParameterType(OpenApiParameter parameter) if (schema.Type == JsonObjectType.Array && (schema.Item?.IsBinary ?? false)) { - if (_settings is CSharpControllerGeneratorSettings controllerSettings) - { - return controllerSettings.ControllerTarget == CSharpControllerTarget.AspNetCore ? - "System.Collections.Generic.ICollection" : - "System.Collections.Generic.ICollection"; - } - - return "System.Collections.Generic.IEnumerable"; + return _settings is CSharpControllerGeneratorSettings controllerSettings + ? ResolveControllerBinaryCollectionType(controllerSettings) + : "System.Collections.Generic.IEnumerable"; } if (schema.IsBinary) { if (parameter.CollectionFormat == OpenApiParameterCollectionFormat.Multi && !schema.Type.HasFlag(JsonObjectType.Array)) { - if (_settings is CSharpControllerGeneratorSettings controllerSettings) - { - return controllerSettings.ControllerTarget == CSharpControllerTarget.AspNetCore ? - "System.Collections.Generic.ICollection" : - "System.Collections.Generic.ICollection"; - } - - return "System.Collections.Generic.IEnumerable"; + return _settings is CSharpControllerGeneratorSettings controllerSettings + ? ResolveControllerBinaryCollectionType(controllerSettings) + : "System.Collections.Generic.IEnumerable"; } - - if (_settings is CSharpControllerGeneratorSettings controllerSettings1) + else { - return controllerSettings1.ControllerTarget == CSharpControllerTarget.AspNetCore ? - "Microsoft.AspNetCore.Http.IFormFile" : - "System.Web.HttpPostedFileBase"; + return _settings is CSharpControllerGeneratorSettings controllerSettings + ? ResolveControllerBinarySingleType(controllerSettings) + : "FileParameter"; } - - return "FileParameter"; } return base.ResolveParameterType(parameter) @@ -318,6 +301,20 @@ protected override string ResolveParameterType(OpenApiParameter parameter) .Replace(_settings.CSharpGeneratorSettings.DictionaryType + "<", _settings.ParameterDictionaryType + "<"); } + private static string ResolveControllerBinarySingleType(CSharpControllerGeneratorSettings controllerSettings) + { + return controllerSettings.ControllerTarget == CSharpControllerTarget.AspNetCore ? + "Microsoft.AspNetCore.Http.IFormFile" : + "System.Web.HttpPostedFileBase"; + } + + private static string ResolveControllerBinaryCollectionType(CSharpControllerGeneratorSettings controllerSettings) + { + return controllerSettings.ControllerTarget == CSharpControllerTarget.AspNetCore ? + "System.Collections.Generic.ICollection" : + "System.Collections.Generic.ICollection"; + } + /// Creates the response model. /// The operation. /// The status code.