F# for the Curious C# Developer
- ShiftQuality Contributor
- Oct 23, 2025
- 5 min read
The previous posts in this path covered dependency injection, Entity Framework Core, and middleware. This post steps sideways to explore F# — the other major .NET language, and one that most C# developers know exists but have never tried.
F# is not a competitor to C#. It is a complement. It runs on the same .NET runtime, uses the same libraries, and interoperates seamlessly with C# code. What it offers is a different way of thinking about problems — a functional-first approach that makes certain categories of problems (data transformation, domain modeling, concurrency, and correctness) dramatically simpler to solve.
You do not need to abandon C# to benefit from F#. Understanding functional patterns through F# makes your C# better, and knowing when to reach for F# gives you a tool that excels where C# is merely adequate.
What Makes F# Different
F# is a functional-first language. This means functions are the primary building blocks, data is immutable by default, and the type system is designed to make illegal states unrepresentable.
Immutability by default. In F#, values are immutable unless you explicitly mark them as mutable. This eliminates entire categories of bugs — you cannot accidentally modify a value that another part of the code depends on. In C#, you opt into immutability (readonly, records). In F#, you opt out of it.
Type inference. F# infers types aggressively. You rarely need to write type annotations — the compiler figures out the types from how values are used. This produces code that is both shorter and type-safe. The compiler catches type errors without you having to annotate everything.
Pattern matching. F# has exhaustive pattern matching — a powerful way to deconstruct data and handle different cases. The compiler warns you if you miss a case, which prevents the "forgot to handle the new enum value" bugs that plague C# switch statements.
Pipeline operator. The |> operator passes the result of one function to the next, creating readable data transformation chains: data |> filter isActive |> map toSummary |> sortBy name. This reads like a description of the transformation, not like nested function calls.
Domain Modeling: Where F# Shines
F#'s discriminated unions make domain modeling precise. Consider modeling a payment status in C#:
You might use an enum, but enums cannot carry data. A "Refunded" status needs a refund amount and date — information that does not fit in an enum. You end up with a status enum plus nullable fields for refund data, and the compiler cannot enforce that refund fields are only populated when the status is Refunded.
In F#, a discriminated union models this precisely: Pending, Approved, Declined of reason: string, Refunded of amount: decimal * date: DateTime. Each case carries exactly the data it needs. Pattern matching forces you to handle every case. You cannot access the refund amount on an Approved payment because the type system prevents it.
This "make illegal states unrepresentable" philosophy means that if your code compiles, many categories of bugs are already eliminated. You cannot create a refunded payment without specifying the refund amount. You cannot process a payment without handling the declined case. The type system enforces your business rules.
Data Transformation Pipelines
F# excels at transforming data through a series of steps. The pipeline operator, combined with higher-order functions (map, filter, reduce, groupBy), produces code that reads like a specification of what the transformation does.
Processing a list of orders: take all orders, filter to those placed this month, group by customer, calculate total spend per customer, sort by spend descending, take the top 10. In F#, this is a pipeline of five functions, each one transforming the output of the previous one. Each step is independently testable, independently readable, and independently replaceable.
C# has LINQ, which provides similar capabilities. But F#'s pipeline syntax and the broader functional ecosystem make data transformation feel more natural. When your task is "take this data and transform it through a series of steps," F# is often the more expressive choice.
Interoperability: F# and C# Together
F# and C# interoperate seamlessly because they compile to the same intermediate language and run on the same runtime. You can:
Call C# libraries from F# code (all NuGet packages work in F#). Call F# libraries from C# code (F# modules appear as static classes, discriminated unions appear as class hierarchies). Mix F# and C# projects in the same solution. Gradually introduce F# into an existing C# codebase by adding F# projects for specific domains.
The practical approach: use F# where its strengths align with your needs. A data processing pipeline in F#. A domain model with complex business rules in F#. A web API endpoint in C# that calls F# domain logic. The languages complement each other rather than competing.
When to Reach for F#
F# is not always the right choice. For ASP.NET web applications with extensive middleware, dependency injection, and framework integration, C# is more natural — the framework is designed for C#, the documentation uses C#, and the community tooling targets C#.
F# shines for: data processing and transformation (ETL pipelines, report generation, data analysis), domain modeling with complex business rules (financial calculations, scheduling, compliance logic), concurrent and parallel processing (F#'s immutability makes concurrency safer by default), scripting and automation (.fsx scripts provide a lightweight way to run F# without a project), and any task where correctness matters more than framework integration.
The recommendation for the curious C# developer: start with a small, standalone project. A data processing script. A domain model for a side project. A utility library. Experience the functional approach on a manageable problem before deciding whether to incorporate F# into a larger codebase.
Functional Concepts That Transfer Back to C#
Even if you never write production F#, understanding functional concepts improves your C# code.
Immutability. Use records, init-only properties, and readonly collections in C#. Immutable data is easier to reason about, safer in concurrent scenarios, and less prone to state-related bugs.
Pure functions. Functions that take inputs and return outputs without side effects are easier to test, easier to understand, and easier to compose. Separating pure logic from side effects (database calls, HTTP requests) makes your C# code more testable.
Pattern matching. C# has adopted pattern matching features from F# — switch expressions, property patterns, relational patterns. Use them. They produce cleaner code than long if-else chains.
Pipeline thinking. LINQ is C#'s pipeline operator. Chain operations instead of using intermediate variables. Think about data transformation as a series of steps rather than a series of state mutations.
The Takeaway
F# is a functional-first language on the .NET platform that excels at data transformation, domain modeling, and correctness-oriented programming. It interoperates seamlessly with C#, so you can adopt it incrementally for the problems where it provides the most value.
For the curious C# developer, F# is not a replacement — it is an expansion of your toolkit. The functional concepts it teaches transfer back to your C# code, and the problems it solves elegantly are problems you have probably struggled with in C#'s object-oriented paradigm.
Next in the "Building Real .NET Apps" learning path: We'll cover configuration and secrets management — how .NET's configuration system handles environment-specific settings without hardcoding values.



Comments