diff --git a/.github/wordlist.txt b/.github/wordlist.txt
index 0ddf7ccf1c..3a38808e63 100644
--- a/.github/wordlist.txt
+++ b/.github/wordlist.txt
@@ -1,6 +1,7 @@
alloc
apis
ASP.NET
+astask
async
azurefunctions
bcl
@@ -18,7 +19,9 @@ enricher
eshoponcontainers
extensibility
flurl
+fs
hangfire
+interop
jetbrains
jitter
jittered
@@ -67,6 +70,7 @@ timingpolicy
ui
unhandled
uwp
+valuetask
waitandretry
wpf
xunit
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f886355373..2a3262ca1e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -8,7 +8,9 @@
+
+
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 9d434d5e7e..f4ae874c95 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -23,6 +23,10 @@ await pipeline.ExecuteAsync(static async token => { /* Your custom logic goes he
```
+> [!NOTE]
+> Asynchronous methods in the Polly API return `ValueTask` or `ValueTask` instead of `Task` or `Task`.
+> If you are using Polly in Visual Basic or F#, please read [Use with F# and Visual Basic](use-with-fsharp-and-visual-basic.md) for more information.
+
## Dependency injection
If you prefer to define resilience pipelines using [`IServiceCollection`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection), you'll need to install the [Polly.Extensions](https://www.nuget.org/packages/Polly.Extensions/) package:
diff --git a/docs/toc.yml b/docs/toc.yml
index 6f0f230254..886d7404fc 100644
--- a/docs/toc.yml
+++ b/docs/toc.yml
@@ -44,6 +44,9 @@
- name: Behavior
href: chaos/behavior.md
+- name: Use with F# and Visual Basic
+ href: use-with-fsharp-and-visual-basic.md
+
- name: Advanced topics
expanded: true
items:
diff --git a/docs/use-with-fsharp-and-visual-basic.md b/docs/use-with-fsharp-and-visual-basic.md
new file mode 100644
index 0000000000..1100c3c16d
--- /dev/null
+++ b/docs/use-with-fsharp-and-visual-basic.md
@@ -0,0 +1,151 @@
+# Use with F# and Visual Basic
+
+Asynchronous methods in the Polly.Core API return either `ValueTask` or `ValueTask`
+instead of `Task` or `Task`. This is because Polly v8 was designed to be optimized
+for high performance and uses `ValueTask` to avoid unnecessary allocations.
+
+One downside to this choice is that in Visual Basic and F#, it is not possible to directly
+await a method that returns `ValueTask` or `ValueTask`, instead requiring the use of
+`Task` and `Task`.
+
+A proposal to support awaiting `ValueTask` can be found in F# language design repository:
+[[RFC FS-1021 Discussion] Support Interop with ValueTask in Async Type][fsharp-fslang-design-118].
+
+To work around this limitation, you can use the [`AsTask()`][valuetask-astask] method to convert a
+`ValueTask` to a `Task` in F# and Visual Basic. This does however introduce an allocation and make
+the code a bit more difficult to work with compared to C#.
+
+Examples of such conversions are shown below.
+
+## F\#
+
+```fsharp
+open FSharp.Control
+open System
+open System.Threading
+open System.Threading.Tasks
+open IcedTasks
+open Polly
+
+let getBestFilmAsync token =
+ task {
+ do! Task.Delay(1000, token)
+ return "https://www.imdb.com/title/tt0080684/"
+ }
+
+let demo () =
+ task {
+ // The ResiliencePipelineBuilder creates a ResiliencePipeline
+ // that can be executed synchronously or asynchronously
+ // and for both void and result-returning user-callbacks.
+ let pipeline =
+ ResiliencePipelineBuilder()
+ .AddTimeout(TimeSpan.FromSeconds(5))
+ .Build()
+
+ let token = CancellationToken.None
+
+ // Synchronously
+ pipeline.Execute(fun () -> printfn "Hello, world!")
+
+ // Asynchronously
+ // Note that Polly expects a ValueTask to be returned, so the function uses the valueTask builder
+ // from IcedTasks to make it easier to use ValueTask. See https://github.com/TheAngryByrd/IcedTasks.
+ do! pipeline.ExecuteAsync(
+ fun token ->
+ valueTask {
+ printfn "Hello, world! Waiting for 2 seconds..."
+ do! Task.Delay(1000, token)
+ printfn "Wait complete."
+ }
+ , token
+ )
+
+ // Synchronously with result
+ let someResult = pipeline.Execute(fun token -> "some-result")
+
+ // Asynchronously with result
+ // Note that Polly expects a ValueTask to be returned, so the function uses the valueTask builder
+ // from IcedTasks to make it easier to use ValueTask. See https://github.com/TheAngryByrd/IcedTasks.
+ let! bestFilm = pipeline.ExecuteAsync(
+ fun token ->
+ valueTask {
+ let! url = getBestFilmAsync(token)
+ return url
+ }
+ , token
+ )
+
+ printfn $"Link to the best film: {bestFilm}"
+ }
+```
+
+[Source][sample-fsharp]
+
+## Visual Basic
+
+```vb
+Imports System.Threading
+Imports Polly
+
+Module Program
+ Sub Main()
+ Demo().Wait()
+ End Sub
+
+ Async Function Demo() As Task
+ ' The ResiliencePipelineBuilder creates a ResiliencePipeline
+ ' that can be executed synchronously or asynchronously
+ ' and for both void and result-returning user-callbacks.
+ Dim pipeline = New ResiliencePipelineBuilder().AddTimeout(TimeSpan.FromSeconds(5)).Build()
+
+ ' Synchronously
+ pipeline.Execute(Sub()
+ Console.WriteLine("Hello, world!")
+ End Sub)
+
+ ' Asynchronously
+ ' Note that the function is wrapped in a ValueTask for Polly to use as VB.NET cannot
+ ' await ValueTask directly, and AsTask() is used to convert the ValueTask returned by
+ ' ExecuteAsync() to a Task so it can be awaited.
+ Await pipeline.ExecuteAsync(Function(token)
+ Return New ValueTask(GreetAndWaitAsync(token))
+ End Function,
+ CancellationToken.None).AsTask()
+
+ ' Synchronously with result
+ Dim someResult = pipeline.Execute(Function(token)
+ Return "some-result"
+ End Function)
+
+ ' Asynchronously with result
+ ' Note that the function is wrapped in a ValueTask(Of String) for Polly to use as VB.NET cannot
+ ' await ValueTask directly, and AsTask() is used to convert the ValueTask(Of String) returned by
+ ' ExecuteAsync() to a Task(Of String) so it can be awaited.
+ Dim bestFilm = Await pipeline.ExecuteAsync(Function(token)
+ Return New ValueTask(Of String)(GetBestFilmAsync(token))
+ End Function,
+ CancellationToken.None).AsTask()
+
+ Console.WriteLine("Link to the best film: {0}", bestFilm)
+
+ End Function
+
+ Async Function GreetAndWaitAsync(token As CancellationToken) As Task
+ Console.WriteLine("Hello, world! Waiting for 1 second...")
+ Await Task.Delay(1000, token)
+ End Function
+
+ Async Function GetBestFilmAsync(token As CancellationToken) As Task(Of String)
+ Await Task.Delay(1000, token)
+ Return "https://www.imdb.com/title/tt0080684/"
+ End Function
+End Module
+```
+
+[Source][sample-vb]
+
+[fsharp-fslang-design-118]: https://github.com/fsharp/fslang-design/discussions/118
+[valuetask-astask]: https://learn.microsoft.com/dotnet/api/system.threading.tasks.valuetask.astask
+[sample-fsharp]: https://github.com/App-vNext/Polly/tree/main/samples/Intro.FSharp
+[sample-vb]: https://github.com/App-vNext/Polly/tree/main/samples/Intro.VisualBasic
diff --git a/samples/Intro.FSharp/Intro.FSharp.fsproj b/samples/Intro.FSharp/Intro.FSharp.fsproj
new file mode 100644
index 0000000000..414975d92e
--- /dev/null
+++ b/samples/Intro.FSharp/Intro.FSharp.fsproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Intro.FSharp/Program.fs b/samples/Intro.FSharp/Program.fs
new file mode 100644
index 0000000000..f7b7ca0221
--- /dev/null
+++ b/samples/Intro.FSharp/Program.fs
@@ -0,0 +1,63 @@
+open FSharp.Control
+open System
+open System.Threading
+open System.Threading.Tasks
+open IcedTasks
+open Polly
+
+let getBestFilmAsync token =
+ task {
+ do! Task.Delay(1000, token)
+ return "https://www.imdb.com/title/tt0080684/"
+ }
+
+let demo () =
+ task {
+ // The ResiliencePipelineBuilder creates a ResiliencePipeline
+ // that can be executed synchronously or asynchronously
+ // and for both void and result-returning user-callbacks.
+ let pipeline =
+ ResiliencePipelineBuilder()
+ .AddTimeout(TimeSpan.FromSeconds(5))
+ .Build()
+
+ let token = CancellationToken.None
+
+ // Synchronously
+ pipeline.Execute(fun () -> printfn "Hello, world!")
+
+ // Asynchronously
+ // Note that Polly expects a ValueTask to be returned, so the function uses the valueTask builder
+ // from IcedTasks to make it easier to use ValueTask. See https://github.com/TheAngryByrd/IcedTasks.
+ do! pipeline.ExecuteAsync(
+ fun token ->
+ valueTask {
+ printfn "Hello, world! Waiting for 2 seconds..."
+ do! Task.Delay(1000, token)
+ printfn "Wait complete."
+ }
+ , token
+ )
+
+ // Synchronously with result
+ let someResult = pipeline.Execute(fun token -> "some-result")
+
+ // Asynchronously with result
+ // Note that Polly expects a ValueTask to be returned, so the function uses the valueTask builder
+ // from IcedTasks to make it easier to use ValueTask. See https://github.com/TheAngryByrd/IcedTasks.
+ let! bestFilm = pipeline.ExecuteAsync(
+ fun token ->
+ valueTask {
+ let! url = getBestFilmAsync(token)
+ return url
+ }
+ , token
+ )
+
+ printfn $"Link to the best film: {bestFilm}"
+ }
+
+[]
+let main _ =
+ demo().Wait()
+ 0
diff --git a/samples/Intro.VisualBasic/Intro.VisualBasic.vbproj b/samples/Intro.VisualBasic/Intro.VisualBasic.vbproj
new file mode 100644
index 0000000000..499481d3d4
--- /dev/null
+++ b/samples/Intro.VisualBasic/Intro.VisualBasic.vbproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/samples/Intro.VisualBasic/Program.vb b/samples/Intro.VisualBasic/Program.vb
new file mode 100644
index 0000000000..dac80f4d21
--- /dev/null
+++ b/samples/Intro.VisualBasic/Program.vb
@@ -0,0 +1,56 @@
+Imports System.Threading
+Imports Polly
+
+Module Program
+ Sub Main()
+ Demo().Wait()
+ End Sub
+
+ Async Function Demo() As Task
+ ' The ResiliencePipelineBuilder creates a ResiliencePipeline
+ ' that can be executed synchronously or asynchronously
+ ' and for both void and result-returning user-callbacks.
+ Dim pipeline = New ResiliencePipelineBuilder().AddTimeout(TimeSpan.FromSeconds(5)).Build()
+
+ ' Synchronously
+ pipeline.Execute(Sub()
+ Console.WriteLine("Hello, world!")
+ End Sub)
+
+ ' Asynchronously
+ ' Note that the function is wrapped in a ValueTask for Polly to use as VB.NET cannot
+ ' await ValueTask directly, and AsTask() is used to convert the ValueTask returned by
+ ' ExecuteAsync() to a Task so it can be awaited.
+ Await pipeline.ExecuteAsync(Function(token)
+ Return New ValueTask(GreetAndWaitAsync(token))
+ End Function,
+ CancellationToken.None).AsTask()
+
+ ' Synchronously with result
+ Dim someResult = pipeline.Execute(Function(token)
+ Return "some-result"
+ End Function)
+
+ ' Asynchronously with result
+ ' Note that the function is wrapped in a ValueTask(Of String) for Polly to use as VB.NET cannot
+ ' await ValueTask directly, and AsTask() is used to convert the ValueTask(Of String) returned by
+ ' ExecuteAsync() to a Task(Of String) so it can be awaited.
+ Dim bestFilm = Await pipeline.ExecuteAsync(Function(token)
+ Return New ValueTask(Of String)(GetBestFilmAsync(token))
+ End Function,
+ CancellationToken.None).AsTask()
+
+ Console.WriteLine("Link to the best film: {0}", bestFilm)
+
+ End Function
+
+ Async Function GreetAndWaitAsync(token As CancellationToken) As Task
+ Console.WriteLine("Hello, world! Waiting for 1 second...")
+ Await Task.Delay(1000, token)
+ End Function
+
+ Async Function GetBestFilmAsync(token As CancellationToken) As Task(Of String)
+ Await Task.Delay(1000, token)
+ Return "https://www.imdb.com/title/tt0080684/"
+ End Function
+End Module
diff --git a/samples/Samples.sln b/samples/Samples.sln
index ade0ec3fc6..c1e66935c3 100644
--- a/samples/Samples.sln
+++ b/samples/Samples.sln
@@ -24,6 +24,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyInjection", "Depe
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chaos", "Chaos\Chaos.csproj", "{A296E17C-B95F-4B15-8B0D-9D6CC0929A1D}"
EndProject
+Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "Intro.VisualBasic", "Intro.VisualBasic\Intro.VisualBasic.vbproj", "{10F1C68E-DBF8-43DE-8A72-3EB4491ECD9C}"
+EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Intro.FSharp", "Intro.FSharp\Intro.FSharp.fsproj", "{2C0F3F7F-63ED-472B-80B7-905618B07714}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -54,6 +58,14 @@ Global
{A296E17C-B95F-4B15-8B0D-9D6CC0929A1D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A296E17C-B95F-4B15-8B0D-9D6CC0929A1D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A296E17C-B95F-4B15-8B0D-9D6CC0929A1D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {10F1C68E-DBF8-43DE-8A72-3EB4491ECD9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {10F1C68E-DBF8-43DE-8A72-3EB4491ECD9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {10F1C68E-DBF8-43DE-8A72-3EB4491ECD9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {10F1C68E-DBF8-43DE-8A72-3EB4491ECD9C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2C0F3F7F-63ED-472B-80B7-905618B07714}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2C0F3F7F-63ED-472B-80B7-905618B07714}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2C0F3F7F-63ED-472B-80B7-905618B07714}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2C0F3F7F-63ED-472B-80B7-905618B07714}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE