jonnifyfsharp

Module C2.08

Interop — calling C# from F# and back

One runtime, two languages. The interop facts live at the seam; the F# core stays clean.

F# and C# compile to the same CLR, so an F# project calls the BCL and existing C# libraries directly, and C# code calls F# types in return. Most of it is unremarkable — a method call is a method call. A handful of details bite a C# developer on day one, and they are all at the boundary: an out parameter comes back as the second half of a tuple, named and optional arguments work, and in .NET 9 a TryParse on an inferred parameter is ambiguous and fails to compile (FS0041). Going the other way, an F# module appears to C# as a static class, a record as a class with read-only properties, a discriminated union as a class hierarchy, and a function value as FSharpFunc<_,_>. And the platform's null and Nullable<T> convert to Option at the edge with Option.ofObj and Option.ofNullable. This module covers both directions in three dives.

The seam, and what crosses it

Interop is not a separate language feature — it is the ordinary CLR fact that F# and C# share a runtime, seen from each side. The work is small and local: a thin layer where the two meet, translating the few things that look different. The three dives below take the two directions and the one type that has no F# equivalent.

The first dive goes F# → C#: F# calls the BCL and C# libraries, where the standout nuance is the out parameter that comes back as a tuple — and the .NET 9 TryParse trap that fails to compile on an inferred argument. The second goes C# → F#: how an F# module, record, union, and function value appear from C#, and the [<CLIMutable>] attribute that makes a record friendly to C# constructors and serializers. The third is the null and Nullable<T> boundary — C# returns both, F# types have neither, so the seam lifts each into an Option once, and no null reaches the core again.

The three dives · select one

C2.08.1 calling C# C2.08.2 exposing F# C2.08.3 the null boundary F# → C# C# → F# the edge
C2.08.1 · Calling C# / the BCL from F#

F# calls .NET methods directly. The standout nuance: a BCL out parameter comes back as the second half of a tuple — System.Int32.TryParse "42" is (true, 42). The dive also captures the .NET 9 FS0041 trap on an inferred argument.

You learn how F# calls the BCL, and the out-param tuple

Open this dive · or pick another above

Two languages, one runtime. The few things that differ all sit at the boundary, and each dive names exactly which.

The seam, in one line each

The whole module is concrete. Calling out, an out parameter is not assigned through a variable — it returns as a tuple. Calling in, a record is a class with read-only properties unless [<CLIMutable>] opens it. At the edge, a C# null lifts to None. The same shapes run through all three dives.

C# — an out parameter assigned through a variable

bool ok = int.TryParse("42", out var v);   // ok = true, v = 42 — two results, one through out

F# — the same call returns a tuple

let okv = System.Int32.TryParse "42"   // val okv : bool * int = (true, 42)
// the out parameter is the second half of the tuple — no out variable

The C# call

An out parameter assigned through a variable; a record with a C# constructor; a method that may return null.

The F# form

The out result is a tuple; the record needs [<CLIMutable>] to take a constructor; the null lifts to Option at the seam.

The three dives

Read them in order. Each stands on valid F# beside the idiomatic C# it meets: every // val it : echo below was produced by dotnet fsi, and the interop facts come from the .NET documentation. Together they take a C# developer across the seam in both directions, and contain the one type — null — that the F# core never has to hold.

live module hub built dive ready soon specified

References

Sources

Related in this course