Comparing F# and C#
If you haven’t tried F# before
As you might already know, F# is a dotnet language, so you don’t need to install anything extra, you get it out of the box with dotnet-sdk.
If you haven’t before, I invite you to shake hands with your neighbor F#, and play out a bit, before reading further; here is a quick and awesome getting started.
dotnet new console -lang F#
A simple WebAPI project
dotnet new webapi -lang F#
In this repo I created a simple CRUD web API in C# with Swagger and ef-core migrations, with a simple background job calling an endpoint with an HTTP client (swagger pets sample endpoint).
After that, I created a copy of that same API in F#, but using features peculiar only to F# language, mostly type providers, in detail:
- FSharp.Data for appsettings.ENV.json
- FSharp.Data.SqlProvider for the ef-like db-context generated at project loading time . An alternative exists for a smarter dapper-like experience, with type checking, FSharp.Data.SqlClient
- SwaggerProvider, for generating HTTP Client classes and DTO Models at compile time using the open-API specification of the external REST HTTP pets endpoint.
Since I didn’t use entity framework, I replaced the migration part with a simplified DbUp approach.
On Type Providers
The cool thing about Type Providers is that you can just use them, the best word to describe them is magic that makes your life easier.
If you haven’t tried them before, you should now create an F# console program (or script), add FSharp.Data nuget package, and play with the JsonProvider or CsvProvider, to have an idea of what it is, so you can stay 100% on track to what follows! It’s not required, but would make much sense, and I ensure you it will be worth it.
dotnet new console -lang F#
dotnet add package FSharp.Data
An F# type provider is a component that provides types, properties, and methods for use in your program. Type Providers generate what are known as Provided Types, which are generated by the F# compiler and are based on an external data source.
For example, an F# Type Provider for SQL can generate types representing tables and columns in a relational database. In fact, this is what the SQLProvider Type Provider does.
Provided Types depend on input parameters to a Type Provider. Such input can be a sample data source (such as a JSON schema file), a URL pointing directly to an external service, or a connection string to a data source. A Type Provider can also ensure that groups of types are only expanded on demand; that is, they are expanded if the types are actually referenced by your program. This allows for the direct, on-demand integration of large-scale information spaces such as online data markets in a strongly typed way. [src]
Good questions to ask yourself while reading further
Think about CHANGE in a piece of software, in a wider and long term context, try to think years instead of days or weeks.
Think about the rippling effect of that changes, and what is needed to cope with that change in both C# and F# solutions. Is it the same effort?
- Change in software is constant! Almost nothing is forever
- What is a good language investment for Core Business Logic?
- Most frequent changes involve data changes: API methods and models, DB models, files.
Much software will get older than people eventually, and my point is, can we neglect some practical advantages when choosing a language over another?
- DB access: we start in the sample code just with 1 entity (just to show the idea), think about what happens when needing to extend the domain. Think about adding new tables, new relations, new entities.
What differs between C# and F#? - appsettings.ENV.json: think about adding and changing configuration over time, think about configuration “validation”. what is more safe, who can make mistakes at runtime? How the F# compiler can ensure compile-time check with type providers.
- HTTP API Clients → how many changes are needed every time a new endpoint in the external Pets API has to be mapped to the client, changes in DTO contract? Do we need to add more and more all the time? how can we detect change? What is the most efficient way of handling with this in C# and F#?
Comparing Finally! Dear Lord!!!
I advise if you want to follow directly in code, by cloning this
git clone https://github.com/jkone27/FsharpWebApi.git
Startup
For startup configuration we have basically no differences between C# and F# implementation. The only apparent change being the automatic type inference in F# where you do not have to specify return types (void in C#),
and configuration constructor parameter is already available as private member when injected, without an extra line for declaring it (gray as it’s unused in the example), whereas in C# that requires an extra line, even if declared as private member instead of property.
In C#
In F#
Configure Services
For the Configure Services method implementation, we can start noticing important differences:
- No DBContext has to be added in F#, as that’s generated by a type provider, no need to update models or EF mappings in F#. This is done by SqlProvider when loading the solution, connecting to the local or development DB connection directly.
- No APIClientsConfig model for appsetting.json has to be added.
A Singleton instance of the provided AppSettingsProvider.Load(…) is generated, based on the current executing environment, using FSharp.Data.JsonProvider, for all possible future changes of any appsetting.json - Adding API clients and Hosted Jobs doesn’t change, as we can say in general any registration doesn’t change at all
- the “funny” ! symbol at the front of F# registrations is just a custom syntactic sugar, so that we don’t have to add an explicit |> ignore at the end of each line. F# let’s you easily implement custom operators if you need them, but shouldn’t be abused. For the sake of the presentation, you can ignore this completely.
- the internal implementations of ApiClient and PersonsRepository changes consistently (and it’s simpler in a way), as they both make use of F# type providers, as we will see later on.
in C#
In F#
Configure Application
There is substantially zero changes here as well, so all is well (ignore the ! symbol again, as that’s just for warnings)
in C#
In F#
API Controller
Some small differences exists in API Controller, because of some needed cast and base reference in the F# version.
In general also the Object Oriented aspect of F# seems more concise than the slightly more verbose C# version, in respect of members and constructor parameters.
Another “small” but important advantage derives from the implicit magic F# type inference, the unfortunately not well known System-F type system, with automatic type inference and generalization, which F# partially inherits from it’s dad OCAML.
In this C# cannot compete here, and possibly might never be able to, as being a child of the C-language family instead of the ML-language family.
We can note also F# version uses pattern matching, instead of if statement. It would be possible to also use an if else statement, but a C# form pattern matching is already in place in the latest versions of the language, so everyone should start getting acquainted with it.
In F#
We cane make use the library FSharp.Control.Tasks.V2 for adding the task computation expression, over the native F# async computation expression, which slightly reduces verbosity.
In C#
API Client
The F# API Client makes use of type providers, so it builds all methods and models based on the openapi specification using the SwaggerProvider, be it a live endpoint or a development/locally stored openapi.json spec.
At runtime, it will invoke the properly configured endpoints in appsettings.json.
The C# version obviously doesn’t have these advantages, even though recently an openApi client source generator has been added to aspnetcore project file capability, so that shortens the gap between C# and type providers, and this is an awesome achievement for the whole .net community I must say.
Here a note on Source Generators.
In C#
In F#
Persons Repository
The Repository Class in F# is again highly simplified by the access to a DB context which is generated at startup by the SqlProvider, so that all DB changes are promptly available to the developer, with zero changes needed to models or mapping.
The developer can consistently focus on changing the database in SQL scripts only, removing the ORM gap and the need for a “manual” ORM.
The real object relational mapping is already taken care of from the type provider.
In C#
in F#
Domain Mapper
In regards to domain mapping, the difference is not really meant, as also in C# the use of AutoMapper library could be avoided.
I still anyway find the mapping as a type extension in the F# solution a relatively elegant solution, but I am no expert.
Type extensions in F# have some similarities with C# extension methods, even though Ifind them more concise, not requiring an external static class wrapper to define the extension.
In C#
In F#
Conclusion
As you can notice, the F# solution is much more lightweight.
Below is the C# version again, notice the extra models needed and extra folders, and again, imagine this difference in a 2–3 years project horizon or even longer.
So a final diagram illustrating my thoughts on the cost of change of these 2 languages compared, within the same framework and runtime
I would add also early integration, since we use data to generate most of our types, assuming we can use reliable data, as close as possible to production, than we have a good step up also in “contract testing” by simply using type providers, contract testing will be done by the compiler itself, and in much more places then previously possible.
Here is the link to the source code.
Hopefully this can be useful to anyone wanting to try out F# for a real web-API, maybe starting from C# knowledge.
I have great hopes for the upcoming NET5 and F# 5.
And after this i suggest is possible to venture towards more functional frameworks like Giraffe/Suave, ore give Saturn a try.
Have Fun! and spread the F# love