This article is dedicated to F# language, more precisely how it is compared to C#.
Writing this article was motivated by my love for functional programming, and particularly for F#. My first contact with F# happened more than 10 years ago. Since then, I periodically came back to do something with F#. Mostly reading new F# documentation and articles dedicated to F#, doing small F# projects, and reading endless "Is F# worth learning" questions on Reddit, Quora and other sources.
At some point I got pissed off by all this, especially of "worth learning" questions, and decided to give F# a real try. So, I picked up one of our C# projects (.Net Web API) and started reimplementing it in F#.
That is when I really realized how functional programming style and F# type system was superior to C# OOP and its type system. Nothing against C#. I consider it to be one of the best object-oriented languages.
And C# also has some functional features. But they seem like foreign bodies unnaturally glued to C# OOP style.
Below is a simple square operation written in both F# and C#, to prove the point.
F#
let square x = x * x
and here is C# equivalent
public static class MathHelpers
{
public static T Square<T>(T x) where T : struct
{
dynamic d = x;
return (T)(d * d);
}
}
While it takes eight lines of code to declare a simple square operation in C#, F# achieves the same result in just one. By leveraging automatic type inference, F# removes the need for explicit type declarations and generic math constraints. You don't have to cast generics to dynamic variables or wrap everything in a class - you simply write the logic and let the compiler handle the ceremony.
Why do we need in C# a class definition to be able to define a simple math operation? why do we need this boilerplate code?
So, with those questions in mind, back to my Web API project.
It is a .Net Web API three-layer microservice hosted by Azure, doing CRUD and searching operations in a Cosmos DB, with relatively large code base.
So, I just wrote a simple typical .Net Web API doing Cosmos DB and Blob Storage CRUD operations - with an exception filter, DTOs, business models and some mappers. It is a functioning web app with not much business logic. The intent was to just prove my point.
The code can be found here.
Here is a detailed diagram of the Web API with all its dependencies.
As you see, each implementation is decoupled via an abstraction. You know
Program to an interface, not to an implementation.
So, the question again - do we always need the complexity of defining abstractions and their implementations? And do we always need this ceremonial interface and class definitions, with repetitive dependency injection, to execute a piece of code? Particularly in this concrete example. And particularly when using scoped and transient services.
Is it worth defining interfaces and class implementations for services which live a couple of milliseconds or even seconds. A scoped service's life begins when the request starts and ends when the response is sent. Transient services have no scope - they're created every time they're resolved, even multiple times during the same request.
I know Copilot Agent can generate code for you. The real question is - do we need to define complex objects with dozens of methods if we only call one or two of them per request. One of the main purposes of OOP is encapsulation of data and methods which work on that data. And probably makes sense for long living objects, for example in a desktop application, when using for example UI components.
We have singleton services in .Net web applications. But they must be thread safe and relatively lightweight.
This is where F# comes into play.
I don't intend to make an introduction to F# here or to explain any of FP or F# concepts in detail. There are many F# dedicated resources out there and one of them is F# for fun and profit.
I just want to show how much easier and faster it is to do the same stuff in F# without OOP corsets of C#.
The code can be found here.
And here is the detailed diagram of the web API with all its dependencies, and much less complexity compared to the C# project.
As you notice, there are only module-level dependencies. The domain logic calls repository modules directly, rather than calling interface implementations.
First, I want to compare Program.cs with Program.fs
In Program.cs we register a lot of services with the built-in DI container. Including our own services and their corresponding implementations to be able to inject them later in our constructors.
In Program.fs we still have initially scaffolded starter F# code when selecting a Web API template, and no service registrations. Explanation later in the article.
Second, let's compare the number of projects in C# and F#. We have 8 in C# against 6 in F#. And the reason for that is that I have implementations and their abstractions in one place. So, there are no separate class libraries for abstractions, and their implementations like in C#.
Let's look at the implementation for our Blob Storage files repository:
And below I have the abstraction:
And what is interesting here is the latter.
I abstract the implementation away by using partial application, a functional programming technique where we take a function with multiple arguments and pre-fill some of them, producing a new function that takes the remaining arguments.
So pre-filled here is BlobServiceClient service, the specific implementation of Blob Storage repository. And with the help of partial application, we can abstract the implementation away by hiding the implementation details, as I do here with BlobServiceClient which is used to access Azure Blob Storage.
So, now I just need to open the Shopping.Files.Repository.Product module, under our Shopping.Products.Domain module and voila you can call the abstraction.
There are no implementation details when calling the abstraction, because it is hidden by partial application. And it is clear what implementation will be called because of the opened full module path, which provides the necessary context. And there is no constructor with dependency injection because there is nothing to inject. Therefore, no service registrations with the built-in DI container are necessary -for our services.
I still use a built-in DI container in our Blob Storage repository project, to create a BlobServiceClient singleton and provide it when necessary.
In a typical ASP.NET Core app the DI container is built once at startup by the host builder and reused. Here, because this module is standalone, the lazy ensures the same behavior - deferred construction and single instance - without relying on the web host. If we were already inside the web app's startup, we'd normally register BlobServiceClient in ConfigureServices and resolve it via the host's provider; the lazy pattern is mainly useful when we're outside that lifecycle (e.g., library, script, or manual invocation).
I could of course just create an instance of BlobServiceClient and provide the cached instance, because of lazy block, when necessary. I use the built-in ID container with generic capabilities in case I want to register more services. Anyway, the creation of BlobServiceClient is encapsulated in the repository itself for further use, thus completely decoupling it from Web API project.
Now compare this with code overhead needed to achieve the same result in C#.
We have file repository abstraction
We have the implementation, with a base class
And of course we have to inject then IProductFileRepository dependency in order to use it
C# adds a lot of syntactic overhead - with all the abstraction and implementation definitions and the required dependency‑injection setup - compared to F#, even for simple file‑related operations.
That's all for now.
In part 2 we will extend the example with Customer and Order business domain and new implementations, and and talk about token efficiency.



Top comments (0)