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 on GitHub
Getting Started
Assuming you've got the .NET Core SDK with F# installed, you can simply create a new project with the following:
dotnet new webapi --language F# --name FSharpWebApi
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:
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:
FSharpWebApi
│ appsettings.Development.json
│ appsettings.json
│ FSharpWebApi.fsproj
│ Program.fs
│ Startup.fs
│ WeatherForecast.fs
│
├───Controllers
│ WeatherForecastController.fs
│
└───Properties
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
namespace FSharpWebApi
module Program =
let exitCode = 0
let CreateHostBuilder args =
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(fun webBuilder ->
webBuilder.UseStartup<Startup>() |> ignore
)
[<EntryPoint>]
let main args =
CreateHostBuilder(args).Build().Run()
exitCode
Startup.fs
namespace FSharpWebApi
type Startup private () =
new (configuration: IConfiguration) as this =
Startup() then
this.Configuration <- configuration
// This method gets called by the runtime. Use this method to add services to the container.
member this.ConfigureServices(services: IServiceCollection) =
// Add framework services.
services.AddControllers() |> ignore
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
member this.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
if (env.IsDevelopment()) then
app.UseDeveloperExceptionPage() |> ignore
app.UseHttpsRedirection() |> ignore
app.UseRouting() |> ignore
app.UseAuthorization() |> ignore
app.UseEndpoints(fun endpoints ->
endpoints.MapControllers() |> ignore
) |> ignore
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
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="WeatherForecast.fs" />
<Compile Include="Controllers/WeatherForecastController.fs" />
<Compile Include="Startup.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
</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
namespace FSharpWebApi
open System
type WeatherForecast =
{ Date: DateTime
TemperatureC: int
Summary: string }
member this.TemperatureF =
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
namespace FSharpWebApi.Controllers
open System
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.Logging
open FSharpWebApi
[<ApiController>]
[<Route("[controller]")>]
type WeatherForecastController (logger : ILogger<WeatherForecastController>) =
inherit ControllerBase()
let summaries = [| "Freezing"; "Bracing"; "Chilly"; "Cool"; "Mild"; "Warm"; "Balmy"; "Hot"; "Sweltering"; "Scorching" |]
[<HttpGet>]
member __.Get() : WeatherForecast[] =
let rng = System.Random()
[|
for index in 0..4 ->
{ Date = DateTime.Now.AddDays(float index)
TemperatureC = rng.Next(-20,55)
Summary = summaries.[rng.Next(summaries.Length)] }
|]
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
<ItemGroup>
<Compile Include="WeatherForecast.fs" />
<Compile Include="Controllers/WeatherForecastController.fs" />
<Compile Include="Controllers/MessageController.fs" />
<Compile Include="Startup.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
And then we can put together the controller in the MessageController.fs
file:
MessageController.fs
namespace FSharpWebApi.Controllers
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.Logging
[<ApiController>]
[<Route("[controller]")>]
type MessageController (logger : ILogger<MessageController>) =
inherit ControllerBase()
[<HttpGet("{id}")>]
member __.Get (id : int, shouldDouble : bool) : string=
logger.LogInformation("I am a controller")
let result =
match shouldDouble with
| true -> id * 2
| false -> id
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
[<HttpGet("{id}")>]
member this.Get (id : int, shouldDouble : bool) : IActionResult =
logger.LogInformation("I am a controller")
let result =
match shouldDouble with
| true -> id * 2
| false -> id
match result with
| 4 -> this.NoContent() :> IActionResult
| _ ->
sprintf "Hello %d" result
|> this.Ok
:> 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:
// call the `this.Ok` function with
match result with
| 4 -> this.NoContent() :> IActionResult
| _ ->
this.Ok(sprintf "Hello %d" result) :> IActionResult
// pipe the result of the format through
match result with
| 4 -> this.NoContent() :> IActionResult
| _ ->
sprintf "Hello %d" result
|> this.Ok
:> IActionResult
// pipe the result of the format through on a single line
match result with
| 4 -> this.NoContent() :> IActionResult
| _ -> 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
[<CLIMutable>]
type PostData =
{ id : int
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
[<HttpPost>]
member this.Post(data : PostData) : IActionResult =
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
Top comments (0)