DEV Community

Amedeo
Amedeo

Posted on

F# - C# Interop

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)}";
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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>()

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (4)

Collapse
 
tunaxor profile image
Angel Daniel Munoz Gonzalez

Nice post it is pretty clear and to the point, I like it!

If you have a chance it would be nice to add a section about task expressions which are meant for C# <-> F# async interoperability

Collapse
 
amedeov profile image
Amedeo

Thank you!

I stated in the post that "Please note that all the observations listed on this page are valid for .NET 5 and below."

One of my colleagues worked on the same examples using .NET 6 and task - I'll share the link to the article once/if he decides to publish it!

Collapse
 
jkone27 profile image
jkone27

indded i also thought , in latest F# you can just use

task { ... }
Enter fullscreen mode Exit fullscreen mode

instead of

async { ... } |> Async.StartAsTask
Enter fullscreen mode Exit fullscreen mode

task state machine is also in some cases more performant than async from what i understood

Collapse
 
amedeov profile image
Amedeo

Please see the reply above, hopefully I can share something soon! Thank you!