Our journey to F#: C#-F# Interop

When we started with TimeRocket in 2015, we used C# as the programming language for our backend. In 2020, we started using F# for our new code. We see little value in rewriting existing C# code to F# and do so only when major changes are due in existing C# code. As a result, we have quite a bit of C#-F# interop in our system. Both from C# to F# and from F# to C#. This post overviews what we learned about C#-F# interop.

This blog post is part of the F# Advent Calendar 2022 – check out the other blog posts!

Mixing C# and F# in the same solution

You can mix C# and F# code in the same solution, but for each assembly/project, you have to decide whether it’s a C# or F# assembly. The (simplified) architecture of our backend system looks like this:

The ASP.NET controllers are distributed in several assemblies and referenced by a central assembly that holds the StartUp and Program classes. Referencing other assemblies containing controllers is enough for ASP.NET to find them. These assemblies can either be F# or C#. We have an API assembly for every sub-system. A sub-system is a cohesive part of our domain. Sub-systems can talk to each other, but they are loosely coupled.

The business logic for every sub-system is put into a dedicated assembly – either C# or F#.

We can keep each vertical slice either in C# or F# so that Interop is minimised. We only need Interop when we talk horizontally between sub-systems. Interop works great, but it’s an additional effort.

Calling F# functions from C#

Calling functions defined in F# code from C# is simple: just call them. Given this module with a couple of functions:

module CallMe =
    let singleArgument x = x + 1

    let multipleArguments x y = x + y

    let multipleArgumentsAsTuple (x, y) = x + y

You can call them from C# like this:

var a = CallMe.singleArgument(41);

var b = CallMe.multipleArguments(17, 42);

var c = CallMe.multipleArgumentsAsTuple(17, 42);

Note that multipleArguments can be called like a normal C# method.

CompiledName

If you’d like to change the name of the function foo to look more like a method with a capitol F, then use the CompiledName attribute:

[<CompiledName("GetI")>]
let getI foo = foo.I
var i = FooModule.GetI(foo);

Calling C# methods from F#

Calling a C# method from F# is straightforward as well. Given the following C# methods:

public class CallMe
{
    public int SingleArgument(int i) { return i; }

    public int MultipleArguments(int i, int j) { return i + j; }
}

You can call them as shown below. Note that methods with multiple arguments have to be called with a tuple, which makes it look like a call in C#.

let callMe = CallMe()

let a = callMe.SingleArgument 1

let b = callMe.MultipleArguments (1, 2)

Calling methods with out-parameters works as well. The return value and the out-parameter(s) are returned as a tuple:

let success, guid = Guid.TryParse "no guid" // success: bool, guid: Guid

Calling async workflows from C#

When dealing with asynchronous calls, things get a bit more difficult, and you have several options to choose from or mix. You can either use async workflows or tasks in F#. See this blog post for a comparison.

Using task computation expression

The simplest solution regarding Interop alone uses tasks in F# and C#:

module CallMeTask =
    let call x y = task { return x + y }
public async Task<int> Call()
{
    return await CallMeTask.call(17, 42);
}

The task computation expression can await async workflows directly as well.

Using vanilla F#

When calling async workflows from C#, we have to convert the async workflow into a task. We can do this using StartImmediatAsTask. There are other options, but this matches a C# await the closest. If you like, you can pass a cancellation token.

module CallMeAsync =
    let call x y = async { return x + y }
public async Task<int> Call()
    {
        return await FSharpAsync.StartImmediateAsTask(CallMeAsync.call(17, 42), FSharpOption<CancellationToken>.None);
    }

Using FusionTasks

A slick way to call async workflows from C# is using the waiter included in FusionTasks. Add the FusionTasks NuGet package to your project; then the code looks like this:

public async Task<int> CallUsingFusionTasks()
    {
        return await CallMeAsync.call(17, 42);
    }

Calling partially applied F# functions from C#

You can call a partially applied function and use currying in C#, but it results in verbose code. So, I’d suggest minimizing this way of Interop. First, let’s provide a partially applied function in F#:

let multipleArgumentsPartiallyApplied x = multipleArguments x

Then we can call this function and provide the second argument:

var partiallyApplied = CallMe.multipleArgumentsPartiallyApplied(17);
var fullyApplied = partiallyApplied.Invoke(42); // = 59

Using discriminated unions from C#

You can use discriminated unions defined in F#. However, pattern matching isn’t reasonable. We sometimes use this to generate values, but the business logic that uses the discriminated unions is always in our F# code.

type MyUnion =
    | A
    | B of int

var a = MyUnion.A;
var b = MyUnion.NewB(42);

Tuples

Tuples can be confusing because there are two types of tuples: reference and struct tuples (ValueTuple in C#-terminology). Normal tuples in F# like 17, 42 are reference tuples (otherwise, you have to declare them as struct (17, 42) ). In C#, a normal tuple like (17, 42) is a struct tuple.

So if you want to pass a tuple from F# to C#, you probably have to convert it:

module Tuple =
    let getTuple () = (17, 42)
int Call((int A, int B) tuple) { return tuple.A + tuple.B; }

var tuple = Tuple.getTuple();
//var _ = Call(tuple); // does not compile

var (a, b) = Tuple.getTuple();
var r = Call((a, b)); // decompose and recompose works

Nullable types and options

In C#, we use nullable reference types to model values that could be missing. In F#, we have Options for this. They can easily be converted. Below, you can see the conversion functions both in C# and F#:

module Options =
    let getOption () = Some 42
    
    let getNullable () = Some 42 |> Option.toNullable
    
    let passOption option =
        match option with
        | Some v -> printfn $"Some %d{v}"
        | None -> printfn "None"
    
    let passNullable nullable =
        let option = Option.ofNullable nullable
        match option with
        | Some v -> printfn $"Some %d{v}"
        | None -> printfn "None"
var options = Options.getOption();
var nullable = OptionModule.ToNullable(options);

var nullable2 = Options.getNullable();

Implicit conversions

C# uses implicit conversions quite a lot. And sometimes, you use a library or framework from F# that relies on implicit conversions – like plain ASP.NET Core’s ActionResult and ActionResult<T>.

But we can invoke an implicit cast explicitly with the help of SRTPs:

let inline private cast (x: ^a) : ^b = ((^a or ^b): (static member op_Implicit: ^a -> ^b) x)

let actionResult: ActionResult = ConflictResult() :> ActionResult
let actionResult': ActionResult<int> = cast actionResult 

The function cast takes an a and returns a b when there is an implicit operator defined that can convert a to b.

Adapter or no adapter?

As you have seen, it’s possible to call code that uses F#-style constructs like discriminated unions, (partially applied) functions, and async workflows directly from C#. Sometimes the code is straightforward and simple, sometimes it gets more verbose, or it doesn’t feel like C# syntax anymore – for example, because of functions starting with small letters, not capital letters. In such cases, we introduce adapters that wrap some F# code into an interface that feels more C#-y.

This is an example from our production code:

type OrganisationAdapter(organisationStorages : OrganisationStorages, logger : ILogger) =
    interface IOrganisationAdapter with
        member self.AssignEmployee
            (
                employeeId : EmployeeId,
                formId : OrganisationFormId,
                unitId : OrganisationUnitId,
                roleId : OrganisationRoleId,
                effective : Calitime.Zeit.Workday,
                application : Calitime.Zeit.ApplicationDateTime
            )
            =
            task {
                let effective' = Bridge.fromWorkday effective
                let application' = Bridge.fromApplication application
                let! domainEventDescription =
                    AdapterFacade.assignEmployee
                        organisationStorages.Assignments.PersistEvent
                        employeeId
                        formId
                        unitId
                        roleId
                        effective'
                        application'
                return [ domainEventDescription ] :> IReadOnlyCollection<DomainEventDescription>
            }

We use an interface to break a direct dependency from the C# code to the F#. The interface is defined in a shared assembly referenced by the C# and F# assemblies. The adapter proved the functionality as methods, which are easy to use from C# and converts data types that exist in the C# world and the F#. Workday for example, exists in both worlds with different implementations. Each implementation is optimised to be as simple as possible for either C# or F#. We put all these small conversion functions in a module called Bridge. The adapter also converts F# collections into C# collections. Here an F# list is converted into a IReadOnlyCollection. This is simple because F# collections implement many interfaces typically used in C# out-of-the-box.

Conclusions

Interop between C# and F# works great and makes it easy to start using F# only for a part of your application. No need to fully switch at once, or ever. In this blog post, I’ve shown you how we deal with the different aspects of Interop. In general, using C# from F# is straightforward because F# was built with Interop towards C# in mind. F# knows (almost) all the features of C# and makes using C# code easy – yes, there are exceptions, but they are seldom. Calling F# code from C# is a bit more difficult, but still, all the tools are there to build great applications with a mix of C# and F#.

About the author

Urs Enzler

2 comments

By Urs Enzler

Recent Posts