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 536b384f78..e0ee0a2acb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,7 +9,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