DEV Community

Amedeo
Amedeo

Posted on

12 2

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.

Image of Timescale

πŸš€ pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applicationsβ€”without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post β†’

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!

Sentry image

See why 4M developers consider Sentry, β€œnot bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more