Intro to F# Web APIs

30 October 2019

Updated: 03 September 2023

So we’re going to be taking a bit of a look on how you can go about building your first F# Web API using .NET Core. I’m going to cover a lot of the basics, a lot of which should be familiar to anyone who has worked with .NET Web Applications and F# in general.

Along the way I’m also going to go through some important concepts that I feel are maybe not that clear from a documentation perspective that are actually super relevant to using this F# in a real-life context

If you’re totally new to F# though you may want to take a look at F# for Fun and Profit or my personal quick reference documentation over here

Getting Started

Assuming you’ve got the .NET Core SDK with F# installed, you can simply create a new project with the following:

1
dotnet new webapi --language F# --name FSharpWebApi
2
3
code .\FSharpWebApi

Alternatively, if you’re feeling a little unexperimental you can use the Visual Studio project creation wizard, psshhtt

Once you have the project open you can run the following command to launch the application:

1
dotnet run

Which should start the application on https://localhost:5001 and http://localhost:5000, you can see the current existing endpoint at /weatherforecast, this is handled by the Controllers/WeatherForecastController.fs file

Looking Around

Looking at the structure of the project files you should see the following:

1
FSharpWebApi
2
│ appsettings.Development.json
3
│ appsettings.json
4
│ FSharpWebApi.fsproj
5
│ Program.fs
6
│ Startup.fs
7
│ WeatherForecast.fs
8
9
├───Controllers
10
│ WeatherForecastController.fs
11
12
└───Properties
13
launchSettings.json

So, mostly we see the typical Web API stuff that we’d expect for a C# project such as the startup and program files. In F# they serve pretty much the same purpose.

Looking at the Program.fs file we can see that it contains the main function and configures the Web Host, next we can see that the Startup.fs file contains the usual configuration methods. We should note that the method calls within these functions are piped to an ignore so the the functions to not return their respective Builders as this will break the application

The Program.fs and Startup.fs files can be seen below

Program.fs

1
namespace FSharpWebApi
2
3
module Program =
4
let exitCode = 0
5
6
let CreateHostBuilder args =
7
Host.CreateDefaultBuilder(args)
8
.ConfigureWebHostDefaults(fun webBuilder ->
9
webBuilder.UseStartup<Startup>() |> ignore
10
)
11
12
[<EntryPoint>]
13
let main args =
14
CreateHostBuilder(args).Build().Run()
15
16
exitCode

Startup.fs

1
namespace FSharpWebApi
2
3
type Startup private () =
4
new (configuration: IConfiguration) as this =
5
Startup() then
6
this.Configuration <- configuration
7
8
// This method gets called by the runtime. Use this method to add services to the container.
9
member this.ConfigureServices(services: IServiceCollection) =
10
// Add framework services.
11
services.AddControllers() |> ignore
12
13
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
14
member this.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
15
if (env.IsDevelopment()) then
16
app.UseDeveloperExceptionPage() |> ignore
17
18
app.UseHttpsRedirection() |> ignore
19
app.UseRouting() |> ignore
20
21
app.UseAuthorization() |> ignore
22
23
app.UseEndpoints(fun endpoints ->
24
endpoints.MapControllers() |> ignore
25
) |> ignore
26
27
member val Configuration : IConfiguration = null with get, set

Next we have the FSharpWebApi.fsproj file which contains references to the relevant code files. It’s important to note that the order of the files in the ItemGroup specifies the order that files depend on each other. Lower files depend on files higher up

1
<Project Sdk="Microsoft.NET.Sdk.Web">
2
3
<PropertyGroup>
4
<TargetFramework>netcoreapp3.0</TargetFramework>
5
</PropertyGroup>
6
7
<ItemGroup>
8
<Compile Include="WeatherForecast.fs" />
9
<Compile Include="Controllers/WeatherForecastController.fs" />
10
<Compile Include="Startup.fs" />
11
<Compile Include="Program.fs" />
12
</ItemGroup>
13
14
</Project>

Lastly, we have a controller that resides in the Controllers/WeatherForecastController.fs with its types defined in the WeatherForecast.fs file. Looking at the WeatherForecast.fs file we can see that the type has a few simple properties and one function

WeatherForecast.fs

1
namespace FSharpWebApi
2
3
open System
4
5
type WeatherForecast =
6
{ Date: DateTime
7
TemperatureC: int
8
Summary: string }
9
10
member this.TemperatureF =
11
32 + (int (float this.TemperatureC / 0.5556))

Next up, we can see the controller which contains a single GET endpoint which delivers a random array of weather forecasts. Here we can see a few different things. First, the namespace is FSharpWebApi.Controllers, this pretty much follows the .NET standard of the Namespace being related to the Folder name, we can also see the ApiController attribute that adds some useful functionality for basic API handling and the Route attribute that states the controller route

The WeatherForecastController type defines the controller and that it inherits from ControllerBase, additionally the constructor requires the ILogger service which will be provided by DependencyInjection

Lastly, looking at the __Get method we can see the HttpGet attribute that specifies that this is a Get Method, and the __ shows that we don’t care about references to the function’s this, and the return type for the function is an array of WeatherForecast

WeatherForecastController.fs

1
namespace FSharpWebApi.Controllers
2
3
open System
4
open Microsoft.AspNetCore.Mvc
5
open Microsoft.Extensions.Logging
6
open FSharpWebApi
7
8
[<ApiController>]
9
[<Route("[controller]")>]
10
type WeatherForecastController (logger : ILogger<WeatherForecastController>) =
11
inherit ControllerBase()
12
13
let summaries = [| "Freezing"; "Bracing"; "Chilly"; "Cool"; "Mild"; "Warm"; "Balmy"; "Hot"; "Sweltering"; "Scorching" |]
14
15
[<HttpGet>]
16
member __.Get() : WeatherForecast[] =
17
let rng = System.Random()
18
[|
19
for index in 0..4 ->
20
{ Date = DateTime.Now.AddDays(float index)
21
TemperatureC = rng.Next(-20,55)
22
Summary = summaries.[rng.Next(summaries.Length)] }
23
|]

Creating a Controller

Creating a new controller is not particularly complex given that we have the above as a starting point.

Get Handler

We’re going to create a handler that is able to return a simple message for an even param, and a 404 for a odd param in order to look at how we can return actual response codes in cases where we aren’t always able to return something of a constant type

First, we can create a Controllers/MessageController.fs file with just some basic scaffolding to start with. We’ll define a Get controller that just returns the id it receives as a route param multiplied by two if the the result shouldDouble param is set to true. Additionally we can see the sprint function used to format the output as a string

Before we can add the data to the actual controller we need to add the <Compile Include="Controllers/MessageController.fs" /> to the ItemGroup in the FSharpWebApi.fsproj file, :

FSharpWebApi.fsproj

1
<ItemGroup>
2
<Compile Include="WeatherForecast.fs" />
3
<Compile Include="Controllers/WeatherForecastController.fs" />
4
<Compile Include="Controllers/MessageController.fs" />
5
<Compile Include="Startup.fs" />
6
<Compile Include="Program.fs" />
7
</ItemGroup>

And then we can put together the controller in the MessageController.fs file:

MessageController.fs

1
namespace FSharpWebApi.Controllers
2
3
open Microsoft.AspNetCore.Mvc
4
open Microsoft.Extensions.Logging
5
6
[<ApiController>]
7
[<Route("[controller]")>]
8
type MessageController (logger : ILogger<MessageController>) =
9
inherit ControllerBase()
10
11
[<HttpGet("{id}")>]
12
member __.Get (id : int, shouldDouble : bool) : string=
13
logger.LogInformation("I am a controller")
14
15
let result =
16
match shouldDouble with
17
| true -> id * 2
18
| false -> id
19
20
sprintf "Hello %d" result

From the function’s signature we can see that it has an id and shouldDouble values as inputs and that it returns a string. We have made these explicit however if we were to leave them out it would be fine too as F# would be able to infer the types, see that below:

We can open the following URLs in our browser and should be able to open the /message/3 and /message/3?shouldDouble=true routes and see hello 3 and hello 6 respectively

Note that if not specified the handler inputs will try to be parsed from the body

Now, if we would want to update this to return some sort of general HTTP Response Code when a user sends some kind of input, for example if the result is 4, we will need to modify the function such that we are able to reference the this and the return type of the function is now an IActionResult

1
[<HttpGet("{id}")>]
2
member this.Get (id : int, shouldDouble : bool) : IActionResult =
3
logger.LogInformation("I am a controller")
4
5
let result =
6
match shouldDouble with
7
| true -> id * 2
8
| false -> id
9
10
match result with
11
| 4 -> this.NoContent() :> IActionResult
12
| _ ->
13
sprintf "Hello %d" result
14
|> this.Ok
15
:> IActionResult

From this we can see that we are using an additional match to either return this.NoContent() as an IActionResult or this.Ok with the piped message as an IActionResult. Just note that the following matches are equivalent:

1
// call the `this.Ok` function with
2
match result with
3
| 4 -> this.NoContent() :> IActionResult
4
| _ ->
5
this.Ok(sprintf "Hello %d" result) :> IActionResult
6
7
// pipe the result of the format through
8
match result with
9
| 4 -> this.NoContent() :> IActionResult
10
| _ ->
11
sprintf "Hello %d" result
12
|> this.Ok
13
:> IActionResult
14
15
// pipe the result of the format through on a single line
16
match result with
17
| 4 -> this.NoContent() :> IActionResult
18
| _ -> sprintf "Hello %d" result |> this.Ok :> IActionResult

Post Handler

We can also create a POST handler that will pretty much do the same as the above handler, we can pretty much just take the values from the function body and pass it to the previous handler we put together

Before we can create the handler, we need to create a type called PostData that can be used by the method to receive data, we can define this towards the top of the file, above the type definition for our MessageController. The type also needs to have the CLIMutable attribute so that the JSON deserializer can parse the data from the post body into it correctly

1
[<CLIMutable>]
2
type PostData =
3
{ id : int
4
shouldDouble : bool }

Next we simply need to define the Post method with an HttpPost attribute which will just call the this.Get using the input params. this can be done pretty simply too

1
[<HttpPost>]
2
member this.Post(data : PostData) : IActionResult =
3
this.Get(data.id, data.shouldDouble)

And that’s really all that’s needed

Conclusion

So yeah, that’s pretty much it - Not that bad right? I feel like there are a couple of things that feel a little bit weird because of the pieces of OOP running around from C# that add a bit more overhead than I’d like, but it’s .NET, that’s inevitable

Still a few more to posts on F# to come, so stay in tuned

Nabeel Valley