Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions build.fsx.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
STORAGE: NONE
RESTRICTION: || (== net6.0) (== netstandard2.0)
NUGET
remote: https://api.nuget.org/v3/index.json
FSharp.Core (9.0.303)
3 changes: 3 additions & 0 deletions src/SwaggerProvider.DesignTime/v3/OperationCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
defCompiler.CompileTy providedMethodName "Response" mediaTy.Schema true

Some(MediaTypes.ApplicationOctetStream, ty)
| content when content.Keys |> Seq.exists MediaTypes.isTextMediaType ->
let textKey = content.Keys |> Seq.find MediaTypes.isTextMediaType
Copy link
Member

Choose a reason for hiding this comment

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

here you can avoid code duplication and double enumeration.

This is perfect place to use F# Active Patterns, you can refactor isTextMediaType to active pattern that match and return matched value in the same time

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the tips ❤️! As you can guess, I'm not quite F#luent yet...

I got this to a point where the text/plain test passes but text/csv does not and I can't figure out why... I'm so lost as I can't just run the test in debug mode and use breakpoints in Rider 😅

Copy link
Member

Choose a reason for hiding this comment

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

i think that this log line is the key (aspnetcore does not know how to format csv)

warn: Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor[2]
      No output formatter was found for content types 'text/csv, text/csv' to write the response.

Some(textKey, typeof<string>)
| _ -> None)

let retMime = retMimeAndTy |> Option.map fst |> Option.defaultValue null
Expand Down
3 changes: 3 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"

let isTextMediaType(mediaType: string) =
not(isNull mediaType) && mediaType.StartsWith("text/")


type AsyncExtensions() =
static member cast<'t> asyncOp =
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,18 @@
namespace Swashbuckle.WebApi.Server.Controllers

open Microsoft.AspNetCore.Mvc
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>
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