Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/SwaggerProvider.DesignTime/v3/OperationCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type PayloadType =
| AppOctetStream
| AppFormUrlEncoded
| MultipartFormData
| TextPlain

override x.ToString() =
match x with
Expand All @@ -36,6 +37,7 @@ type PayloadType =
| AppOctetStream -> "octetStream"
| AppFormUrlEncoded -> "formUrlEncoded"
| MultipartFormData -> "formData"
| TextPlain -> "textPlain"

member x.ToMediaType() =
match x with
Expand All @@ -44,6 +46,7 @@ type PayloadType =
| AppOctetStream -> MediaTypes.ApplicationOctetStream
| AppFormUrlEncoded -> MediaTypes.ApplicationFormUrlEncoded
| MultipartFormData -> MediaTypes.MultipartFormData
| TextPlain -> MediaTypes.TextPlain

static member Parse =
function
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) @>
Expand Down
6 changes: 6 additions & 0 deletions src/SwaggerProvider.Runtime/RuntimeHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ module MediaTypes =
[<Literal>]
let MultipartFormData = "multipart/form-data"

[<Literal>]
let TextPlain = "text/plain"

type AsyncExtensions() =
static member cast<'t> asyncOp =
async {
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ let ``Return text/plain GET Test``() =
[<Fact>]
let ``Return text/csv GET Test``() =
api.GetApiReturnCsv() |> asyncEqual "Hello,world"

[<Fact>]
let ``Send & return text/plain POST Test``() =
api.PostApiConsumesText("hello") |> asyncEqual "hello"
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,6 +21,13 @@ type ReturnCsvController() =
member this.Get() =
"Hello,world" |> ActionResult<string>

[<Route("api/[controller]")>]
[<ApiController>]
type ConsumesTextController() =
[<HttpPost; Consumes("text/plain"); Produces("text/plain")>]
member this.Post([<FromBody>] request: string) =
request |> ActionResult<string>

// Simple CSV output formatter
// This formatter assumes the controller returns a string (already CSV-formatted)
type CsvOutputFormatter() as this =
Expand All @@ -38,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 this.CanRead(context) =
base.CanRead(context) && context.ModelType = typeof<string>

override _.ReadRequestBodyAsync(context, encoding) =
task {
use reader = new StreamReader(context.HttpContext.Request.Body, encoding)
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The StreamReader should be constructed with leaveOpen: true to prevent it from disposing the underlying Request.Body stream. When the StreamReader is disposed by the use binding, it will by default also dispose the Request.Body stream, which is owned by ASP.NET Core and should not be disposed by the formatter.

Suggested change
use reader = new StreamReader(context.HttpContext.Request.Body, encoding)
use reader = new StreamReader(context.HttpContext.Request.Body, encoding, true, 1024, true)

Copilot uses AI. Check for mistakes.
let! content = reader.ReadToEndAsync()
return InputFormatterResult.Success(content)
}
4 changes: 3 additions & 1 deletion tests/Swashbuckle.WebApi.Server/Startup.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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.Add(TextPlainInputFormatter()))
|> ignore
// Register the Swagger & OpenApi services
services.AddSwaggerGen(fun c ->
Expand Down
Loading