Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
28 changes: 26 additions & 2 deletions src/SwaggerProvider.DesignTime/v3/OperationCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
| true, mediaTyObj -> Some(mediaTyObj)
| _ -> None

let (|TextReturn|_|)(input: string) =
if input.StartsWith("text/") then Some(input) else None

let (|TextMediaType|_|)(content: IDictionary<string, OpenApiMediaType>) =
content.Keys |> Seq.tryPick (|TextReturn|_|)

let (|NoMediaType|_|)(content: IDictionary<string, OpenApiMediaType>) =
if content.Count = 0 then Some() else None

Expand Down Expand Up @@ -176,6 +182,7 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
defCompiler.CompileTy providedMethodName "Response" mediaTy.Schema true

Some(MediaTypes.ApplicationOctetStream, ty)
| TextMediaType mediaTy -> Some(mediaTy, typeof<string>)
| _ -> None)

let retMime = retMimeAndTy |> Option.map fst |> Option.defaultValue null
Expand Down Expand Up @@ -373,6 +380,17 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
}
@>

let responseString =
<@
let x = %action

task {
let! response = x
let! data = response.ReadAsStringAsync()
return data
}
@>

let responseUnit =
<@
let x = %action
Expand All @@ -389,15 +407,21 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
match retTy with
| None -> responseUnit.Raw
| Some t when t = typeof<IO.Stream> -> <@ %responseStream @>.Raw
| Some t -> Expr.Coerce(<@ RuntimeHelpers.taskCast t %responseObj @>, overallReturnType)
| Some t ->
match retMime with
| TextReturn _ -> <@ %responseString @>.Raw
| _ -> Expr.Coerce(<@ RuntimeHelpers.taskCast t %responseObj @>, overallReturnType)
else
let awaitTask t =
<@ Async.AwaitTask(%t) @>

match retTy with
| None -> (awaitTask responseUnit).Raw
| Some t when t = typeof<IO.Stream> -> <@ %(awaitTask responseStream) @>.Raw
| Some t -> Expr.Coerce(<@ RuntimeHelpers.asyncCast t %(awaitTask responseObj) @>, overallReturnType)
| Some t ->
match retMime with
| TextReturn _ -> <@ %(awaitTask responseString) @>.Raw
| _ -> Expr.Coerce(<@ RuntimeHelpers.asyncCast t %(awaitTask responseObj) @>, overallReturnType)
)

if not <| String.IsNullOrEmpty(operation.Summary) then
Expand Down
1 change: 0 additions & 1 deletion src/SwaggerProvider.Runtime/RuntimeHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ module MediaTypes =
[<Literal>]
let MultipartFormData = "multipart/form-data"


type AsyncExtensions() =
static member cast<'t> asyncOp =
async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<Compile Include="v3\Swagger.I0181.Tests.fs" />
<Compile Include="v3\Swagger.I0219.Tests.fs" />
<Compile Include="v3\Swashbuckle.ReturnControllers.Tests.fs" />
<Compile Include="v3\Swashbuckle.ReturnTextControllers.Tests.fs" />
<Compile Include="v3\Swashbuckle.UpdateControllers.Tests.fs" />
<Compile Include="v3\Swashbuckle.ResourceControllers.Tests.fs" />
<Compile Include="v3\Swashbuckle.FileController.Tests.fs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module Swashbuckle.v3.ReturnTextControllersTests

open Xunit
open FsUnitTyped
open SwaggerProvider
open System
open System.Net.Http

open Swashbuckle.v3.ReturnControllersTests

let asyncEqual expected actualTask =
task {
let! actual = actualTask
actual |> shouldEqual expected
}

[<Fact>]
let ``Return text/plain GET Test``() =
api.GetApiReturnPlain() |> asyncEqual "Hello world"

[<Fact>]
let ``Return text/csv GET Test``() =
api.GetApiReturnCsv() |> asyncEqual "Hello,world"
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace Swashbuckle.WebApi.Server.Controllers

open System.Text
open Microsoft.AspNetCore.Mvc
open Microsoft.AspNetCore.Mvc.Formatters
open Swagger.Internal

[<Route("api/[controller]")>]
[<ApiController>]
type ReturnPlainController() =
[<HttpGet; Consumes(MediaTypes.ApplicationJson); Produces("text/plain")>]
member this.Get() =
"Hello world" |> ActionResult<string>

[<Route("api/[controller]")>]
[<ApiController>]
type ReturnCsvController() =
[<HttpGet; Consumes(MediaTypes.ApplicationJson); Produces("text/csv")>]
Copy link

Copilot AI Oct 11, 2025

Choose a reason for hiding this comment

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

Using Consumes attribute for a GET request that doesn't accept a request body is unnecessary and potentially confusing. GET requests typically don't consume request bodies.

Suggested change
[<HttpGet; Consumes(MediaTypes.ApplicationJson); Produces("text/plain")>]
member this.Get() =
"Hello world" |> ActionResult<string>
[<Route("api/[controller]")>]
[<ApiController>]
type ReturnCsvController() =
[<HttpGet; Consumes(MediaTypes.ApplicationJson); Produces("text/csv")>]
[<HttpGet; Produces("text/plain")>]
member this.Get() =
"Hello world" |> ActionResult<string>
[<Route("api/[controller]")>]
[<ApiController>]
type ReturnCsvController() =
[<HttpGet; Produces("text/csv")>]

Copilot uses AI. Check for mistakes.
member this.Get() =
"Hello,world" |> ActionResult<string>

// Simple CSV output formatter
// This formatter assumes the controller returns a string (already CSV-formatted)
type CsvOutputFormatter() as this =
inherit TextOutputFormatter()

do
this.SupportedMediaTypes.Add("text/csv")
this.SupportedEncodings.Add(Encoding.UTF8)
this.SupportedEncodings.Add(Encoding.Unicode)

override _.CanWriteType(t) =
// Accept string type only (for simplicity)
t = typeof<string>

override _.WriteResponseBodyAsync(context, encoding) =
let response = context.HttpContext.Response
let value = context.Object :?> string
let bytes = encoding.GetBytes(value)
response.Body.WriteAsync(bytes, 0, bytes.Length)
1 change: 1 addition & 0 deletions tests/Swashbuckle.WebApi.Server/Startup.fs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Startup private () =
let converters = options.JsonSerializerOptions.Converters
converters.Add(JsonFSharpConverter())
converters.Add(JsonStringEnumConverter()))
.AddMvcOptions(_.OutputFormatters.Add(CsvOutputFormatter()))
|> ignore
// Register the Swagger & OpenApi services
services.AddSwaggerGen(fun c ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<ItemGroup>
<Compile Include="Controllers\Types.fs" />
<Compile Include="Controllers\ReturnControllers.fs" />
<Compile Include="Controllers\ReturnTextControllers.fs" />
<Compile Include="Controllers\UpdateControllers.fs" />
<Compile Include="Controllers\ResourceControllers.fs" />
<Compile Include="Controllers\ValuesController.fs" />
Expand Down
Loading