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
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.
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.
Calling C# / the BCL from F#
F# calls .NET methods directly; an out param comes back as a tuple, named args work, and .NET 9's TryParse needs an annotation.
- F# → C#the out-param tuple and the FS0041 trap
Exposing F# to C#
A module is a static class, a record has read-only properties, a union is a class hierarchy; [<CLIMutable>] opens the record.
- C# → F#how F# declarations look from the C# side
The null & Nullable boundary
C# returns null and Nullable<T>; lift both to Option at the seam with ofObj / ofNullable.
- the edge
Option.ofObj,ofNullable, guarded once
References
Sources
- Microsoft — F# documentation — the interop guidance: calling .NET,
[<CLIMutable>], and how F# types appear to other CLR languages. - Microsoft — .NET documentation — the CLR,
outparameters,Nullable<T>, and the BCL methods F# calls. - FSharp.Core API reference —
Option.ofObjandOption.ofNullable, the seam converters.
Related in this course
- C2.08.1 — Calling C# / the BCL from F# — the out-param tuple and the FS0041 trap.
- C2.03 — From null to Option, from exceptions to Result — the interop-boundary dive this module extends.
- C1.08 — Option, Result & railway-oriented errors —
Optionin language depth.