From 5a8f3f4ba66434a35b10dd7fc3166632ad6df443 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 22:26:30 +0000 Subject: [PATCH 1/2] feat: add CancellationToken support to SwaggerClientProvider (v2) generated methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #336 which added CancellationToken support to the v3 OpenApiClientProvider. This change brings the same capability to the v2 SwaggerClientProvider. Every generated method now has an optional cancellationToken parameter appended last (defaulting to CancellationToken.None), so existing code continues to compile without changes. Changes: - v2/OperationCompiler.fs: compute ctArgIndex, append CT ProvidedParameter, extract CT in invokeCode, use this.CallAsync(msg, [], [], ct) so the CancellationToken flows through to HttpClient.SendAsync — consistent with the v3 pattern. - v2/Swashbuckle.CancellationToken.Tests.fs: 10 integration tests mirroring the v3 CT test suite (with/without CT, cancelled token, async variant). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../v2/OperationCompiler.fs | 28 +++- .../SwaggerProvider.ProviderTests.fsproj | 1 + .../v2/Swashbuckle.CancellationToken.Tests.fs | 120 ++++++++++++++++++ 3 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 tests/SwaggerProvider.ProviderTests/v2/Swashbuckle.CancellationToken.Tests.fs diff --git a/src/SwaggerProvider.DesignTime/v2/OperationCompiler.fs b/src/SwaggerProvider.DesignTime/v2/OperationCompiler.fs index 5e266c9..8afd8ec 100644 --- a/src/SwaggerProvider.DesignTime/v2/OperationCompiler.fs +++ b/src/SwaggerProvider.DesignTime/v2/OperationCompiler.fs @@ -56,6 +56,18 @@ type OperationCompiler(schema: SwaggerObject, defCompiler: DefinitionCompiler, i // reverse it again so that all required properties come first |> List.rev + // Append an optional CancellationToken parameter last (after all OpenAPI params). + // Using a UniqueNameGenerator avoids collisions with existing parameter names. + let ctArgIndex = List.length parameters + + let parameters = + let scope = UniqueNameGenerator() + parameters |> List.iter(fun p -> scope.MakeUnique p.Name |> ignore) + let ctName = scope.MakeUnique "cancellationToken" + + parameters + @ [ ProvidedParameter(ctName, typeof, false, null) ] + // find the inner type value let retTy = let okResponse = @@ -110,9 +122,15 @@ type OperationCompiler(schema: SwaggerObject, defCompiler: DefinitionCompiler, i "Content-Type", MediaTypes.ApplicationJson |] @> - // Locates parameters matching the arguments + // Extract CancellationToken (appended at ctArgIndex) and separate from OpenAPI params. + let allArgs = List.tail args // skip `this` param + let ct = List.item ctArgIndex allArgs |> Expr.Cast + + // Locates parameters matching the arguments (excluding CT arg) let parameters = - List.tail args // skip `this` param + allArgs + |> List.indexed + |> List.choose(fun (i, arg) -> if i = ctArgIndex then None else Some arg) |> List.map (function | ShapeVar sVar as expr -> let param = @@ -212,11 +230,7 @@ type OperationCompiler(schema: SwaggerObject, defCompiler: DefinitionCompiler, i <@ let msg = %httpRequestMessageWithPayload RuntimeHelpers.fillHeaders msg %heads - - task { - let! response = (%this).HttpClient.SendAsync(msg) - return response.EnsureSuccessStatusCode().Content - } + (%this).CallAsync(msg, [||], [||], %ct) @> let responseObj = diff --git a/tests/SwaggerProvider.ProviderTests/SwaggerProvider.ProviderTests.fsproj b/tests/SwaggerProvider.ProviderTests/SwaggerProvider.ProviderTests.fsproj index 14c5674..c79cb40 100644 --- a/tests/SwaggerProvider.ProviderTests/SwaggerProvider.ProviderTests.fsproj +++ b/tests/SwaggerProvider.ProviderTests/SwaggerProvider.ProviderTests.fsproj @@ -12,6 +12,7 @@ + diff --git a/tests/SwaggerProvider.ProviderTests/v2/Swashbuckle.CancellationToken.Tests.fs b/tests/SwaggerProvider.ProviderTests/v2/Swashbuckle.CancellationToken.Tests.fs new file mode 100644 index 0000000..a0a29ac --- /dev/null +++ b/tests/SwaggerProvider.ProviderTests/v2/Swashbuckle.CancellationToken.Tests.fs @@ -0,0 +1,120 @@ +module Swashbuckle.v2.CancellationTokenTests + +open Xunit +open FsUnitTyped +open System +open System.Net.Http +open System.Threading +open SwaggerProvider +open Swashbuckle.v2.ReturnControllersTests + +type WebAPIAsync = + SwaggerClientProvider<"http://localhost:5000/swagger/v1/swagger.json", IgnoreOperationId=true, SsrfProtection=false, PreferAsync=true> + +let apiAsync = + let handler = new HttpClientHandler(UseCookies = false) + + let client = + new HttpClient(handler, true, BaseAddress = Uri("http://localhost:5000")) + + WebAPIAsync.Client(client) + +[] +let ``v2 Call generated method without CancellationToken uses default token``() = + task { + let! result = api.GetApiReturnBoolean() + result |> shouldEqual true + } + +[] +let ``v2 Call generated method with explicit CancellationToken None``() = + task { + let! result = api.GetApiReturnBoolean(CancellationToken.None) + result |> shouldEqual true + } + +[] +let ``v2 Call generated method with valid CancellationTokenSource token``() = + task { + use cts = new CancellationTokenSource() + let! result = api.GetApiReturnInt32(cts.Token) + result |> shouldEqual 42 + } + +[] +let ``v2 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 ``v2 Call POST generated method with explicit CancellationToken None``() = + task { + let! result = api.PostApiReturnString(CancellationToken.None) + result |> shouldEqual "Hello world" + } + +[] +let ``v2 Call async generated method without CancellationToken uses default token``() = + async { + let! result = apiAsync.GetApiReturnBoolean() + result |> shouldEqual true + } + |> Async.StartAsTask + +[] +let ``v2 Call method with required param and explicit CancellationToken``() = + task { + use cts = new CancellationTokenSource() + let! result = api.GetApiUpdateString("Serge", cts.Token) + result |> shouldEqual "Hello, Serge" + } + +[] +let ``v2 Call method with optional param and explicit CancellationToken``() = + task { + use cts = new CancellationTokenSource() + let! result = api.GetApiUpdateBool(Some true, cts.Token) + result |> shouldEqual false + } + +[] +let ``v2 Call async generated method with explicit CancellationToken``() = + async { + use cts = new CancellationTokenSource() + let! result = apiAsync.GetApiReturnInt32(cts.Token) + result |> shouldEqual 42 + } + |> Async.StartAsTask + +[] +let ``v2 Call async generated method with already-cancelled token raises OperationCanceledException``() = + async { + use cts = new CancellationTokenSource() + cts.Cancel() + + try + let! _ = apiAsync.GetApiReturnString(cts.Token) + failwith "Expected OperationCanceledException" + with + | :? OperationCanceledException -> () + | :? AggregateException as aex when (aex.InnerException :? OperationCanceledException) -> () + } + |> Async.StartAsTask + +[] +let ``v2 Call async POST generated method with explicit CancellationToken``() = + async { + use cts = new CancellationTokenSource() + let! result = apiAsync.PostApiReturnString(cts.Token) + result |> shouldEqual "Hello world" + } + |> Async.StartAsTask From 2fc3e49d797892ad5078b5fc70df214f63dde116 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 29 Mar 2026 22:26:32 +0000 Subject: [PATCH 2/2] ci: trigger checks