Skip to content

Commit 16053ea

Browse files
committed
feat: propagate CancellationToken through ReadAsStringAsync/ReadAsStreamAsync via RuntimeHelpers
Add readContentAsString and readContentAsStream wrappers to RuntimeHelpers with #if NET5_0_OR_GREATER guards, enabling CancellationToken propagation in generated quotation code that must compile against netstandard2.0. Also add explicit CancellationToken integration tests and conditional CT support in ProvidedApiClientBase error path.
1 parent e06b5b0 commit 16053ea

File tree

5 files changed

+52
-3
lines changed

5 files changed

+52
-3
lines changed

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## Git Policy
2+
3+
- **NEVER commit or push unless the user explicitly asks you to.** Only create commits when directly requested.
4+
15
## Build, Test & Lint Commands
26

37
- **Build**: `dotnet fake build -t Build` (Release configuration)

src/SwaggerProvider.DesignTime/v3/OperationCompiler.fs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -428,32 +428,35 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
428428

429429
<@
430430
let x = %action
431+
let ct = %ct
431432

432433
task {
433434
let! response = x
434-
let! content = response.ReadAsStringAsync()
435+
let! content = RuntimeHelpers.readContentAsString response ct
435436
return (%this).Deserialize(content, innerReturnType)
436437
}
437438
@>
438439

439440
let responseStream =
440441
<@
441442
let x = %action
443+
let ct = %ct
442444

443445
task {
444446
let! response = x
445-
let! data = response.ReadAsStreamAsync()
447+
let! data = RuntimeHelpers.readContentAsStream response ct
446448
return data
447449
}
448450
@>
449451

450452
let responseString =
451453
<@
452454
let x = %action
455+
let ct = %ct
453456

454457
task {
455458
let! response = x
456-
let! data = response.ReadAsStringAsync()
459+
let! data = RuntimeHelpers.readContentAsString response ct
457460
return data
458461
}
459462
@>

src/SwaggerProvider.Runtime/ProvidedApiClientBase.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,11 @@ type ProvidedApiClientBase(httpClient: HttpClient, options: JsonSerializerOption
6363
let! body =
6464
task {
6565
try
66+
#if NET5_0_OR_GREATER
67+
return! response.Content.ReadAsStringAsync(cancellationToken)
68+
#else
6669
return! response.Content.ReadAsStringAsync()
70+
#endif
6771
with _ ->
6872
// If reading the body fails (e.g., disposed stream or invalid charset),
6973
// fall back to an empty body so we can still throw OpenApiException.

src/SwaggerProvider.Runtime/RuntimeHelpers.fs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,20 @@ module RuntimeHelpers =
273273

274274
castFn.MakeGenericMethod([| runtimeTy |]).Invoke(null, [| asyncOp |])
275275

276+
let readContentAsString (content: HttpContent) (ct: System.Threading.CancellationToken) : Task<string> =
277+
#if NET5_0_OR_GREATER
278+
content.ReadAsStringAsync(ct)
279+
#else
280+
content.ReadAsStringAsync()
281+
#endif
282+
283+
let readContentAsStream (content: HttpContent) (ct: System.Threading.CancellationToken) : Task<IO.Stream> =
284+
#if NET5_0_OR_GREATER
285+
content.ReadAsStreamAsync(ct)
286+
#else
287+
content.ReadAsStreamAsync()
288+
#endif
289+
276290
let taskCast runtimeTy (task: Task<obj>) =
277291
let castFn = typeof<TaskExtensions>.GetMethod "cast"
278292

tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.CancellationToken.Tests.fs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,27 @@ let ``Call async generated method without CancellationToken uses default token``
6868
let! result = apiAsync.GetApiReturnBoolean()
6969
result |> shouldEqual true
7070
}
71+
72+
[<Fact>]
73+
let ``Call method with required param and explicit CancellationToken``() =
74+
task {
75+
use cts = new CancellationTokenSource()
76+
let! result = api.GetApiUpdateString("Serge", cts.Token)
77+
result |> shouldEqual "Hello, Serge"
78+
}
79+
80+
[<Fact>]
81+
let ``Call method with optional param and explicit CancellationToken``() =
82+
task {
83+
use cts = new CancellationTokenSource()
84+
let! result = api.GetApiUpdateBool(Some true, cts.Token)
85+
result |> shouldEqual false
86+
}
87+
88+
[<Fact>]
89+
let ``Call async generated method with explicit CancellationToken``() =
90+
async {
91+
use cts = new CancellationTokenSource()
92+
let! result = apiAsync.GetApiReturnInt32(cts.Token)
93+
result |> shouldEqual 42
94+
}

0 commit comments

Comments
 (0)