F# on the .NET Platform
F# compiles to the same Common Intermediate Language (CIL) that C# and VB.NET target, and it runs on the same Common Language Runtime (CLR) with the same garbage collector and Base Class Library (BCL). Because of this shared foundation, an F# project can reference any .NET assembly — a NuGet package written in C#, System.Net.Http, ASP.NET Core, Entity Framework Core — directly, with no adapter layer, and a C# project can reference a compiled F# DLL and call its public functions and types as ordinary .NET members.
Cricket analogy: Just as England's Bazball approach and Australia's traditional Test-match strategy both operate under the same ICC Laws of Cricket and DRS review system, F# and C# both compile down to the same CIL and run under the identical CLR rules, so neither side needs a special rulebook to play on the other's pitch.
Calling .NET APIs from F#
F# calls .NET methods using the same dot-notation and tupled-argument syntax the BCL expects: System.String.Join(",", items) passes two arguments in parentheses, which looks like an F# tuple but is actually .NET's native multi-argument call convention, distinct from F#'s own curried let add a b = a + b style. Overload resolution, named arguments like StreamReader(path, encoding = Encoding.UTF8), and optional parameters all work exactly as they do from C#, and the use keyword automatically calls Dispose() on IDisposable resources such as a StreamReader at the end of scope.
Cricket analogy: When Virat Kohli calls for a DRS review, he doesn't invent his own signal — he uses the exact hand-signal protocol the ICC specifies, just as F# code calling String.Join(",", items) must use .NET's exact tupled-argument calling convention rather than F#'s own curried style.
Exposing F# Libraries to C#
Because F# supports curried functions, tuples, and lowercase identifiers that feel unnatural in C#, well-designed interop libraries expose members as ordinary tupled methods with PascalCase names, often applying [<CompiledName("Add")>] so a function named add in F# appears as Add to C# callers. Discriminated unions and records are usable from C# but feel foreign — pattern matching a DU from C# requires checking a Tag property or Is* members instead of F#'s match expression, so many teams add explicit static factory methods and ToString overrides to make F# domain types ergonomic for C# consumers.
Cricket analogy: When Sachin Tendulkar's coaching notes, written in his own shorthand, are translated into the official BCCI coaching manual for wider use, they get relabeled with standard terminology; similarly an F# function named add gets a [<CompiledName("Add")>] so it reads naturally in the official C# 'manual.'
Nullability and Interop Attributes
F# idiomatically avoids null in favor of Option<'T>, but .NET APIs and C# libraries routinely return or accept null, so interop code must convert at the boundary using patterns like Option.ofObj or checking isNull x before wrapping. Attributes such as [<CLIMutable>] add a parameterless constructor and mutable properties to an otherwise immutable F# record so serializers like System.Text.Json or ORMs like Entity Framework Core can construct it, and [<AllowNullLiteral>] lets an F# class type accept null when interoperating with a .NET API that expects it.
Cricket analogy: A boundary umpire must convert an ambiguous 'maybe six, maybe four' signal into a definitive decision before it's recorded in the official scorebook; F# interop code must convert .NET's ambiguous null into a definitive Some/None using Option.ofObj before the rest of the F# program can safely proceed.
open System
open System.IO
// Calling a .NET BCL method with tupled arguments
let joined = String.Join(", ", [| "F#"; "C#"; "VB.NET" |])
// Using `use` for IDisposable interop
let readFirstLine (path: string) =
use reader = new StreamReader(path)
reader.ReadLine()
// Exposing an F#-friendly API to C# callers
type Calculator =
[<CompiledName("Add")>]
static member add (x: int) (y: int) = x + y[<CompiledName("...")>] only changes the name .NET sees in compiled metadata — F# code in the same project still calls the function by its original lowercase name, so you get idiomatic casing on both sides of the language boundary without maintaining two APIs.
String.Join(",", items) is not an F# tuple even though it looks like one — it is .NET's standard multi-argument call syntax. Confusing the two is a common source of "this isn't a function" compiler errors when developers new to F# try to pipe a genuine tuple into a curried F# function expecting separate arguments.
- F# and C# compile to the same CIL and run on the same CLR, so assemblies from either language can reference each other directly.
- Calling .NET/BCL methods uses tupled-argument syntax (Method(a, b)), which differs from F#'s native curried function style.
useautomatically disposes IDisposable resources like StreamReader at the end of scope.- [<CompiledName>] lets an F# function keep an idiomatic lowercase name in F# while presenting a PascalCase name to C#.
- Discriminated unions and records are usable from C# but require Tag/Is* checks instead of F#'s match expression.
- Option.ofObj converts .NET null values into F#'s Option type at interop boundaries.
- [<CLIMutable>] adds a parameterless constructor and mutable properties so serializers and ORMs can construct F# records.
Practice what you learned
1. What do F# and C# share that allows an F# project to reference a C# NuGet package with no adapter code?
2. In `String.Join(", ", items)`, what is `(", ", items)` actually?
3. Which attribute lets an F# function named `add` appear as `Add` to C# consumers?
4. Why does `[<CLIMutable>]` get added to an F# record intended for use with Entity Framework Core?
5. What does `Option.ofObj` do?
Was this page helpful?
You May Also Like
Asynchronous Programming in F#
F#'s Async<'T> type and async { } computation expression provide a composable, cold-start model for asynchronous work that predates and interoperates with .NET's Task-based async/await.
F# and Web Development
F# builds web applications through plain ASP.NET Core, the functional Giraffe framework, the higher-level Saturn DSL, and Fable-compiled frontends that share domain types with the backend.
Testing F# Code
F#'s pure, immutable style makes much business logic naturally testable, and frameworks like Expecto and FsCheck lean into that style with lightweight, value-based test suites and property-based testing.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics