C# or F#: Which one to choose for more robust and maintainable code?
A pragmatic comparison between C# and F# through the lens of Software Craftsmanship: expressiveness, testability, robustness, and domain modelling. No proselytising - just the facts.
By nicolas — Published on April 13, 2025

When talking about software quality, discussions often circle back to principles like readability, testability, and the ability to evolve code without pain. In other words: writing high-quality code. Within the .NET ecosystem, C# is the default language. Yet F#, often seen as niche, offers a different approach - one that sometimes aligns more closely with these values.
Rather than pitting them against each other, this article presents a fair, experience-based comparison. The goal: to understand when one language or the other might better support craftsmanship principles.
1. Expressiveness and Verbosity
C# has evolved significantly in recent years. The arrival of records, init setters, pattern matching and simplified lambdas has made the language more expressive. Still, its object-oriented structure - with classes, properties and attributes - remains relatively verbose.
F#, on the other hand, is designed for conciseness. Everything is built to minimise syntactic noise. The code becomes more readable, provided you embrace the functional style. A simple Person type with two fields is a one-liner, no accessors required.
type Person = { Name: string; Age: int }
In C#, you'd write:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Or, using C# 9 syntax:
public record Person(string Name, int Age);
In practice:
- C# is more verbose, but familiar to most .NET teams.
- F# allows you to express more with less code, but requires an initial learning curve.
2. Domain Modelling
Clear and exhaustive domain modelling is a key pillar of software craftsmanship. C# offers solid structure, especially with records. However, handling complex business cases (closed types, alternatives, etc.) often requires more boilerplate.
F# shines here with discriminated unions, which make it easy to define all possible cases of a domain concept explicitly. For example, an authentication result might be either a success or an error. This is naturally expressed in F#, and the compiler enforces completeness.
type AuthResult =
| Success of string
| Error of string
In C#, you’d typically resort to classes or enums, which may be less clear and more error-prone:
public class AuthResult
{
public string? Success { get; }
public string? Error { get; }
private AuthResult(string? success, string? error)
{
Success = success;
Error = error;
}
public static AuthResult CreateSuccess(string token) => new(token, null);
public static AuthResult CreateError(string message) => new(null, message);
}
In practice:
- C# works well for standard models, but extra code is needed to enforce case coverage.
- F# encourages clear, type-safe domain modelling by design.
3. Testability and Effect Isolation
Craftsmanship also means writing code that's easy to test. C# supports this with mature tooling (xUnit, Moq, etc.) and practices like dependency injection. But it can quickly become heavy: mocks, services, interfaces everywhere.
F# naturally encourages pure functions. Side effects (database access, I/O) are pushed to the edges of the system. As a result, most business logic can be tested directly - no framework required.
In practice:
- In C#, testing often requires more architecture (interfaces, mocks, etc.).
- In F#, you frequently test pure functions with no structural overhead.
4. Refactoring and Robustness
Quality code is code you can confidently evolve. In C#, tools like Resharper or Rider make refactoring easier. However, some errors slip past compilation - especially incomplete switches or null related issues.
F# offers stronger guarantees thanks to exhaustive pattern matching and strict typing. Removing a case from a discriminated union will force updates wherever it’s used. Nothing is forgotten.
In practice:
- C# is well tooled, but type safety is not always enforced.
- F# provides a stricter safety net, helping secure refactors.
5. Ecosystem and Integration
C# dominates the .NET ecosystem. Frameworks, tutorials, libraries and IDEs are built with it in mind. That’s a major advantage, especially for teams or long-term projects.
F# is less widespread. Yet it integrates perfectly with .NET - you can use the same NuGet packages, share code with C#, and enjoy Visual Studio or Rider support. The community is smaller though, and some frameworks (like WinForms or MAUI) are less accessible in F#.
In practice:
- C# benefits from a mature and universal ecosystem.
- F# integrates well, but can require a more resourceful approach.
6. Recommended Use Cases
This is not about declaring F# "better" than C#. Each language has strengths depending on the context.
Use C# when:
- You're building traditional object-oriented projects.
- The team is 100% C# and not interested in functional approaches.
- You're targeting rich client apps (MAUI, WPF, etc.).
Use F# when:
- You’re developing isolated modules with complex business logic.
- Testability, robustness and conciseness are priorities.
- You're designing a modular architecture (microservices, rule engines, analytics tools, etc.).
Conclusion
F# won’t replace C#, but it can expand the toolbox of a .NET developer committed to writing clean, maintainable code. There’s no need to use F# for everything. But in specific contexts - especially when business complexity grows - it can help produce clearer, more reliable and longer-lasting code.
A thoughtful developer knows how to choose the right tool for the job. That’s exactly the mindset this article encourages.