F# for .NET
Trigger On
- the task touches
.fs,.fsx,.fsi, or.fsprojfiles - domain logic benefits from records, discriminated unions, options, results, or exhaustive pattern matching
- generated code needs a strongly typed functional model instead of nullable primitive state
- F# code must interoperate with C# or other .NET libraries
- the repository needs project setup, source ordering, or validation for F# code
Do Not Use For
- C# language feature selection; use
modern-csharp - analyzer-only, formatter-only, or CI-only work with no F# language decisions
- one-off REPL or scripting exploration that does not affect project code; use
fsi - non-.NET functional languages
Project Setup
Use the .NET SDK templates when adding F# projects.
dotnet new classlib -lang "F#" -o src/Domain
dotnet new console -lang "F#" -o src/App
dotnet new xunit -lang "F#" -o tests/Domain.Tests
dotnet sln add src/Domain/Domain.fsproj tests/Domain.Tests/Domain.Tests.fsproj
dotnet add tests/Domain.Tests/Domain.Tests.fsproj reference src/Domain/Domain.fsproj
dotnet build
FSharp.Core is normally supplied by the F# SDK project. Add or pin it explicitly only when the repo has a package policy that requires deterministic package versions.
dotnet add src/Domain/Domain.fsproj package FSharp.Core
Source Ordering
F# compiles files in the order listed in the project file. Define types and modules before files that consume them.
<ItemGroup>
<Compile Include="Domain.fs" />
<Compile Include="Validation.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
When adding a file, update the .fsproj intentionally. Do not assume wildcard ordering.
Workflow
- Inspect the existing
.fsproj,.fs,.fsi, and.fsxfiles to determine compile order, public boundaries, and whether the code is F#-first or C#-facing. - Choose the smallest F# model that represents the invariant: records for named product data, discriminated unions for closed state,
optionfor absence, andResultfor expected failures. - Add or edit source files in dependency order, then update the
.fsprojcompile list before consumers reference the new code. - Review public API shape for .NET interop. Translate F#-specific internal models to DTOs, methods, or
Try*patterns when C# callers need a stable surface. - Run
dotnet build, targeted tests, and any relevantdotnet fsiprobes before returning the final result.
Current Upstream Notes
- The August 2026 F# overview continues to emphasize functional-first programming on .NET with records, discriminated unions, pattern matching, units of measure, type providers, and interop. Keep F# guidance domain-model oriented rather than translating C# object patterns mechanically.
- Use
fsifor exploratory scripts, but move durable code into ordered.fsprojfiles before it becomes production behavior.
Practical Patterns
Model State With Records And Unions
Prefer a type that names every valid state over loose strings, nullable values, or parallel booleans.
module Orders
type OrderId = private OrderId of string
module OrderId =
let tryCreate value =
if System.String.IsNullOrWhiteSpace value then
Error "Order id is required."
else
Ok (OrderId value)
type Payment =
| Card of last4: string
| Wire of iban: string
| PurchaseOrder of number: string
type Order =
{ Id: OrderId
Customer: string
Payment: Payment }
let describePayment payment =
match payment with
| Card last4 -> $"card ending {last4}"
| Wire iban -> $"wire transfer {iban}"
| PurchaseOrder number -> $"purchase order {number}"
Compose Validation With Result
Return Result<'T,'Error> when callers must handle failure and when failures are part of the domain.
module Pricing
type Price =
private
| Price of decimal
module Price =
let tryCreate amount =
if amount < 0m then
Error "Price cannot be negative."
else
Ok (Price amount)
let value (Price amount) = amount
type Line =
{ Sku: string
Quantity: int
UnitPrice: Price }
let tryCreateLine sku quantity amount =
match Price.tryCreate amount with
| Ok price when quantity > 0 ->
Ok { Sku = sku; Quantity = quantity; UnitPrice = price }
| Ok _ ->
Error "Quantity must be positive."
| Error message ->
Error message
Read And Transform Data
Use pipelines for readable transformations, but stop before the pipeline hides error handling or allocation cost.
open System.IO
let readActiveUsers path =
File.ReadLines path
|> Seq.skip 1
|> Seq.choose (fun line ->
match line.Split(',') with
| [| id; name; "active" |] -> Some {| Id = id; Name = name |}
| _ -> None)
|> Seq.toList
Write A Small .NET Boundary
Make public interop APIs easy for C# callers. Hide F#-specific details behind functions, methods, or DTO records when needed.
namespace Company.Domain
type InvoiceDto =
{ Id: string
Total: decimal
IsPaid: bool }
module Invoice =
let markPaid invoice =
{ invoice with IsPaid = true }
Interop Guidance
- Keep F# discriminated unions inside F# boundaries unless C# consumers are expected to understand their generated shape.
- Use records or explicit classes for public cross-language DTOs.
- Use
optioninternally; translate to nullable annotations,Try*methods, orResultat C#-first public boundaries. - Avoid throwing for expected domain failures. Use exceptions for unexpected infrastructure failures.
- Prefer
task { }orTask-returning APIs at .NET interop boundaries; useAsync<'T>when the code is F#-first.
Review Checklist
- The
.fsprojcompile order matches dependency order. - All union cases are handled explicitly; wildcard branches are justified.
- Public types have stable names and shapes for downstream .NET consumers.
- Domain failures are modeled with
optionorResult, not undocumentednull. - Pipelines remain readable and do not hide repeated enumeration of expensive sequences.
- Tests cover at least one success and one failure path for each domain constructor or validator.
Validate
Run the narrowest relevant checks after changes:
dotnet build
dotnet test
dotnet fsi scripts/check.fsx
If adding a new F# file, also inspect the project file diff to confirm the compile order is deliberate.
Sources
- https://learn.microsoft.com/dotnet/fsharp/what-is-fsharp
- https://learn.microsoft.com/dotnet/fsharp/get-started/get-started-command-line
- https://learn.microsoft.com/dotnet/fsharp/language-reference/discriminated-unions
- https://learn.microsoft.com/dotnet/fsharp/language-reference/pattern-matching
- https://learn.microsoft.com/dotnet/fsharp/language-reference/results