Skip to content
AgileLogics
7 min read

Interop with C# and .NET

Prerequisites: Classes and members

F# code can use any .NET library and be used from C#. A few differences between the two languages are worth knowing.

null from .NET

F# types can't be null, but .NET methods can return it. Convert at the boundary:

let envValue name =
    System.Environment.GetEnvironmentVariable name |> Option.ofObj

printfn "%A" (envValue "SURELY_NOT_DEFINED_12345")

let maybeText: string = null
match maybeText with
| null -> printfn "got null"
| text -> printfn "got %s" text

let nullableAge = System.Nullable<int>(42)
printfn "%A" (Option.ofNullable nullableAge)
None
got null
Some 42

Option.ofObj turns a possibly-null reference into an option; Option.ofNullable does the same for Nullable<T> values. F# 9 added optional nullable reference types (<Nullable>enable</Nullable> in the project, then string | null in signatures) so the compiler can track nulls for you.

Delegates, Func, and LINQ

F# lambdas convert automatically to .NET delegates like Func and Action when passed to a method:

open System.Linq

let numbers = [ 5; 3; 8; 1 ]

let big = numbers.Where(fun n -> n > 2).OrderBy(fun n -> n).ToList()
printfn "%A" (List.ofSeq big)

let square = System.Func<int, int>(fun x -> x * x)
printfn "%d" (square.Invoke 7)
[3; 5; 8]
49

LINQ works, but the F# List/Seq functions are usually more natural in F# code.

Extension methods

open System.Runtime.CompilerServices

type System.String with
    member s.Shout() = s.ToUpper() + "!"

[<Extension>]
type StringExtensions =
    [<Extension>]
    static member Whisper(s: string) = s.ToLower() + "..."

printfn "%s" ("hello".Shout())
printfn "%s" ("HELLO".Whisper())
HELLO!
hello...

type X with member ... is an F# type extension, visible to F# code. The [<Extension>] form produces a real .NET extension method that C# can use as well.

Making F# friendly to C#

When a library will be called from C#:

  • Put public types in a namespace and expose methods on classes or modules — C# sees F# modules as static classes.
  • Use tupled parameters for public methods, and .NET collection types (IEnumerable<T>, arrays, IReadOnlyList<T>) rather than F# lists.
  • Return Task<T> rather than Async<T>.
  • Avoid exposing Option and discriminated unions in the public API, or add helper members — C# can use them, but awkwardly.
  • [<CLIMutable>] on a record adds a parameterless constructor and setters, which some serializers and ORMs (like Entity Framework) require.

Calling C# from F#

Reference the C# project (dotnet add reference ../MyLib/MyLib.csproj) and use its types as you would any .NET library. Methods are called with tupled arguments, out parameters become tuples (as with TryParse), and properties are set with <- or with named arguments in the constructor call:

let builder = System.UriBuilder(Scheme = "https", Host = "example.com", Path = "docs")
printfn "%s" (builder.Uri.ToString())
https://example.com/docs

The C# course is a good companion: most .NET documentation shows C# first.