FSharp.Control.FusionTasks
F# Async workflow <--> .NET Task/ValueTask easy seamless interoperability library.
Install / Use
/learn @kekyo/FSharp.Control.FusionTasksREADME
F# FusionTasks

Status
| | main | devel |
|:---|:--:|:--:|
| NuGet Package | | |
| Continuous integration |
|
|
What is this?
- F# Async workflow <--> .NET Task/ValueTask easy seamless interoperability library.
- Sample code (F# side):
let asyncTest = async {
use ms = new MemoryStream()
// FusionTasks directly interpreted System.Threading.Tasks.Task class in F# async-workflow block.
do! ms.WriteAsync(data, 0, data.Length)
do ms.Position <- 0L
// FusionTasks directly interpreted System.Threading.Tasks.Task<T> class in F# async-workflow block.
let! length = ms.ReadAsync(data2, 0, data2.Length)
do length |> should equal data2.Length
}
- Sample code (C# side):
using System.Threading.Tasks;
using Microsoft.FSharp.Control;
public async Task AsyncTest(FSharpAsync<int> asyncIntComp)
{
// FusionTasks simple usage F#'s Async<unit> direct awaitable.
await FSharpAsync.Sleep(500);
Console.WriteLine("Awaited F# async function (unit).");
// FusionTasks simple usage F#'s Async<int> direct awaitable.
var result = await asyncIntComp;
Console.WriteLine("Awaited F# async function: Result=" + result);
}
Features
- Easy interoperable .NET Task/ValueTask <--> F#'s Async.
- F# async workflow block now supports directly .NET Task/ValueTask handle with let!, do! and use!.
- .NET (C# async-await) now supports directly F#'s Async.
- SyncronizationContext capture operation support (F#: AsyncConfigure method / .NET (C#) AsAsyncConfigured method)
- .NET now supports standard asynchronous sequence called
IAsyncEnumerable<T>, FusionTasks supports it withforexpression.
Benefits
- Easy interoperability, combination and relation standard .NET OSS packages using Task/ValueTask and F#'s Async.
- F# 6.0/4.5 with .NET 6.0/5.0, .NET Core 3.0/2.0 (or higher), .NET Standard 1.6/2.0 and .NET Framework 4.5/4.6.1/4.8.
- Ready to LINQPad 5.
Environments
- F# 6.0 or higher/4.5
- .NET 6.0
- .NET 5.0
- .NET Core 3.0/2.0 or higher
- .NET Standard 1.6/2.0/2.1
- .NET Framework 4.5/4.6.1/4.8
Combination chart:
| .NET BCL | F# | Details | |:----|:----|:----| | .NET 6.0 | F# 6.0 or higher | | | .NET 5.0 | F# 6.0 or higher | | | .NET Core 3.1, 3.0 | F# 6.0 or higher | (3.0 is deprecated) | | .NET Core 2.,2 2.1, 2.0 | F# 6.0 or higher | (2.0 is deprecated) | | .NET Standard 2.1, 2.0 | F# 6.0 or higher | | | .NET Standard 1.6 | F# 4.5 | | | .NET Framework 4.8, 4.6.1 | F# 6.0 or higher | | | .NET Framework 4.5 | F# 4.5 | |
How to use
- Search NuGet package and install "FSharp.Control.FusionTasks".
- F# use, autoopen'd namespace "FSharp.Control". "System.Threading.Tasks" is optional.
- C# use, using namespace "System.Threading.Tasks". "Microsoft.FSharp.Control" is optional.
Samples
Basic async workflow:
let asyncTest = async {
use ms = new MemoryStream()
// FusionTasks directly interpreted System.Threading.Tasks.Task class in F# async-workflow block.
// Sure, non-generic Task mapping to Async<unit>.
do! ms.WriteAsync(data, 0, data.Length)
do ms.Position <- 0L
// FusionTasks directly interpreted System.Threading.Tasks.Task<T> class in F# async-workflow block.
// Standard usage, same as manually used Async.AwaitTask.
let! length = ms.ReadAsync(data2, 0, data2.Length)
do length |> should equal data2.Length
}
Without async workflow:
use ms = new MemoryStream()
// Manually conversion by an operator "Async.AsAsync" : Task<T> --> Async<'T>
let asy = ms.ReadAsync(data, 0, data.Length) |> Async.AsAsync
let length = asy |> Async.RunSynchronosly
Without async workflow (CancellationToken):
use ms = new MemoryStream()
let cts = new CancellationTokenSource()
// Produce with CancellationToken:
// TIPS: FusionTasks cannot handle directly CancellationToken IN ASYNC WORKFLOW.
// Because async workflow semantics implicitly handled CancellationToken with Async.DefaultCancellationToken, CancellationToken and CancelDefaultToken().
// (CancellationToken derived from Async.StartWithContinuations() in async workflow.)
let asy = ms.ReadAsync(data, 0, data.Length).AsAsync(cts.Token)
let length = asy |> Async.RunSynchronosly
Handle Task.ConfigureAwait(...) (Capture/release SynchContext):
let asyncTest = async {
use ms = new MemoryStream(...)
// We can use ConfigureAwait() on let!/do!.
let! length = ms.ReadAsync(data, 0, data.Length).ConfigureAwait(false)
}
NOTE: Older released contains AsyncConfigure(bool) method, but it was obsoleted.
Because it existed for avoiding PCL strange linking errors.
Delegate async continuation - works like TaskCompletionSource<T>:
open System.Threading
let asyncCalculate() =
// Create AsyncCompletionSource<'T>.
let acs = new AsyncCompletionSource<int>()
// Execution with completely independent another thread...
let thread = new Thread(new ThreadStart(fun _ ->
Thread.Sleep(5000)
// If you captured thread context (normally continuation or callbacks),
// can delegation async continuation using AsyncCompletionSource<'T>.
acs.SetResult(123 * 456)))
thread.Start()
// Async<'T> instance
acs.Async
Standard asynchronous sequence IAsyncEnumerable<T>:
let asyncTest = async {
// FusionTasks directly interpreted System.Collection.Generic.IAsyncEnumerable<T> in
// F# async-workflow for expression.
for value in FooBarAccessor.EnumerableAsync() do
// Totally asynchronous operation in each asynchronous iteration:
let! result = value |> FooBarCollector.calculate
do! output.WriteAsync(result)
// ... (Continuation is asynchronously behind `for` loop)
}
And, we can use IAsyncEnumerable<T>.ConfigureAwait(bool) on it.
NOTE: IAsyncEnumerable<T> is supported only these environments:
- net461 or higher.
- netstandard2.0 or higher.
- netcoreapp2.1 or higher.
It limitation comes from NuGet Microsoft.Bcl.AsyncInterfaces 5.0.0.
Standard asynchronous disposer IAsyncDisposable:
let asyncTest = async {
// FusionTasks directly interpreted System.IAsyncDisposable in
// F# async-workflow use expression.
// TIP: We can use `use` expression instead of `use!`,
// Because the `use!` will be bound asynchronously BEFORE calling `DisposeAsync()`.
use accessor = DatabaseAccessor.getAsyncDisposableAccessor()
// (Use accessor...)
// (Will be disposed asynchronously, calls `DisposeAsync()` at end of scope...)
}
TIPS: We have to add annotation for arguments if using it in async workflow:
let asyncInner arg0 = async {
// Cause FS0041:
// A unique overload for method 'Source' could not be determined based on type information prior to this program point.
// A type annotation may be needed.
// --> Because F# compiler conflict arg0 type inferences: Async<int> or Task<int>.
let! result = arg0
let calculated = result + 1
printfn "%d" calculated
}
// Fixed with type annotation Async<'T> or Task<'T>:
let asyncInner (arg0:Async<_>) = async {
let! result = arg0
let calculated = result + 1
printfn "%d" calculated
}
In C# side:
- Really need sample codes? huh? :)
Easy LINQPad 5 driven:
- Before setup NuGet package (FSharp.Control.FusionTasks) the LINQPad NuGet Manager.
open System.IO
// Result is Async<byte[]>
let asyncSequenceData =
let r = new Random()
let data = [| for i = 1 to 100 do yield byte (r.Next()) |]
async {
use fs = new MemoryStream()
do! fs.WriteAsync(data, 0, data.Length)
do! fs.FlushAsync()
return fs.ToArray()
}
// Convert to Task<byte[]> and dump:
asyncSequenceData.AsTask().Dump()

"task-like" and ValueTask appendix
- .NET add new "task-like" type. "task-like" means applied a attribute "System.Runtime.CompilerServices.AsyncMethodBuilderAttribute" and declared the async method builder.
- ValueTask overview:
- New standard "task-like" type named for "ValueTask<T>" for C#. FusionTasks supported ValueTask<T> on 1.0.20.
- ValueTask<T> declared by struct (Value type) for goal is improvement performance. But this type has the Task<T> instance inside and finally continuation handle by Task<T>.
- ValueTask<T> performance effective situation maybe chatty-call fragments using both caller C# and awaiter C# code...
- ValueTask<T> a little bit or no effect improvement performance, because usage of senario for FusionTasks.
- "task-like" augumenting is difficult:
- We have to apply to task-like type with the attribute "AsyncMethodBuilderAttribute".
- Means if already declared type (Sure, we have FSharpAsync<'T>) cannot augument and cannot turn to task-like type.
- Therefore cannot directly return for FSharpAsync<'T> from C#'s async-await method.
- And cannot auto handle task-like type by FusionTasks, because no type safe declaration for task-like type...
- For example, if force support task-like type, FusionTasks require augument "Source: taskLike: obj -> FSharpAsync<'T>" overload on FSharpAsync<'T&g
