One of the great features of .NET is that you can mix its programming languages (C#, F#, VB.NET) in a single solution and that assemblies written in one language can call assemblies written in other languages. This is great, especially when starting with F#. A team does not need to switch everything at once, but can keep using the existing C# code and use Interop to call C# from F# or vice versa.
In this article, we take a look at the Interop story in 2026. Spoilers: it works great.
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 in either C# or F#, minimising Interop. We only need Interop when we talk horizontally between sub-systems. Interop works great, but it’s an additional effort.
Can’t you slice your codebase into F# or C# sub-systems? Do not worry, you can, of course, still use Interop and have a (wild) mix of C# or F# assemblies referencing each other. The tricky part is typically deciding where to define shared types – and not ending up with cyclic references.
Calling C# from F# – the very easy direction
Calling C# from F# is really easy, just do it. F# understands C# really well. F# supports methods, properties, extension methods, etc., out of the box.
Calling F# from C# – the easy direction
Most F# things are also easy to call from C#, but there are some exceptions. However, this is the more interesting direction because, typically, we want to rely on F#’s strengths in business logic to model the domain with its strong type system and discriminated unions, and on its simple function composition to implement business rules. So F# on the inside, and C# on the outside (frameworks typically think in C# in the .NET space).
Records
F# records feel like C# records in C#.


Discriminated Unions
Creating a discriminated union value depends on whether the case has additional fields.


Pattern matching in C# on F# unions is straight-forward for cases with additional fields. For simple cases, we can’t directly match on the case, but need to use the when syntax combined with an IsXyz property:

Also, notice that in C# we always have to add an _ => pattern.
Modules
C# can call functions from F# modules directly. Maybe you already noticed that sometimes modules in F# have a Module suffix when called from C#. This is the case when you have a module and a type with the same name inside the same module/namespace in F#. Since C# does not support this scenario, the compiler adds the Module suffix.
For consistency reasons, we want a Module suffix for all modules. So we add an attribute (CompilationRepresentationAttribute) to the module to tell the compiler always to add the Module suffix.

As a side note, the RequiredQualifiedAccess attribute tells the compiler only to accept calls to the functions inside the module when it is qualified with the module name: Library.function.
Functions
Thanks to the compilers, calling functions is straightforward – even functions with multiple arguments:


If you want, you can give functions CamelCase names, too:


This makes the code feel more like C#.
Lists
We use the library FSharpx to make creating F# lists easy. The library provides the ToFSharpList extension method.


Functions
Functions in F# are not Func<>s, so we need to convert them when passing C# lambdas to F#. Again, we use a helper from FSharpx.


FromFunc to convert the function argument.Measures
F# supports units of measure. With a small trick, even for non-numeric values (FSharp.UMX).
C# does not, the measure get’s simply erased.


Async Workflows
We can call async workflows directly from C# – no need to wrap every async workflow in a task computation expression or task call in F#.


FsToolkit.ErrorHandling primarily provides functionality for Options, Results, and Validation, but it also includes some handy async helper functions.
StartImmediateAsTask is closest to a Task await, but there are other options as well.
Partial Application
We can even call partially applied functions from C# – it’s not nice, but it works. We can use the Invoke method.


Tuples
In F#, tuples by default are reference tuples; in C#, struct tuples. But both know the other:


Options
To simplify working with options, we can again rely on FSharpx, which provides the ToFSharpOption extension method.


Extension Methods
Sometimes, there is no nice way to call some F# functionality. Then, we can always add some extension methods to make it easier to be called from C#. Simply decorate an F# function with the Extension attribute:


Don’t pass functions to C# this way
Make your life easy, don’t pass functions (as type function – a function without explicitly defined arguments) from F# to C#. Define the functions used by C# with an argument:


Summary
Mixing F# and C# in a codebase works. While not always ideal, it provides a straightforward way to introduce F# into a C# codebase.
[…] C# – F# Interop (2026 edition) (Urs Enzler) […]