From 4ec68012da3ceffc89451b12c2bad3011981ce25 Mon Sep 17 00:00:00 2001 From: Richard Webb Date: Mon, 15 Dec 2025 12:06:18 +0000 Subject: [PATCH 1/6] Add a test controller which consumes text/plain content --- .../Controllers/ConsumesTextController.fs | 11 +++++++++++ .../Swashbuckle.WebApi.Server.fsproj | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 tests/Swashbuckle.WebApi.Server/Controllers/ConsumesTextController.fs diff --git a/tests/Swashbuckle.WebApi.Server/Controllers/ConsumesTextController.fs b/tests/Swashbuckle.WebApi.Server/Controllers/ConsumesTextController.fs new file mode 100644 index 0000000..1ea3596 --- /dev/null +++ b/tests/Swashbuckle.WebApi.Server/Controllers/ConsumesTextController.fs @@ -0,0 +1,11 @@ +namespace Swashbuckle.WebApi.Server.Controllers + +open Microsoft.AspNetCore.Mvc +open Swagger.Internal + +[] +[] +type ConsumesTextController() = + [] + member this.Post([] request: string) = + request |> ActionResult diff --git a/tests/Swashbuckle.WebApi.Server/Swashbuckle.WebApi.Server.fsproj b/tests/Swashbuckle.WebApi.Server/Swashbuckle.WebApi.Server.fsproj index b70e960..1587810 100644 --- a/tests/Swashbuckle.WebApi.Server/Swashbuckle.WebApi.Server.fsproj +++ b/tests/Swashbuckle.WebApi.Server/Swashbuckle.WebApi.Server.fsproj @@ -1,4 +1,4 @@ - + net9.0 @@ -16,6 +16,7 @@ + From 163c000d2922ebbf2f0630c81b60e9a8fa8b544d Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Mon, 15 Dec 2025 21:58:47 +0100 Subject: [PATCH 2/6] feat: call new endpoint --- .../v3/Swashbuckle.ReturnTextControllers.Tests.fs | 4 ++++ .../Controllers/ConsumesTextController.fs | 11 ----------- .../Controllers/ReturnTextControllers.fs | 7 +++++++ .../Swashbuckle.WebApi.Server.fsproj | 3 +-- 4 files changed, 12 insertions(+), 13 deletions(-) delete mode 100644 tests/Swashbuckle.WebApi.Server/Controllers/ConsumesTextController.fs diff --git a/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.ReturnTextControllers.Tests.fs b/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.ReturnTextControllers.Tests.fs index 7eea5fd..4545259 100644 --- a/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.ReturnTextControllers.Tests.fs +++ b/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.ReturnTextControllers.Tests.fs @@ -15,3 +15,7 @@ let ``Return text/plain GET Test``() = [] let ``Return text/csv GET Test``() = api.GetApiReturnCsv() |> asyncEqual "Hello,world" + +[] +let ``Send & return text/plain POST Test``() = + api.GetApiConsumesText("hello") |> asyncEqual "hello" diff --git a/tests/Swashbuckle.WebApi.Server/Controllers/ConsumesTextController.fs b/tests/Swashbuckle.WebApi.Server/Controllers/ConsumesTextController.fs deleted file mode 100644 index 1ea3596..0000000 --- a/tests/Swashbuckle.WebApi.Server/Controllers/ConsumesTextController.fs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Swashbuckle.WebApi.Server.Controllers - -open Microsoft.AspNetCore.Mvc -open Swagger.Internal - -[] -[] -type ConsumesTextController() = - [] - member this.Post([] request: string) = - request |> ActionResult diff --git a/tests/Swashbuckle.WebApi.Server/Controllers/ReturnTextControllers.fs b/tests/Swashbuckle.WebApi.Server/Controllers/ReturnTextControllers.fs index 70c58dd..fdb9d81 100644 --- a/tests/Swashbuckle.WebApi.Server/Controllers/ReturnTextControllers.fs +++ b/tests/Swashbuckle.WebApi.Server/Controllers/ReturnTextControllers.fs @@ -19,6 +19,13 @@ type ReturnCsvController() = member this.Get() = "Hello,world" |> ActionResult +[] +[] +type ConsumesTextController() = + [] + member this.Post([] request: string) = + request |> ActionResult + // Simple CSV output formatter // This formatter assumes the controller returns a string (already CSV-formatted) type CsvOutputFormatter() as this = diff --git a/tests/Swashbuckle.WebApi.Server/Swashbuckle.WebApi.Server.fsproj b/tests/Swashbuckle.WebApi.Server/Swashbuckle.WebApi.Server.fsproj index 1587810..b70e960 100644 --- a/tests/Swashbuckle.WebApi.Server/Swashbuckle.WebApi.Server.fsproj +++ b/tests/Swashbuckle.WebApi.Server/Swashbuckle.WebApi.Server.fsproj @@ -1,4 +1,4 @@ - + net9.0 @@ -16,7 +16,6 @@ - From be5a27f9f55823824686c4a86768b22e81b22efc Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Mon, 22 Dec 2025 12:02:18 +0100 Subject: [PATCH 3/6] feat: add plain text support fot request payload --- .../v3/OperationCompiler.fs | 12 ++++++++++++ src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 6 ++++++ .../v3/Swashbuckle.ReturnTextControllers.Tests.fs | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/SwaggerProvider.DesignTime/v3/OperationCompiler.fs b/src/SwaggerProvider.DesignTime/v3/OperationCompiler.fs index 524e7a1..3498e20 100644 --- a/src/SwaggerProvider.DesignTime/v3/OperationCompiler.fs +++ b/src/SwaggerProvider.DesignTime/v3/OperationCompiler.fs @@ -28,6 +28,7 @@ type PayloadType = | AppOctetStream | AppFormUrlEncoded | MultipartFormData + | TextPlain override x.ToString() = match x with @@ -36,6 +37,7 @@ type PayloadType = | AppOctetStream -> "octetStream" | AppFormUrlEncoded -> "formUrlEncoded" | MultipartFormData -> "formData" + | TextPlain -> "textPlain" member x.ToMediaType() = match x with @@ -44,6 +46,7 @@ type PayloadType = | AppOctetStream -> MediaTypes.ApplicationOctetStream | AppFormUrlEncoded -> MediaTypes.ApplicationFormUrlEncoded | MultipartFormData -> MediaTypes.MultipartFormData + | TextPlain -> MediaTypes.TextPlain static member Parse = function @@ -52,6 +55,7 @@ type PayloadType = | "octetStream" -> AppOctetStream | "formUrlEncoded" -> AppFormUrlEncoded | "formData" -> MultipartFormData + | "textPlain" -> TextPlain | name -> failwithf $"Payload '%s{name}' is not supported" /// Object for compiling operations. @@ -114,6 +118,7 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler, | MediaType MediaTypes.ApplicationOctetStream mediaTyObj -> formatAndParam AppOctetStream mediaTyObj.Schema | MediaType MediaTypes.MultipartFormData mediaTyObj -> formatAndParam MultipartFormData mediaTyObj.Schema | MediaType MediaTypes.ApplicationFormUrlEncoded mediaTyObj -> formatAndParam AppFormUrlEncoded mediaTyObj.Schema + | MediaType MediaTypes.TextPlain mediaTyObj -> formatAndParam TextPlain mediaTyObj.Schema | NoMediaType -> // Assume that server treat it as `applicationJson` let defSchema = OpenApiSchema() // todo: we need to test it @@ -359,6 +364,13 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler, msg.Content <- RuntimeHelpers.toFormUrlEncodedContent(data) msg @> + | Some(TextPlain, textObj) -> + <@ + let text = (%%textObj: obj).ToString() + let msg = %httpRequestMessage + msg.Content <- RuntimeHelpers.toTextContent(text) + msg + @> let action = <@ (%this).CallAsync(%httpRequestMessageWithPayload, errorCodes, errorDescriptions) @> diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 3663698..10bb2ee 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -18,6 +18,9 @@ module MediaTypes = [] let MultipartFormData = "multipart/form-data" + [] + let TextPlain = "text/plain" + type AsyncExtensions() = static member cast<'t> asyncOp = async { @@ -127,6 +130,9 @@ module RuntimeHelpers = let toStringContent(valueStr: string) = new StringContent(valueStr, Text.Encoding.UTF8, "application/json") + let toTextContent(valueStr: string) = + new StringContent(valueStr, Text.Encoding.UTF8, "text/plain") + let toStreamContent(boxedStream: obj) = match boxedStream with | :? IO.Stream as stream -> new StreamContent(stream) diff --git a/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.ReturnTextControllers.Tests.fs b/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.ReturnTextControllers.Tests.fs index 4545259..19c6498 100644 --- a/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.ReturnTextControllers.Tests.fs +++ b/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.ReturnTextControllers.Tests.fs @@ -18,4 +18,4 @@ let ``Return text/csv GET Test``() = [] let ``Send & return text/plain POST Test``() = - api.GetApiConsumesText("hello") |> asyncEqual "hello" + api.PostApiConsumesText("hello") |> asyncEqual "hello" From 4db73dd3ac23b9cec7fcfd3599f3913d20206b99 Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Mon, 22 Dec 2025 12:10:48 +0100 Subject: [PATCH 4/6] feat: add plain text input formatter --- .../Controllers/ReturnTextControllers.fs | 21 +++++++++++++++++++ tests/Swashbuckle.WebApi.Server/Startup.fs | 4 +++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/Swashbuckle.WebApi.Server/Controllers/ReturnTextControllers.fs b/tests/Swashbuckle.WebApi.Server/Controllers/ReturnTextControllers.fs index fdb9d81..03fc728 100644 --- a/tests/Swashbuckle.WebApi.Server/Controllers/ReturnTextControllers.fs +++ b/tests/Swashbuckle.WebApi.Server/Controllers/ReturnTextControllers.fs @@ -1,6 +1,8 @@ namespace Swashbuckle.WebApi.Server.Controllers +open System.IO open System.Text +open System.Threading.Tasks open Microsoft.AspNetCore.Mvc open Microsoft.AspNetCore.Mvc.Formatters open Swagger.Internal @@ -45,3 +47,22 @@ type CsvOutputFormatter() as this = let value = context.Object :?> string let bytes = encoding.GetBytes(value) response.Body.WriteAsync(bytes, 0, bytes.Length) + +// Text/plain input formatter for reading plain text request bodies +type TextPlainInputFormatter() as this = + inherit TextInputFormatter() + + do + this.SupportedMediaTypes.Add("text/plain") + this.SupportedEncodings.Add(Encoding.UTF8) + this.SupportedEncodings.Add(Encoding.Unicode) + + override _.CanRead(context) = + context.ModelType = typeof + + override _.ReadRequestBodyAsync(context, encoding) = + task { + use reader = new StreamReader(context.HttpContext.Request.Body, encoding) + let! content = reader.ReadToEndAsync() + return InputFormatterResult.Success(content) + } diff --git a/tests/Swashbuckle.WebApi.Server/Startup.fs b/tests/Swashbuckle.WebApi.Server/Startup.fs index 87f55c4..bf31d47 100644 --- a/tests/Swashbuckle.WebApi.Server/Startup.fs +++ b/tests/Swashbuckle.WebApi.Server/Startup.fs @@ -24,7 +24,9 @@ type Startup private () = let converters = options.JsonSerializerOptions.Converters converters.Add(JsonFSharpConverter()) converters.Add(JsonStringEnumConverter())) - .AddMvcOptions(_.OutputFormatters.Add(CsvOutputFormatter())) + .AddMvcOptions(fun options -> + options.OutputFormatters.Add(CsvOutputFormatter()) + options.InputFormatters.Insert(0, TextPlainInputFormatter())) |> ignore // Register the Swagger & OpenApi services services.AddSwaggerGen(fun c -> From 60308a051d9022f467150408917d048a1c927d0b Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Mon, 22 Dec 2025 12:20:40 +0100 Subject: [PATCH 5/6] fix: input formatter order --- tests/Swashbuckle.WebApi.Server/Startup.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Swashbuckle.WebApi.Server/Startup.fs b/tests/Swashbuckle.WebApi.Server/Startup.fs index bf31d47..f36a2d3 100644 --- a/tests/Swashbuckle.WebApi.Server/Startup.fs +++ b/tests/Swashbuckle.WebApi.Server/Startup.fs @@ -26,7 +26,7 @@ type Startup private () = converters.Add(JsonStringEnumConverter())) .AddMvcOptions(fun options -> options.OutputFormatters.Add(CsvOutputFormatter()) - options.InputFormatters.Insert(0, TextPlainInputFormatter())) + options.InputFormatters.Add(TextPlainInputFormatter())) |> ignore // Register the Swagger & OpenApi services services.AddSwaggerGen(fun c -> From 7f5a41e9c68877ba62a43d13455d2a037662b0b1 Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Mon, 22 Dec 2025 12:21:10 +0100 Subject: [PATCH 6/6] fix: canread on formatter --- .../Controllers/ReturnTextControllers.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Swashbuckle.WebApi.Server/Controllers/ReturnTextControllers.fs b/tests/Swashbuckle.WebApi.Server/Controllers/ReturnTextControllers.fs index 03fc728..494a069 100644 --- a/tests/Swashbuckle.WebApi.Server/Controllers/ReturnTextControllers.fs +++ b/tests/Swashbuckle.WebApi.Server/Controllers/ReturnTextControllers.fs @@ -57,8 +57,8 @@ type TextPlainInputFormatter() as this = this.SupportedEncodings.Add(Encoding.UTF8) this.SupportedEncodings.Add(Encoding.Unicode) - override _.CanRead(context) = - context.ModelType = typeof + override this.CanRead(context) = + base.CanRead(context) && context.ModelType = typeof override _.ReadRequestBodyAsync(context, encoding) = task {