Introduction
In our previous post, we explored the strengths and weaknesses of F# in the .NET 9 ecosystem and compared its capabilities with those of C#. Now, let’s go a step further and see the differences in practice.
This post showcases side-by-side code comparisons for common backend tasks in F# and C#, helping you understand when each language shines and how they differ in syntax, expressiveness, and readability.
📌 Scenario 1: Modeling a Domain with Discriminated Unions / Enums and Classes
✅ F# – Expressive and Exhaustive
type PaymentMethod =
| Cash
| CreditCard of string
| PayPal of string
let describePayment method =
match method with
| Cash -> "Paid in cash"
| CreditCard number -> $"Paid by credit card ending in {number.Substring(number.Length - 4)}"
| PayPal email -> $"Paid via PayPal ({email})"
✅ C# – More Verbose, Uses Inheritance or Enums
public abstract class PaymentMethod {}
public class Cash : PaymentMethod {}
public class CreditCard : PaymentMethod
{
public string Number { get; }
public CreditCard(string number) => Number = number;
}
public class PayPal : PaymentMethod
{
public string Email { get; }
public PayPal(string email) => Email = email;
}
public string DescribePayment(PaymentMethod method)
{
return method switch
{
Cash => "Paid in cash",
CreditCard cc => $"Paid by credit card ending in {cc.Number[^4..]}",
PayPal pp => $"Paid via PayPal ({pp.Email})",
_ => throw new ArgumentOutOfRangeException()
};
}
🔍 Takeaway: F# uses built-in discriminated unions and pattern matching, making this logic more concise and safer (exhaustiveness is checked at compile time).
📌 Scenario 2: Working with Lists
✅ F# – Pipelines and Functions
let isEven x = x % 2 = 0
let square x = x * x
let result = [1..10] |> List.filter isEven |> List.map square
✅ C# – LINQ
var result = Enumerable.Range(1, 10)
.Where(x => x % 2 == 0)
.Select(x => x * x)
.ToList();
🔍 Takeaway: Both languages are concise here. F# pipelines read naturally for functional programmers. C# LINQ is elegant but slightly more verbose and chained.
📌 Scenario 3: Handling Optional Values
✅ F# – Option Type
let divide a b =
if b = 0 then None
else Some(a / b)
let result =
match divide 10 2 with
| Some v -> $"Result: {v}"
| None -> "Cannot divide by zero"
✅ C# – Nullable and Optional (via pattern or Optional)
public double? Divide(double a, double b) => b == 0 ? null : a / b;
var result = Divide(10, 2) is double value
? $"Result: {value}"
: "Cannot divide by zero";
🔍 Takeaway: F# has native Option types; C# supports nullables and can mimic Option-like behavior with third-party libraries or pattern matching.
📌 Scenario 4: Immutable Records
✅ F# – Lightweight by Default
type User = { Name: string; Age: int }
let user1 = { Name = "Anna"; Age = 30 }
let user2 = { user1 with Age = 31 } // copy and update
✅ C# – record
Type (since C# 9)
public record User(string Name, int Age);
var user1 = new User("Anna", 30);
var user2 = user1 with { Age = 31 };
🔍 Takeaway: F# has supported this since the beginning. C# caught up with record
types and now offers a very similar experience.
📌 Scenario 5: Async Workflows
✅ F# – Async Computation Expressions
let fetchData url =
async {
use client = new HttpClient()
let! result = client.GetStringAsync(url) |> Async.AwaitTask
return result.Length
}
✅ C# – async
/ await
public async Task<int> FetchData(string url)
{
using var client = new HttpClient();
var result = await client.GetStringAsync(url);
return result.Length;
}
🔍 Takeaway: Both handle async elegantly, but F# allows compositional async blocks (via computation expressions), which can feel more natural for pipelines and chaining.
🧠 Final Thoughts: Syntax or Semantics?
F# and C# in .NET 9 are both extremely powerful languages. What sets them apart isn’t the underlying platform (they both target the same CLR) but the developer ergonomics and programming philosophy:
Trait | F# | C# |
---|---|---|
Conciseness | High | Medium |
Learning Curve | Steep for OO devs | Lower for most .NET developers |
Immutability | Default | Opt-in |
Pattern Matching | Built-in with exhaustiveness | Improving but less robust |
Functional Composition | First-class citizen | Supported, but less idiomatic |
Tooling Support | Moderate | Excellent |
Community & Ecosystem | Small but passionate | Huge and active |
✅ Summary
This side-by-side comparison makes one thing clear: both C# and F# are capable of solving the same problems, but with different approaches.
- Choose F# if you value clarity through conciseness, immutability by default, and functional domain modeling.
- Choose C# if you want mainstream ecosystem support, top-tier tooling, and a flexible language that balances OO and functional constructs.
If you’re interested in combining F# for core logic and C# for infrastructure or UI layers, that hybrid approach is perfectly viable in .NET 9—and it may offer the best of both worlds.