
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.
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.