From d53831c6ac949799e30b904badc1f0bc567ab9b1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 23 Mar 2026 20:18:06 +0000
Subject: [PATCH 1/3] Initial plan
From 4d6cc0690ab93e3201e7f31b9740fd3aeeb668ed Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 23 Mar 2026 20:44:36 +0000
Subject: [PATCH 2/3] feat: add type provider integration tests for
CancellationToken support
Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com>
Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/8363d577-d359-47b3-ae35-5f9003978db0
---
.../SwaggerProvider.ProviderTests.fsproj | 1 +
.../v3/Swashbuckle.CancellationToken.Tests.fs | 43 +++++++++++++++++++
2 files changed, 44 insertions(+)
create mode 100644 tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.CancellationToken.Tests.fs
diff --git a/tests/SwaggerProvider.ProviderTests/SwaggerProvider.ProviderTests.fsproj b/tests/SwaggerProvider.ProviderTests/SwaggerProvider.ProviderTests.fsproj
index ecb8b27..14c5674 100644
--- a/tests/SwaggerProvider.ProviderTests/SwaggerProvider.ProviderTests.fsproj
+++ b/tests/SwaggerProvider.ProviderTests/SwaggerProvider.ProviderTests.fsproj
@@ -30,6 +30,7 @@
+
diff --git a/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.CancellationToken.Tests.fs b/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.CancellationToken.Tests.fs
new file mode 100644
index 0000000..d54ec99
--- /dev/null
+++ b/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.CancellationToken.Tests.fs
@@ -0,0 +1,43 @@
+module Swashbuckle.v3.CancellationTokenTests
+
+open Xunit
+open FsUnitTyped
+open System
+open System.Threading
+open Swashbuckle.v3.ReturnControllersTests
+
+[]
+let ``Call generated method with explicit CancellationToken None``() =
+ task {
+ let! result = api.GetApiReturnBoolean(CancellationToken.None)
+ result |> shouldEqual true
+ }
+
+[]
+let ``Call generated method with valid CancellationTokenSource token``() =
+ task {
+ use cts = new CancellationTokenSource()
+ let! result = api.GetApiReturnInt32(cts.Token)
+ result |> shouldEqual 42
+ }
+
+[]
+let ``Call generated method with already-cancelled token raises OperationCanceledException``() =
+ task {
+ use cts = new CancellationTokenSource()
+ cts.Cancel()
+
+ try
+ let! _ = api.GetApiReturnString(cts.Token)
+ failwith "Expected OperationCanceledException"
+ with
+ | :? OperationCanceledException -> ()
+ | :? System.AggregateException as aex when (aex.InnerException :? OperationCanceledException) -> ()
+ }
+
+[]
+let ``Call POST generated method with explicit CancellationToken None``() =
+ task {
+ let! result = api.PostApiReturnString(CancellationToken.None)
+ result |> shouldEqual "Hello world"
+ }
From e74b3e218d9636cd7e489fab3e03efb3413bce8d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 23 Mar 2026 21:07:53 +0000
Subject: [PATCH 3/3] fix: use Expr.Cast (not Expr.Coerce) for
CancellationToken to prevent invalid IL for structs
Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com>
Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/cd02aac9-3f51-40c9-af2f-11d1eda31d77
---
.../v3/OperationCompiler.fs | 67 +++++++++----------
1 file changed, 33 insertions(+), 34 deletions(-)
diff --git a/src/SwaggerProvider.DesignTime/v3/OperationCompiler.fs b/src/SwaggerProvider.DesignTime/v3/OperationCompiler.fs
index 4167287..33b8705 100644
--- a/src/SwaggerProvider.DesignTime/v3/OperationCompiler.fs
+++ b/src/SwaggerProvider.DesignTime/v3/OperationCompiler.fs
@@ -272,46 +272,45 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
// Locates parameters matching the arguments
let mutable payloadExp = None
- let mutable ctExpr: Expr option = None
+
+ // When the CancellationToken overload is generated, CancellationToken is always appended last.
+ // Extract it by position to avoid name-collision issues and invalid Expr.Coerce
+ // on a struct type (which generates an invalid castclass IL instruction).
+ let apiArgs, ct =
+ let allArgs = List.tail args // skip `this`
+
+ if includeCancellationToken then
+ match List.rev allArgs with
+ | ctArg :: revApiArgs -> List.rev revApiArgs, Expr.Cast(ctArg)
+ | [] -> failwith "Expected CancellationToken argument but argument list was empty"
+ else
+ allArgs, <@ Threading.CancellationToken.None @>
let parameters =
- List.tail args // skip `this` param
+ apiArgs
|> List.choose (function
| ShapeVar sVar as expr ->
- // cancellationToken is added by the compiler, not from OpenAPI spec
- if sVar.Name = "cancellationToken" then
- ctExpr <-
- Some(
- Expr.Coerce(expr, typeof)
- |> Expr.Cast
- )
-
- None
- else
-
- let param =
- openApiParameters
- |> Seq.tryFind(fun x ->
- // pain point: we have to make sure that the set of names we search for here are the same as the set of names generated when we make `parameters` above
- let baseName = niceCamelName x.Name
- baseName = sVar.Name || (unambiguousName x) = sVar.Name)
-
- match param with
- | Some(par) -> Some(par, expr)
- | _ ->
- let payloadType = PayloadType.Parse sVar.Name
-
- match payloadExp with
- | None ->
- payloadExp <- Some(payloadType, Expr.Coerce(expr, typeof))
- None
- | Some _ ->
- failwithf
- $"More than one payload parameter is specified: '%A{payloadType}' & '%A{payloadExp.Value |> fst}'"
+ let param =
+ openApiParameters
+ |> Seq.tryFind(fun x ->
+ // pain point: we have to make sure that the set of names we search for here are the same as the set of names generated when we make `parameters` above
+ let baseName = niceCamelName x.Name
+ baseName = sVar.Name || (unambiguousName x) = sVar.Name)
+
+ match param with
+ | Some(par) -> Some(par, expr)
+ | _ ->
+ let payloadType = PayloadType.Parse sVar.Name
+
+ match payloadExp with
+ | None ->
+ payloadExp <- Some(payloadType, Expr.Coerce(expr, typeof))
+ None
+ | Some _ ->
+ failwithf
+ $"More than one payload parameter is specified: '%A{payloadType}' & '%A{payloadExp.Value |> fst}'"
| _ -> failwithf $"Function '%s{providedMethodName}' does not support functions as arguments.")
- let ct = ctExpr |> Option.defaultValue <@ Threading.CancellationToken.None @>
-
// Makes argument a string // TODO: Make body an exception
let coerceString exp =
let obj = Expr.Coerce(exp, typeof) |> Expr.Cast