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