F# - C# Interop
Amedeo
Posted on February 24, 2022
What is Interop
Interoperability is the ability of two different systems to communicate with each other. In this case, it would be a C# and F# application communicating. Because they compile down to the same Intermediate Language we can reference F# projects in C# projects and vice versa.
Why is this useful
One of the benefits is that if you are planning to migrate your codebase from C# to F#, you can do it gradually by introducing some F# code without discarding any of the existing code base.
Sometimes we want to take advantage of the C# world, i.e. a lot of existing nuget packages are written in C# and we don't want to rewrite them in F#.
⚠️ Please note that all the observations listed on this page are valid for .NET 5 and below.
Example F# Application using C# library
In this example, we are going to create an F# application that uses a C# library.
The library simulates an IO operation with await Task.Delay(300, cancellationToken);
and reverses the words sent.
C# Library
namespace WordReverserLibraryCsharp
{
public static class WordReverser
{
public static async Task<string> WordReverserAsync(string sentence, CancellationToken cancellationToken)
{
await Task.Delay(300, cancellationToken);
var words = sentence.Split(' ');
Array.Reverse(words);
return $"{string.Join(" ", words)}";
}
}
}
F# Application
namespace WordReverserConsoleAppFsharp
open System.Threading
open WordReverserLibraryCsharp
module WordReverserModule =
let wordReverser (sentence: string) : Async<unit> =
async {
let cancellationTokenSource = new CancellationTokenSource(800);
let! reversedWords =
WordReverser.WordReverserAsync(sentence, cancellationTokenSource.Token)
|> Async.AwaitTask
printfn ($"{reversedWords}")
}
Async.Start(wordReverser("one two three"))
Async.RunSynchronously(Async.Sleep(1000))
Observations
To consume the C# library from F#, we have to convert the Task<T>
returned from the library to Async<T>
using Async.AwaitTask
which waits for the Task to complete and returns its result as Async<T>
. Read about Async.AwaitTask here
Just before the line Async.Start(wordReverser("one two three"))
we still haven't invoked the operation, only done the setup to call wordReverser
, to start the operation we can use Async.Start(wordReverser("one two three")
.
Example C# Application using F# library
In this example we are doing the opposite of the previous one: we create a C# application that uses an F# library.
F# Library
namespace WordReverserLibraryFsharp
open System.Threading
open System.Threading.Tasks
module WordReverserModule =
let wordReverser (sentence: string, cancellationToken: CancellationToken) : Task<string> =
let reverser =
async {
do! Async.Sleep(300)
return
sentence.Split [| ' ' |]
|> Array.rev
|> String.concat " "
}
Async.StartAsTask(reverser, TaskCreationOptions.None, cancellationToken)
C# Application
using WordReverserLibraryFsharp;
namespace WordReverserConsoleAppCsharp
{
internal static class Program
{
private static async Task Main()
{
var cancellationTokenSource = new CancellationTokenSource(400);
var reversedWords = await WordReverserModule.wordReverser("one two three", cancellationTokenSource.Token);
Console.WriteLine(reversedWords);
}
}
}
Observations
We can't use F# Async
in C#, but the F# library can return a Task
so that the C# application can consume it without any issues. We can achieve this by using Async.StartAsTask
in .NET 5 and below.
Read about Async.StartAsTask here
It's good practice for any Async
work in C#, to always pass a CancellationToken as an argument, while the F# code does not necessarily need one as the CancellationToken propagation is controlled by how the asynchronous work is kicked off and as a result, CancellationTokens may or may not be propagated. For example Async.Sleep
doesn't accept a CancellationToken as a parameter.
Example C# Application using F# library handling exceptions
In this example, we are going to use an F# library from a C# application, handling exceptions and passing a CancellationToken.
F# Library
namespace FsharpLibraryComplete
open System.Threading
open System.Threading.Tasks
module FsharpLibraryCompleteModule =
let wordReverser (sentence: string, raiseException: bool, cancellationToken: CancellationToken) : Task<string> =
let reverser =
async {
do! Async.Sleep(300)
if(raiseException) then
raise (System.Exception("wordReverser threw Exception"))
return
sentence.Split [| ' ' |]
|> Array.rev
|> String.concat " "
}
Async.StartAsTask(reverser, TaskCreationOptions.None, cancellationToken)
C# Application
using FsharpLibraryComplete;
namespace ConsoleAppCsharpComplete;
internal static class Program
{
private static async Task Main()
{
var cancellationTokenSource = new CancellationTokenSource(500);
string reversedWords;
try
{
reversedWords = await FsharpLibraryCompleteModule.wordReverser("one two three", true, cancellationTokenSource.Token);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
Console.WriteLine(reversedWords);
}
}
Console output
System.Exception: someAsyncFunction threw Exception
at FsharpLibraryComplete.FsharpLibraryCompleteModule.wordReverser@12-1.Invoke(Unit _arg1) in C:\projects\InteropPlayGround\Docker\ConsoleAppCsharpComplete\FsharpLibraryComplete\FsharpLibraryComplete.fs:line 13
at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvokeNoHijackCheck[a,b](AsyncActivation`1 ctxt, b result1, FSharpFunc`2 userCode) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 464
at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 104
--- End of stack trace from previous location ---
at ConsoleAppCsharpComplete.Program.Main() in C:\projects\InteropPlayGround\Docker\ConsoleAppCsharpComplete\ConsoleAppCsharpComplete\Program.cs:line 14
Unhandled exception. System.Exception: someAsyncFunction threw Exception
at FsharpLibraryComplete.FsharpLibraryCompleteModule.wordReverser@12-1.Invoke(Unit _arg1) in C:\projects\InteropPlayGround\Docker\ConsoleAppCsharpComplete\FsharpLibraryComplete\FsharpLibraryComplete.fs:line 13
at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvokeNoHijackCheck[a,b](AsyncActivation`1 ctxt, b result1, FSharpFunc`2 userCode) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 464
at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 104
--- End of stack trace from previous location ---
at ConsoleAppCsharpComplete.Program.Main() in C:\projects\InteropPlayGround\Docker\ConsoleAppCsharpComplete\ConsoleAppCsharpComplete\Program.cs:line 14
at ConsoleAppCsharpComplete.Program.<Main>()
Observations
Running the code as it is, will raise an exception from the F# library that gets caught by the C# application.
Please note that if the Task gets cancelled before raising the exception on the F# library, the C# application will catch the exception as System.Threading.Tasks.TaskCanceledException: A task was canceled.
Conclusion
It's not particularly difficult to use F# libraries in a C# application and vice versa, but there are a few things that we have to keep in mind, i.e. C# does not work with F# Async
, instead, we have to add additional steps to convert Async
into a Task
.
Posted on February 24, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.