Professional Documents
Culture Documents
Fs Applied II
Fs Applied II
Introduction 1.1
2
3
Introduction
Hi,
The objective of this book is to answer the most significant question that developers
encounter while learning or working with F#.
Immutability, Type Safety, Pattern Matching, Pure Functions and all other functional
programming things sound good on paper but How can I build something useful by
applying it?
This book has been created exclusively to answer these questions in a developer-
friendly way!
Sounds interesting?
We are going to build a clone of the Twitter application, FsTweet. Right from starting
an empty directory for the application to deploying it on Azure App Service, we are
going to learn step by step. It was narrated in such a way that I will be doing pair
programming by sitting next to you and collaboratively building the application from
scratch.
4
Introduction
On the technical side, we are going to learn a lot of things and to quote a few; we’ll
be learning
Overall, It’s going to be a lot of fun, and I believe it will add value in your functional
programming journey in F#.
Acknowledgement
This entire book was initially planned to be released as a video course in FSharp.TV
after my Build a Web Server based on Suave course. Due to some personal
reasons, we couldn't complete it. Thank you, Mark, for the courteous support to
release the course material as a book.
I'd like to thank the entire F# Community for their open source contributions, support,
and thought-provoking blog posts, articles, and tutorials.
Improvements
We all need people who will give us feedback. That's how we improve - Bill
Gates.
Any suggestions or error reports from you are welcome. You can report them as
issues in the FsTweet's GitHub repository.
5
Introduction
Dedicated to
6
Chapter 1 - Setting Up FsTweet Project
In this first chapter, we will be kickstarting the project from a boilerplate F# project
and setting up the tools that are going to assist us in building the application.
DotNet Version
At the time of writing this book, Microsoft announced the release of .NET Core 2.0.
and its support in the F# ecosystem had some rough edges. Especially the type
providers, which we are going to use extensively in the later chapters, were not
adequately supported.
So, we are going to use the .NET Framework version 4.6.1 for our development.
Installing Forge
Forge is a command line tool that provides the command line interface for creating
and managing F# projects. In this book, we are going to make use of Forge to
develop our application, FsTweet.
We can also use IDEs like Visual Studio, Rider or VS Code with Ionide to
create and manage F# projects. In case if you choose to use an IDE that you
are already comfortable with instead of Forge, you are indeed welcome to do.
Under the assumption that you will be aware of how to translate the forge
commands into its equivalent IDE actions (if you are choosing an IDE over
forge), we won't be seeing anything specific to IDEs in this book.
As the project formats were changed between .NET core 2.0 and its early versions,
the latest version of Forge didn't support projects from earlier .NET version. So, we
are going to make use of the version 1.4.2 of Forge to manage our projects.
Let's get started by installing it on a windows machine. If you are using a non-
windows machine, you can skip the following section and jump to the next section.
On Windows
Download the Forge zip file from this releases link.
Unzip the "forge" directory to some local directory (for example c:\tools ).
7
Chapter 1 - Setting Up FsTweet Project
Add the bin directory path of forge( c:\tools\forge\bin ) to the Path environment
variable.
Now open the command prompt and execute the following command to verify
the installation.
> forge -h
If you already installed forge either uninstall it (if you are not using the latest
version) or rename the "Forge.exe" file in the bin directory to something else
and use the same name to execute the forge commands
8
Chapter 1 - Setting Up FsTweet Project
On Non-Windows
On Non-Windows the prerequisite is Mono, and I believe you will be already having it
on your machine.
The first step is downloading the Forge zip file from this releases link.
Unzip the "forge" directory to some local directory (for example ~/tools ).
Then add an alias in the .bashrc file like the below one
Now open the terminal and execute the following command to verify the
installation.
$ forge --help
Available parameters:
USAGE: forge.exe [--help] [new] [add] [move] [remove] [rename] [list]
[update-version] [paket] [fake] [refresh] [exit]
OPTIONS:
...
If you already installed forge either uninstall it (if you are not using the latest
version) or set the alias to something else and use the same alias to execute
the forge commands
9
Chapter 1 - Setting Up FsTweet Project
If you are using any other CLIs (like Powershell or DOS Prompt) some of the
commands may not available/work as intended.
If you are not a command line fan, you can safely replace the command line
instructions in the book with the corresponding manual UI action.
The above command creates a new directory FsTweet and clones the
FsTweetBoilerplate project there.
You can also download the boilerplate project from GitHub instead of cloning
and put it inside a directory with the name FsTweet .
10
Chapter 1 - Setting Up FsTweet Project
FsTweet.Web.fs
This file contains the entry point of the application.
module FsTweetWeb.Main
open Suave
open Suave.Successful
[<EntryPoint>]
let main argv =
startWebServer defaultConfig (OK "Hello World!")
0
It is a most straightforward Suave application with an HTTP server that greets all
visitors with the string "Hello World!"
11
Chapter 1 - Setting Up FsTweet Project
paket.dependencies
source https://www.nuget.org/api/v2
framework: net461
nuget FAKE
nuget FSharp.Core
nuget Suave
paket.lock
RESTRICTION: == net461
NUGET
remote: https://www.nuget.org/api/v2
FAKE (4.63)
FSharp.Core (4.2.2)
Suave (2.1.1)
FSharp.Core (>= 4.0.0.1)
One to clear the build directory, another one to build the application and the last one
for running the application.
#r "./packages/FAKE/tools/FakeLib.dll"
open Fake
// Targets
Target "Clean" (fun _ ->
CleanDirs [buildDir]
)
12
Chapter 1 - Setting Up FsTweet Project
// Build order
"Clean"
==> "Build"
==> "Run"
// start build
RunTargetOrDefault "Build"
In the Run target, we are running the FsTweet.Web console application using the
ExecProcess function from FAKE. It is only intended to run the application during our
development.
You can also find two more files, build.cmd & build.sh to execute the FAKE build
script in command prompt and bash shell respectively.
After successful packages restore, we can run the application by executing FAKE
build script's Run target.
This command will build the application and start the Suave standalone web server
on port 8080 . You can verify whether it is working or not by visiting the URL
http://127.0.0.1:8080/ in the browser.
13
Chapter 1 - Setting Up FsTweet Project
As we will be executing the forge fake Run command very often while developing the
application, to save some keystrokes and time, the boilerplate project has the
Forge.toml file with a Forge alias for this command
# Forge.toml
[alias]
run='fake Run'
With this alias in place, we can build and run our application using this simplified
command
From here on, the term run/test drive the application in the following chapters
implies running this forge run command.
Summary
In this chapter, we installed forge, then CLI and then we bootstrapped the project
from the boilerplate. In the upcoming chapters, we will be leveraging this.
14
Chapter 2 - Setting Up Server Side Rendering using DotLiquid
In this second chapter, we are going to extend our FsTweet app to render Hello,
This commands download the references of the specified NuGet packages and add
their references to the FsTweet.Web.fsproj file.
At the time of this writing, there are some incompatibility changes in the latest
versions of DotLiquid and Suave.DotLiquid. So we are sticking to their old
versions here.
Initializing DotLiquid
Now we have the required NuGet packages onboard
DotLiquid requires the following global initilization settings to enable us to render the
liquid templates.
Let's have a directory called views in the FsTweet.Web project to put the liquid
template files
15
Chapter 2 - Setting Up Server Side Rendering using DotLiquid
The add a new function called initDotLiquid , which invokes the required helper
functions to initialize DotLiquid to use this views directory for templates.
// FsTweet.Web.fs
// ...
open Suave.DotLiquid
open System.IO
open System.Reflection
let currentPath =
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
let initDotLiquid () =
let templatesDir = Path.Combine(currentPath, "views")
setTemplatesDir templatesDir
[<EntryPoint>]
let main argv =
initDotLiquid ()
setCSharpNamingConvention ()
// ...
By default, DotLiquid uses Ruby Naming Convention to refer the view model
properties in the liquid template. For example, if you are passing a record type
having a property UserId as a view model while using it in the liquid template, we
have to use user_id instead of UserId to access the value.
├── build
│ ├── ...
│ ├── FsTweet.Web.exe
│ └── views/
16
Chapter 2 - Setting Up Server Side Rendering using DotLiquid
1. Adding the liquid templates files in the views directory to FsTweet.Web.fsproj file
with the Build Action property as Content and Copy to Output property to either
Copy always or Copy if newer as mentioned in the project file properties
documentation.
2. The second option is leveraging our build script to copy the entire views
We are going to use the latter one as it is a one time work rather than fiddling with
the properties whenever we add a new liquid template file.
To do this let's add a new Target in the FAKE build script called Views and copy the
directory the FAKE's CopyDir function
Then modify the build order to invoke Views Target before Run
// Build order
"Clean"
==> "Build"
==> "Views"
==> "Run"
That's it!
Now it's time to add some liquid templates and see it in action
17
Chapter 2 - Setting Up Server Side Rendering using DotLiquid
Add a new file master_page.liquid in the views directory and update it as below
This master_page template defines three placeholders head , content and scripts
The next step is adding a child page liquid template guest/home.liquid with some title
and content
{% extends "master_page.liquid" %}
{% block head %}
<title> FsTweet - Powered by F# </title>
{% endblock %}
{% block content %}
<p>Hello, World!</p>
{% endblock %}
This guest home page template extends the master_page template and provides
values for the head and content placeholders.
18
Chapter 2 - Setting Up Server Side Rendering using DotLiquid
The Suave.DotLiquid package has a function called page which takes a relative file
path (from the templates root directory) and a view model and returns a WebPart
We just need to define the app using this page function. As the page is not using a
view model we can use an empty string for the second parameter.
Let's also add a path filter in Suave to render the page only if the path is a root ( / )
// FsTweet.Web.fs
// ...
open Suave.Operators
open Suave.Filters
// ...
[<EntryPoint>]
let main argv =
// ...
let app =
path "/" >=> page "guest/home.liquid" ""
startWebServer defaultConfig app
0
Now if you build and run the application using the forge run command, you can see
an HTML document with the Hello, World! content in the browser on
http://localhost:8080/
Summary
In this chapter, we have seen how to set up a Suave application to render server
side views using DotLiquid and also how to make use of FAKE build script to
manage static files.
19
Chapter 3 - Serving Static Asset Files
In this third capter, we will be changing the guest homepage from displaying Hello,
20
Chapter 3 - Serving Static Asset Files
└── FsTweet.Web
├── FsTweet.Web.fs
├── FsTweet.Web.fsproj
├── assets
│ ├── css
│ │ └── styles.css
│ └── images
│ ├── FsTweetLogo.png
│ └── favicon.ico
├── ...
For simplicity, I am leaving the other static content that is modified in the templates,
and you can find all the changes in this diff
21
Chapter 3 - Serving Static Asset Files
Then modify the build order to run this Target before the Run Target.
// Build order
"Clean"
==> "Build"
==> "Views"
==> "Assets"
==> "Run"
Suave has a lot of useful functions to handle files, and in our case, we are going to
make use of the browseHome function to serve the assets
'browse' the file in the sense that the contents of the file are sent based on the
request's Url property. Will serve from the current as configured in directory.
Suave's runtime. - Suave Documentation
The current directory in our case is the directory in which the FsTweet.Web.exe
exists. i.e build directory.
22
Chapter 3 - Serving Static Asset Files
// FsTweet.Web.fs
// ...
open Suave.Files
// ...
let serveAssets =
pathRegex "/assets/*" >=> browseHome
[<EntryPoint>]
let main argv =
// ...
let app =
choose [
serveAssets
path "/" >=> page "guest/home.liquid" ""
]
The serveAssets defines a new WebPart using the pathRegex. It matches all the
requests for the assets and serves the corresponding files using the browseHome
function.
As we are handling more than one requests now, we need to change our app
to handle all of them. Using the choose function, we are defining the app to
combine both serveAssets WebPart and the one that we already had for serving
the guest home page.
Serving favicon.ico
While serving our FsTweet application, the browser automatically makes a request
for favicon. As the URL path for this request is /favicon.ico our serveAssets
To serve it we need to use an another specific path filter and use the file function to
get the job done.
23
Chapter 3 - Serving Static Asset Files
// FsTweet.Web.fs
// ...
let serveAssets =
let faviconPath =
Path.Combine(currentPath, "assets", "images", "favicon.ico")
choose [
pathRegex "/assets/*" >=> browseHome
path "/favicon.ico" >=> file faviconPath
]
//...
Summary
In this chapter, we learned how to serve static asset files in Suave. The source code
can be found in the GitHub repository
24
Chapter 4 - Handling User signup Form
Hi,
In the last chapter, we added a cool landing page for FsTweet to increase the user
signups. But the signup form and its backend are not ready yet!
In this chapter, we will be extending FsTweet to serve the signup page and
implement its backend scaffolding
You can ignore the \ character from the above command if you are typing
everything in a single line
The next step is moving this file above FsTweet.Web.fs file as we will be referring
UserSignup in the Main function.
Though working with the command line is productive than its visual counterpart, the
commands that we typed for creating and moving a file is verbose.
Forge has an advanced feature called alias using which we can get rid of the
boilerplate to a large extent.
As we did for the forge Run alias during the project setup, let's add few three more
alias
# ...
web='-p src/FsTweet.Web/FsTweet.Web.fsproj'
newFs='new file -t fs'
moveUp='move file -u'
25
Chapter 4 - Handling User signup Form
The web is an alias for the project argument in the Forge commands. The newFs
and moveUp alias are for the new file and move file operations respectively.
If we had this alias beforehand, we could have used the following commands to do
what we just did
If you feel some of the things like, adding a new file, moving the file up/down,
etc., are better using your favourite IDE/editor than forge, you can ignore
these steps and use their equivalents in the IDE.
As we will be capturing the user details during signup, we need to use an view model
while using the dotliquid template for the signup page.
In the UserSignup.fs, delete all the initial content, then define a namespace
UserSignup and a module Suave with a webPart function.
// FsTweet.Web/UserSignup.fs
namespace UserSignup
26
Chapter 4 - Handling User signup Form
module Suave =
open Suave.Filters
open Suave.Operators
open Suave.DotLiquid
//
let webPart () =
path "/signup"
>=> page "user/signup.liquid" ???
The namespace represents the use case or the feature that we are about to
implement. The modules inside the namespace represent the different layers of the
use case implementation.
The Suave module defines the Web layer of the User Signup feature. You can learn
about organizing modules from this blog post.
The ??? symbol is a placeholder that we need to fill in with a view model.
The view model has to capture user's email address, password, and username.
// FsTweet.Web/UserSignup.fs
module Suave =
type UserSignupViewModel = {
Username : string
Email : string
Password: string
Error : string option
}
let emptyUserSignupViewModel = {
Username = ""
Email = ""
Password = ""
Error = None
}
let webPart () =
path "/signup"
>=> page "user/signup.liquid" emptyUserSignupViewModel
As the name indicates, emptyUserSignupViewModel provide the default values for the
view model.
27
Chapter 4 - Handling User signup Form
The next step is creating a dotliquid template for the signup page.
{% block head %}
<title> Sign Up - FsTweet </title>
{% endblock %}
{% block content %}
<form method="POST" action="/signup">
{% if model.Error %}
<p>{{ model.Error.Value }}</p>
{% endif %}
<input type="email" name="Email" value={{ model.Email }}>
<input type="text" name="Username" value={{ model.Username }}>
<input type="password" name="Password">
<button type="submit">Sign up</button>
</form>
{% endblock %}
For brevity, the styles and some HTML tags are ignored.
In the template, the value of the name attribute should match its corresponding view
model property's name to do the model binding on the server side.
And another thing to notice here is the if condition that displays the error only if it
is available.
The last step in serving the user signup page is adding this new webpart in the
application.
To do this, we just need to call the webPart function while defining the app in the
main function.
// FsTweet.Web/FsTweet.Web.fs
// ...
let main argv =
// ...
let app =
choose [
28
Chapter 4 - Handling User signup Form
// ...
UserSignup.Suave.webPart ()
]
// ...
That's it!
29
Chapter 4 - Handling User signup Form
For the GET request on the /signup path, we are serving the signup page. And for
the POST request, we need a function to handle the POST request.
// FsTweet.Web/UserSignup.fs
module Suave =
// ...
open Suave
// ...
let webPart () =
path "/signup"
>=> choose [
GET >=> page "user/signup.liquid" emptyUserSignupViewModel
POST >=> ???
]
The function that we are going to write to fill the placeholder ??? has to satisfy two
criteria.
1. It has to return a WebPart so that it can be composed using >=> operator (or
infix function).
2. The other requirement is its interaction with the database should be
asynchronous (non-blocking) otherwise it'd block the Suave Web Server.
With this knowledge, Let's name the function that is going to handle the user signup
post request as handleUserSignup .
30
Chapter 4 - Handling User signup Form
This is a naive implementation of the handleUserSignup which just prints the whatever
value there in the request's form type in the console and return the HttpContext as it
is.
The second criteria for asynchronous are already satisfied as the handleUserSignup
To get a feel for how we will be interacting with the database in this function, let's
redirect the user to the signup page again instead of returning the HttpContext as it
is.
We can do page redirection using the FOUND function in the Redirection module of
Suave.
The FOUND function takes a path (of type string ) to redirect the browser to and
returns a WebPart
Now we can say that this function takes a string and an HttpContext and
asynchronously returns HttpContext option .
31
Chapter 4 - Handling User signup Form
// HttpContext option
let! redirectionResponse =
Redirection.FOUND "/signup" ctx
return redirectionResponse
}
The async computation expression takes care of waiting and returning the value
from an asynchronous operation without blocking the main thread.
The usage of let! and followed by return can be simplified using a syntactic sugar
return! which does the both
32
Chapter 4 - Handling User signup Form
// FsTweet.Web/UserSignup.fs
module Suave =
// ...
let handleUserSignup ctx = async {
printfn "%A" ctx.request.form
return! Redirection.FOUND "/signup" ctx
}
let webPart () =
path "/signup"
>=> choose [
// ...
POST >=> handleUserSignup
]
When we rerun the program with this new changes, we can find the values being
posted in the console upon submitting the signup form.
In other words, we need to bind the request form data to the UserSignupViewModel .
There is an inbuilt support for doing this Suave using Suave.Experimental package.
33
Chapter 4 - Handling User signup Form
Let's add this to our FsTweet.Web project using paket and forge.
After we add the reference, we can make use of the bindEmptyForm function to carry
out the model binding for us.
The bindEmptyForm function takes a request and returns either the value of the given
type or an error message.
// ...
module Suave =
// ...
open Suave.Form
// ...
As the bindEmptyForm function returns a generic type as its first option, we need to
specify the type to enable the model binding explicitly.
If the model binding succeeds, we just print the view model and redirects the user to
the signup page as we did in the previous section.
If it fails, we modify the viewModel with the error being returned and render the
signup page again.
When we rerun the program and do the form post again, we will get the following
output.
34
Chapter 4 - Handling User signup Form
{Username = "demystifyfp";
Email = "demystifyfp@gmail.com";
Password = "secret";
Error = None;}
Summary
In this chapter, We started with rendering the signup form, and then we learned how
to do view model binding using the Suave.Experimental library.
35
Chapter 5 - Validating New User Signup Form
In the last chapter, we created the server side representation of the user submitted
details. The next step is validating this view model against a set of constraints before
persisting them in a data store.
Let's assume that we have a business requirement stating the username should not
be empty, and it can't have more than 12 characters. An ideal way to represent this
requirement in our code is to type called Username and when we say a value is of
type Username it is guaranteed that all the specified requirements for Username has
been checked and it is a valid one.
Email should have a valid email address, and Password has to meet the
application's password policy.
Let's assume that we have a function tryCreate that takes UserSignupViewModel as its
input, performs the validations based on the requirements and returns either a
domain model UserSignupRequest or a validation error of type string .
36
Chapter 5 - Validating New User Signup Form
The subsequent domain actions will take UserSignupRequest as its input without
bothering about the validness of the input!
If we zoom into the tryCreate function, it will have three tryCreate function being
called sequentially. Each of these functions takes care of validating the individual
properties and transforming them into their corresponding domain type.
37
Chapter 5 - Validating New User Signup Form
In some cases, we may need to capture all the errors instead of short
circuiting and returning the first error that we encountered.
Let's get started with the validation by adding the Chessie package.
38
Chapter 5 - Validating New User Signup Form
namespace UserSignup
module Domain =
// TODO
Then define a single case discriminated union with a private constructor for the
domain type Username
module Domain =
type Username = private Username of string
The private constructor ensures that we can create a value of type Username only
inside the Domain module.
module Domain =
open Chessie.ErrorHandling
As we saw in the previous function, the TryCreate function has the following function
signature
The Result , a type from the Chessie library, represents the result of our validation.
It will have either the Username (if the input is valid) or a string list (for invalid
input)
39
Chapter 5 - Validating New User Signup Form
The presence string list instead of just string is to support an use case
where we are interested in capturing all the errors. As we are going to capture
only the first error, we can treat this as a list with only one string .
The ok and fail are helper functions from Chessie to wrap our custom values
with the Success and Failure part of the Result type respectively.
As we will need the string representation of the Username to persist it in the data
store, let's add a property Value which returns the underlying actual string value.
module Domain =
// ...
type Username = private Username of string with
// ...
member this.Value =
let (Username username) = this
username
Let's do the same thing with the other two input that we are capturing during the user
signup
module Domain =
// ...
type EmailAddress = private EmailAddress of string with
member this.Value =
let (EmailAddress emailAddress) = this
emailAddress
static member TryCreate (emailAddress : string) =
try
new System.Net.Mail.MailAddress(emailAddress) |> ignore
EmailAddress emailAddress |> ok
with
| _ -> fail "Invalid Email Address"
40
Chapter 5 - Validating New User Signup Form
module Domain =
// ...
type Password = private Password of string with
member this.Value =
let (Password password) = this
password
static member TryCreate (password : string) =
match password with
| null | "" -> fail "Password should not be empty"
| x when x.Length < 4 || x.Length > 8 ->
fail "Password should contain only 4-8 characters"
| x -> Password x |> ok
Now we have all individual validation and transformation in place. The next step is
composing them together and create a new type UserSignupRequest that represents
the valid domain model version of the UserSignupViewModel
module Domain =
// ...
type UserSignupRequest = {
Username : Username
Password : Password
EmailAddress : EmailAddress
}
With the help of trial, a computation expression(CE) builder from Chessie and the
TryCreate functions that we created earlier we can achieve it with ease.
41
Chapter 5 - Validating New User Signup Form
module Domain =
// ...
type UserSignupRequest = {
// ...
}
with static member TryCreate (username, password, email) =
trial {
let! username = Username.TryCreate username
let! password = Password.TryCreate password
let! emailAddress = EmailAddress.TryCreate email
return {
Username = username
Password = password
EmailAddress = emailAddress
}
}
The TryCreate function in the UserSignupRequest takes a tuple with three elements
and returns a Result<UserSignupRequest, string list>
We might require some of the types that we have defined in the Domain
It takes three parameters, two functions fSuccess and fFailure and a Result type.
42
Chapter 5 - Validating New User Signup Form
It maps the Result type with fSuccess if it is a Success otherwise it maps it with
fFailure .
module Suave =
// ...
open Domain
open Chessie.ErrorHandling
// ...
let handleUserSignup ctx = async {
match bindEmptyForm ctx.request with
| Choice1Of2 (vm : UserSignupViewModel) ->
let result =
UserSignupRequest.TryCreate (vm.Username, vm.Password, vm.Email)
let onSuccess (userSignupRequest, _) =
printfn "%A" userSignupRequest
Redirection.FOUND "/signup" ctx
let onFailure msgs =
let viewModel = {vm with Error = Some (List.head msgs)}
page "user/signup.liquid" viewModel ctx
return! either onSuccess onFailure result
// ...
}
// ...
During failure, we populate the Error property of the view model with the first item in
the error messages list and re-render the signup page again.
As we are referring the liquid template path of the signup page in three places now,
let's create a label for this value and use the label in all the places.
43
Chapter 5 - Validating New User Signup Form
module Suave =
// ..
let signupTemplatePath = "user/signup.liquid"
let webPart () =
path "/signup"
>=> choose [
GET >=> page signupTemplatePath emptyUserSignupViewModel
// ...
]
Now if we build and run the application, we will be getting following console output
for valid signup details.
Summary
In this chapter, we learned how to do validation and transform view model to a
domain model using the Railway Programming technique with the help of the
Chessie library.
44
Chapter 5 - Validating New User Signup Form
45
Chapter 6 - Setting Up Database Migration
In the last chapter, we validated the signup details submitted by the user and
transformed it into a domain model.
In this sixth chapter, we are going to learn how to setup PostgreSQL database
migrations in fsharp using Fluent Migrator.
As we are not going to use EF in favor of SQLProvider, we are picking the fluent
migrator to help us in managing the database schema.
This command creates this new project with .NET Framework 4.6.2 as Target. As we
are using a lower version, downgrade it to 4.6.1 by manually editing the
FsTweet.Db.Migrations.fsproj file.
- <TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
+ <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
The next step is adding the FluentMigrator NuGet package and referring it in the
newly created FsTweet.Db.Migrations project.
46
Chapter 6 - Setting Up Database Migration
This class also has to have an attribute Migration to specify the order of the
migration and also it should override the Up and Down methods.
Fsharp provides nicer support to write OO code. So writing the migration is straight
forward and we don't need to go back to C#!
As a first step, clean up the default code in the FsTweet.Db.Migrations.fs file and
update it as below.
namespace FsTweet.Db.Migrations
open FluentMigrator
override this.Up() = ()
override this.Down() = ()
The next step is using the fluent methods offered by the fluent migrator we need to
define the Users table and its columns.
// ...
type CreateUserTable()=
// ...
override this.Up() =
base.Create.Table("Users")
.WithColumn("Id").AsInt32().PrimaryKey().Identity()
.WithColumn("Username").AsString(12).Unique().NotNullable()
.WithColumn("Email").AsString(254).Unique().NotNullable()
.WithColumn("PasswordHash").AsString().NotNullable()
.WithColumn("EmailVerificationCode").AsString().NotNullable()
.WithColumn("IsEmailVerified").AsBoolean()
|> ignore
// ...
47
Chapter 6 - Setting Up Database Migration
The last step is overriding the Down method. In the Down method, we just need to
delete the Users table.
type CreateUserTable()=
// ...
override this.Down() =
base.Delete.Table("Users") |> ignore
// ...
Let's add a new FAKE Target BuildMigrations in the build script to build the
migrations.
// build.fsx
// ...
Target "BuildMigrations" (fun _ ->
!! "src/FsTweet.Db.Migrations/*.fsproj"
|> MSBuildDebug buildDir "Build"
|> Log "MigrationBuild-Output: "
)
// ...
Then we need to change the existing Build target to build only the FsTweet.Web
// build.fsx
// ...
Target "Build" (fun _ ->
!! "src/FsTweet.Web/*.fsproj"
|> MSBuildDebug buildDir "Build"
|> Log "AppBuild-Output: "
)
// ...
48
Chapter 6 - Setting Up Database Migration
To run the migration against Postgres, we need to install the Npgsql package from
NuGet.
At the time of this writing there is an issue with the latest version of Npgsql.
So, we are using the version 3.1.10 here.
FAKE has inbuilt support for running fluent migration from the build script.
To do it add the references of the FluentMigrator and Npgsql DLLs in the build
script.
// build.fsx
// ...
#r "./packages/FAKE/tools/Fake.FluentMigrator.dll"
#r "./packages/Npgsql/lib/net451/Npgsql.dll"
// ...
open Fake.FluentMigratorHelper
// ...
// build.fsx
// ...
let connString =
@"Server=127.0.0.1;Port=5432;Database=FsTweet;User Id=postgres;Password=test;"
let dbConnection = ConnectionString (connString, DatabaseProvider.PostgreSQL)
let migrationsAssembly =
combinePaths buildDir "FsTweet.Db.Migrations.dll"
49
Chapter 6 - Setting Up Database Migration
This migration script doesn't create the database. It assumes that you are having a
PostgreSQL server with a database name FsTweet that can be connected using the
given connection string. You may need to change the connection string according to
your PostgreSQL server instance.
The last step in running the migration script is adding it to the build script build order.
We need to run the migrations before the Build target, as we need to have the
database schema in place to use SQLProvider to interact with the PostgreSQL.
// build.fsx
// ...
"Clean"
==> "BuildMigrations"
==> "RunMigrations"
==> "Build"
// ...
This command is an inbuilt alias in forge representing the forge fake Build
command.
While the build script is running, we can see the console log of the RunMigrations
...
Starting Target: RunMigrations (==> BuildMigrations)
...
----------------------------------------------------
201709250622: CreateUserTable migrating
----------------------------------------------------
[+] Beginning Transaction
[+] CreateTable Users
[+] Committing Transaction
[+] 201709250622: CreateUserTable migrated
[+] Task completed.
Finished Target: RunMigrations
...
50
Chapter 6 - Setting Up Database Migration
Upon successful execution of the build script, we can verify the schema using psql
FsTweet=# \d "Users"
Table "public.Users"
FAKE has function environVarOrDefault , which takes the value from the given
environment name and if the environment variable is not available, it returns the
provided default value.
// build.fsx
// ...
let connString =
environVarOrDefault
"FSTWEET_DB_CONN_STRING"
@"Server=127.0.0.1;Port=5432;Database=FsTweet;User Id=postgres;Password=test;"
// ...
51
Chapter 6 - Setting Up Database Migration
That's it!
Summary
In this chapter, we learned how to set up database migration using Fluent Migrator in
fsharp and leverage FAKE to run the migrations while running the build script.
52
Chapter 7 - Orchestrating User Signup
Before diving into the implementation of the user signup use case, let's spend some
time to jot down its requirements.
1. If the user submitted invalid details, we should let him/her know about the error
(which we already implemented in the fifth chapter part)
2. We also need to check the whether the username or the email provided by the
user has been already used by someone else and report it if we found it is not
available.
3. If all the details are well, then we need to persist the user details with his
password hashed and also a randomly generated verification code.
4. Then we need to send an email to the provided email address with the
verification code.
5. Upon receiving an URL with the verification code, the user will be navigating to
the provided URL to complete his signup process.
In this chapter, we are going to implement the service layer part of the user signup
which coordinates the steps two, three and four.
To generate the hash, we are going to use the Bcrypt algorithm. In .NET we can use
the Bcrypt.Net library to create the password hash using the Bcrypt algorithm.
53
Chapter 7 - Orchestrating User Signup
// UserSignup.fs
module Domain =
// ...
open BCrypt.Net
// ...
type PasswordHash = private PasswordHash of string with
member this.Value =
let (PasswordHash passwordHash) = this
passwordHash
As we did for the other Domain types, PasswordHash has a private constructor
function to prevent it from creating from outside.
The static function Create takes care of creating the password hash from the
provided password using the Bcrypt library.
We are placing all the Domain types in UserSignup namespace now. Some of
the types that we declared here may be needed for the other use cases. We
will be doing the module reorganization when we require it.
// UserSignup.fs
module Domain =
// ...
open System.Security.Cryptography
// ...
let base64URLEncoding bytes =
let base64String =
System.Convert.ToBase64String bytes
base64String.TrimEnd([|'='|])
.Replace('+', '-').Replace('/', '_')
54
Chapter 7 - Orchestrating User Signup
member this.Value =
let (VerificationCode verificationCode) = this
verificationCode
base64URLEncoding b
|> VerificationCode
In our case, trimming the white-space characters and converting to the string to
lower case should suffice.
To do it, we can use the existing TryCreate function in the Username and
EmailAddress type.
// ...
55
Chapter 7 - Orchestrating User Signup
As a first step towards persisting new user details, let's define a type signature for
the Create User function that we will be implementing in an upcoming chapter.
// UserSignup.fs
module Domain =
// ...
type CreateUserRequest = {
Username : Username
PasswordHash : PasswordHash
Email : EmailAddress
VerificationCode : VerificationCode
}
// ...
Then we need to have a type for the response. We will be returning the primary key
that has been generated from the PostgreSQL database.
// UserSignup.fs
module Domain =
// ...
type UserId = UserId of int
// ...
56
Chapter 7 - Orchestrating User Signup
// UserSignup.fs
module Domain =
// ...
type CreateUserError =
| EmailAlreadyExists
| UsernameAlreadyExists
| Error of System.Exception
// ...
With the help of the types that we declared so far, we can now declare the type for
the create user function
type CreateUser =
CreateUserRequest -> AsyncResult<UserId, CreateUserError>
The AsyncResult type is from the Chessie library. It represents the Result of an
asynchronous computation.
The inputs for this function are Username , EmailAddress , and the VerificationCode .
// UserSignup.fs
module Domain =
// ...
type SignupEmailRequest = {
Username : Username
EmailAddress : EmailAddress
VerificationCode : VerificationCode
}
// ...
As sending an email may fail, we need to have a type for representing it as well
57
Chapter 7 - Orchestrating User Signup
module Domain =
// ...
type SendEmailError = SendEmailError of System.Exception
// ...
With the help of these two types, we can declare the SendSignupEmail type as
module Domain =
// ...
type SendSignupEmail =
SignupEmailRequest -> AsyncResult<unit, SendEmailError>
// ...
In addition to these two functions, the SignupUser function takes a record of type
UserSignupRequest as its input but what about the output?
type SignupUser =
CreateUser -> SendSignupEmail -> UserSignupRequest
-> ???
58
Chapter 7 - Orchestrating User Signup
module Domain =
// ...
type UserSignupError =
| CreateUserError of CreateUserError
| SendEmailError of SendEmailError
// ...
type SignupUser =
CreateUser -> SendSignupEmail -> UserSignupRequest
-> AsyncResult<UserId, UserSignupError>
We are not going to use this SignupUser type anywhere else, and it is just for
illustration. In the later chapters, we'll see how to make use of this kind of type
aliases.
module Domain =
// ...
let signupUser (createUser : CreateUser)
(sendEmail : SendSignupEmail)
(req : UserSignupRequest) = asyncTrial {
// TODO
}
Like the trial computation that we used to do the user signup form validation, the
asyncTrail computation expression is going to help us here to do the error handling
in asynchronous operations.
59
Chapter 7 - Orchestrating User Signup
The ... notation is just a convention that we are using here to avoid
repeating the parameters, and it is not part of the fsharp language syntax
The next step is calling the createUser function with the createUserReq
Steps involved are creating a value of type SignupEmailRequest and calling the
sendEmail function with this value.
As the sendEmail function returning unit on success, we can use the do! notation
instead of let!
60
Chapter 7 - Orchestrating User Signup
Would you be able to find why are we getting this compiler error?
To figure out the solution, let's go back to the TryCreate function in UserSignupRequest
type.
// ...
with static member TryCreate (username, password, email) =
trial {
let! username = Username.TryCreate username
let! password = Password.TryCreate password
let! emailAddress = EmailAddress.TryCreate email
return {
Username = username
Password = password
EmailAddress = emailAddress
}
}
Let's focus our attention to the type that represents the result of a failed computation
61
Chapter 7 - Orchestrating User Signup
Coming back to the signupUser function what we are implementing, here is a type
signature of the functions
In this case, the types that are representing the failure are of different type. That's
thing that we need to fix!
If we transform (or map) the failure type to UserSignupError , then things would be
fine!
62
Chapter 7 - Orchestrating User Signup
types to UserSignupError
type UserSignupError =
| CreateUserError of CreateUserError
| SendEmailError of SendEmailError
These union case identifiers are functions which have the following signature
63
Chapter 7 - Orchestrating User Signup
But we can't use it directly, as the CreateUserError and the SendEmailError are part of
the AsyncResult type!
AsyncResult<UserId, CreateUserError>
to
AsyncResult<UserId, UserSignupError>
and
AsyncResult<unit, SendEmailError>
to
AsyncResult<unit, UserSignupError>
// UserSignup.fs
module Domain =
// ...
let mapAsyncFailure f aResult =
// TODO
The mapAsyncFailure function is a generic function with the following type signature.
64
Chapter 7 - Orchestrating User Signup
AsyncResult<'c, 'a>
to
Async<Result<'c, 'a>>
The Chessie library already has a function, ofAsyncResult , to carry out this
transformation (or unboxing!)
The next step is mapping the value that is part of the Async type.
We can again make use of the Chessie library again by using its map function. This
map function like other map functions in the fsharp standard module takes a function
as its input to do the mapping.
The easier way to understand is to think Async as a box. All mapping function does
is takes the value out of the Async box, perform the mapping using the provided
function, then put it to back to a new Async box and return it.
65
Chapter 7 - Orchestrating User Signup
But what is the function that we need to give to the map function to do the mapping
We can't give the CreateUserError union case function directly as the f parameter
here; it only maps CreateUserError to UserSignupError .
The reason is, the value inside the Async is not CreateUserError , it's Result<UserId,
CreateUserError> .
We need to have an another map function which maps the failure part of the Result
type
66
Chapter 7 - Orchestrating User Signup
Let's assume that we have mapFailure function which takes a function f to do this
failure type mapping on the Result type.
We can continue with the definition of the mapAsyncFailure function using this
mapFailure function.
The final step is putting the Async of Result type back to AsyncResult type. As
AsyncResult is defined as single case discriminated union, we can use the AR union
case to complete the mapping.
The mapFailure is not part of the codebase yet. So, Let's add it before going back to
the signupUser function.
The Chessie library already has a mapFailure function. But the mapping function
parameter maps a list of errors instead of a single error
67
Chapter 7 - Orchestrating User Signup
'a list -> 'b list -> Result<'c, 'a list> -> Result<'c, 'b list>
The reason for this signature is because the library treats failures as a list in the
Result type.
As we are treating the failure in the Result type as a single item, we can't directly
make use of it.
// UserSignup.fs
module Domain =
// ...
let mapFailure f aResult =
let mapFirstItem xs =
List.head xs |> f |> List.singleton
mapFailure mapFirstItem aResult
// ...
With this, we are done with the mapping of AsyncResult failure type.
68
Chapter 7 - Orchestrating User Signup
module Domain =
// ...
let signupUser ... = asyncTrial {
return userId
}
That's it!!
Summary
One of the key take away of this chapter is how we can solve a complex problem in
fsharp by just focusing on the function signature.
We also learned how to compose functions together, transforming the values using
the map function to come up with a robust implementation.
69
Chapter 8 - Transforming Async Result to Webpart
Hi there!
In the last chapter, using the Chessie library, we orchestrated the user signup
process.
There are three more things that we need to do wrap up the user signup workflow.
3. Adding the presentation layer to inform the user about his/her progress in the
signup process.
In this chapter, we are going to pick the third item. We will be faking the
implementation of user creation and sending an email.
We can apply the same thing while creating a presentation layer for a domain
construct.
UserSignupViewModel ->
AsyncResult<UserId, UserSignupError> -> Async<WebPart>
The UserSignupViewModel is required communicate the error details with the user
along with the information that he/she submitted.
70
Chapter 8 - Transforming Async Result to Webpart
// UserSignup.fs
...
module Suave =
// ...
let handleUserSignupAsyncResult viewModel aResult =
// TODO
We are using the prefix handle instead of map here as we are going to do a
side effect (printing in console in case of error) in addition to the transforming
the type.
AsyncResult<UserId, UserSignupError>
to
Async<Result<UserId, UserSignupError>>
As we seen in the previous post, we can make use of the ofAsyncResult function
from Chessie, to do it
Async<Result<UserId, UserSignupError>>
to
Async<WebPart>
71
Chapter 8 - Transforming Async Result to Webpart
As we did for mapping Async Failure in the previous post, We make use of the map
type to WebPart
Now we have a scaffolding for transforming the domain type to the presentation type.
Transforming UserSignupResult to
WebPart
In this section, we are going to define the handleUserSignupResult function that we left
as a placeholder in the previous section.
We are going to define it by having separate functions for handling success and
failures and then use them in the actual definition of handleUserSignupResult
If the result is a success, we need to redirect the user to a signup success page.
// UserSignup.fs
...
module Suave =
// ...
let handleUserSignupSuccess (viewModel : UserSignupViewModel) _ =
sprintf "/signup/success/%s" viewModel.Username
|> Redirection.FOUND
// let handleUserSignupAsyncResult viewModel aResult = ...
72
Chapter 8 - Transforming Async Result to Webpart
We are leaving the second parameter as _ , as we are not interested in the result of
the successful user signup ( UserId ) here.
module Suave =
// ...
let handleCreateUserError viewModel = function
| EmailAlreadyExists ->
let viewModel =
{viewModel with Error = Some ("email already exists")}
page signupTemplatePath viewModel
| UsernameAlreadyExists ->
let viewModel =
{viewModel with Error = Some ("username already exists")}
page signupTemplatePath viewModel
| Error ex ->
printfn "Server Error : %A" ex
let viewModel =
{viewModel with Error = Some ("something went wrong")}
page signupTemplatePath viewModel
// ...
We are updating the Error property with the appropriate error messages and re-
render the signup page in case of unique constraint violation errors.
For exceptions, which we modeled as Error here, we re-render the signup page
with an error message as something went wrong and printed the actual error in the
console.
We need to do the similar thing for handling error while sending emails.
73
Chapter 8 - Transforming Async Result to Webpart
module Suave =
// ...
let handleSendEmailError viewModel err =
printfn "error while sending email : %A" err
let msg =
"Something went wrong. Try after some time"
let viewModel =
{viewModel with Error = Some msg}
page signupTemplatePath viewModel
// ...
module Suave =
// ...
let handleUserSignupError viewModel errs =
match List.head errs with
| CreateUserError cuErr ->
handleCreateUserError viewModel cuErr
| SendEmailError err ->
handleSendEmailError viewModel err
// ...
The errs parameter is a list of UserSignupError as the Result type models failures
as lists.
Now we have functions to transform both the Sucess and the Failure part of the
UserSignupResult .
With the help of these functions, we can define the handleUserSignupResult using the
either function from Chessie
74
Chapter 8 - Transforming Async Result to Webpart
// UserSignup.fs
...
module Suave =
// ...
let handleUserSignupResult viewModel result =
either
(handleUserSignupSuccess viewModel)
(handleUserSignupError viewModel) result
// ...
Wiring Up WebPart
In the previous section, we defined functions to transform the result of a domain
functionality to its corresponding presentation component.
The next work is wiring up this presentation component with the function which
handles the user signup POST request.
As a recap, here is a skeleton of the request handler function that we already defined
in the fifth chapter.
75
Chapter 8 - Transforming Async Result to Webpart
As a first step towards wiring up the user signup result, we need to use the pattern
matching on the validation result instead of using the either function.
The reason for this split is we will be doing an asynchronous operation if the request
is valid. For the invalid request, there is no asynchronous operation involved.
The next step is changing the signature of the handleUserSignup to take signupUser
type SignupUser =
CreateUser -> SendSignupEmail ->
UserSignupRequest -> AsyncResult<UserId, UserSignupError>
Then in the pattern matching part of the valid request, replace the placeholders
(printing and redirecting) with the actual functionality
76
Chapter 8 - Transforming Async Result to Webpart
For valid signup request, we call the signupUser function and then pass the return
value of this function to the handleUserSignupAsyncResult function which returns an
Async<WebPart>
Through let! binding we retrieve the WebPart from Async<WebPart> and then using it
to send the response back to the user.
But that doesn't mean we need to wait until the end to see the final output in the
browser.
These two types are just functions! So, We can provide a fake implementation of
them and exercise the functionality that we wrote!
Let's add two more modules above the Suave module with these fake
implementations.
77
Chapter 8 - Transforming Async Result to Webpart
// UserSignup.fs
// ...
module Persistence =
open Domain
open Chessie.ErrorHandling
module Email =
open Domain
open Chessie.ErrorHandling
The next step is using the fake implementation to complete the functionality
// ...
module Suave =
// ...
let webPart () =
let createUser = Persistence.createUser
let sendSignupEmail = Email.sendSignupEmail
let signupUser =
Domain.signupUser createUser sendSignupEmail
path "/signup"
>=> choose [
// ...
POST >=> handleUserSignup signupUser
]
78
Chapter 8 - Transforming Async Result to Webpart
Composition Root
If we try to signup with a valid user signup request, we will get the following output in
the console
79
Chapter 8 - Transforming Async Result to Webpart
{% block head %}
<title> Signup Success </title>
{% endblock %}
{% block content %}
<div class="container">
<p class="well">
Hi {{ model }}, Your account has been created.
Check your email to activate the account.
</p>
</div>
{% endblock %}
This liquid template makes use of view model of type string to display the user name
The next step is adding a route for rendering this template with the actual user name
in the webpart function.
As we are now exposing more than one paths in user signup (one for the request
and another for the successful signup), we need to use the choose function to define
a list of WebPart s.
// UserSignup.fs
// ...
module Suave =
let webPart () =
// ...
choose [
path "/signup"
// ...
pathScan
"/signup/success/%s"
(page "user/signup_success.liquid")
]
80
Chapter 8 - Transforming Async Result to Webpart
The pathScan from Suave enable us to do strongly typed pattern matching on the
route. It takes a string (route) with PrintfFormat string and a function with parameters
matching the values in the route.
Here the user name is being matched on the route. Then we partially apply page
function with one parameter representing the path of the liquid template.
Now if we run the application, we will get the following page upon receiving a valid
user signup request.
That's it :)
Summary
In this chapter, we learned how to transform the result representation of a domain
functionality to its corresponding view layer representation.
The separation of concerns enables us to add a new Web RPC API or even
replacing Suave with any other library/framework without touching the existing
functionality.
81
Chapter 9 - Persisting New User
We are on track to complete the user signup feature. In this chapter, we are going to
implement the persistence layer for creating a user which we faked in the last
chapter.
Initializing SQLProvider
We are going to use SQLProvider, a SQL database type provider, to takes care of
PostgreSQL interactions,
As usual, let's add its NuGet package to our Web project using paket
To do it, let's add a separate fsharp file Db.fs in the Web Project
We are making use of the Forge alias that we set in the fourth chapter
The next step is initializing the SQLProvider with all the required parameters
82
Chapter 9 - Persisting New User
// src/FsTweet.Web/Db.fs
module Database
open FSharp.Data.Sql
[<Literal>]
let private connString =
"Server=127.0.0.1;Port=5432;Database=FsTweet;" +
"User Id=postgres;Password=test;"
[<Literal>]
let private npgsqlLibPath =
@"./../../packages/Npgsql/lib/net451"
[<Literal>]
let private dbVendor =
Common.DatabaseProviderTypes.POSTGRESQL
type Db = SqlDataProvider<
ConnectionString=connString,
DatabaseVendor=dbVendor,
ResolutionPath=npgsqlLibPath,
UseOptionTypes=true>
parameter. The connString that we are using here is the same one that we used
while running the migration script. You may have to change it to connect your
database.
The dataContext is specific to the database that we provided in the connection string,
and this is available as a property of the Db type.
As we will be passing this dataContext object around, in all our data access
functions, we can define a specific type for it to save some key strokes!
module Database
// ...
type DataContext = Db.dataContext
83
Chapter 9 - Persisting New User
But when the application goes live, we will be certainly pointing to a separate
database! To use a different PostgreSQL database at run time, we need a separate
DataContext pointing to that database.
We are already using one in our build script, which contains the connection string for
the migration script.
// build.fsx
//...
let connString =
environVarOrDefault
"FSTWEET_DB_CONN_STRING"
@"Server=127.0.0.1;Port=5432;..."
let dbConnection =
ConnectionString (connString, DatabaseProvider.PostgreSQL)
//...
The connString label here takes the value from the environment variable
FSTWEET_DB_CONN_STRING if it exists otherwise it picks a default one
// build.fsx
// ...
84
Chapter 9 - Persisting New User
Now if we run the application using the fake build script, the environment variable
FSTWEET_DB_CONN_STRING always has value!
The next step is using this environment variable to get a new data context.
So, while using SQLProvider in an application that can be used by multiple users
concurrently, we need to create a new data context for every request from the user
that involves database operation.
Let's assume that we have a function getDataContext , that takes a connection string
and returns its associated SQLProvider's data context. There are two ways that we
can use this function to create a new data context per request.
1. For every database layer function, we can pass the connection string and inside
that function that we can call the getDataContext using the connection string.
We are going to use the second option as its hides the details of getting an
underlying data context.
As a first step, define a type that represents the factory function to create a data
context.
// src/FsTweet.Web/Db.fs
// ...
type GetDataContext = unit -> DataContext
85
Chapter 9 - Persisting New User
// src/FsTweet.Web/Db.fs
// ...
let dataContext (connString : string) : GetDataContext =
fun _ -> Db.GetDataContext connString
Then in the application bootstrap get the connection string value from the
environment variable and call this function to get the factory function to create data
context for every request
// src/FsTweet.Web/FsTweet.Web.fs
// ...
open System
open Database
// ...
let main argv =
let fsTweetConnString =
Environment.GetEnvironmentVariable "FSTWEET_DB_CONN_STRING"
let getDataCtx = dataContext fsTweetConnString
// ...
The next step is passing the GetDataContext function to the request handlers which
we will address later in this chapter.
So, if we use the datacontext from the above factory function in mono, we may get
some errors associated with transaction when we asynchronously write any data to
the database
86
Chapter 9 - Persisting New User
// src/FsTweet.Web/UserSignup.fs
// ...
module Persistence =
// ...
open Database
87
Chapter 9 - Persisting New User
We need to explicitly specify the type of the parameter GetDataContext to use the
types provided by the SQLProvider.
Then we need to call the SubmitUpdatesAsync method on the ctx and return the Id
Though it appears like that we have completed the functionality, one important task
is pending in this function.
88
Chapter 9 - Persisting New User
// src/FsTweet.Web/Db.fs
module Database
// ...
let submitUpdates (ctx : DataContext) =
// TODO
Then call the SubmitUpdatesAsync method and use Async.Catch function from the
Async standard module which catches the exception thrown during the
asynchronous operation and return the result of the operation as a Choice type.
The return type of each function has been added as comments for clarity!
The Chessie library has a function ofChoice which transforms a Choice type to a
Result type. With the help of this function and the Async.map function from Chessie
library we can do the following
module Database
// ...
open Chessie.ErrorHandling
// ...
89
Chapter 9 - Persisting New User
The final step is transforming it to AsyncResult by using the AR union case as we did
while mapping the Failure type of AsyncResult.
AsyncResult<unit, System.Exception>
to
AsyncResult<unit, CreateUserError>
As this very similar to what we did while mapping the Failure type of AsyncResult in
the previous parts, let's jump in directly.
90
Chapter 9 - Persisting New User
// src/FsTweet.Web/FsTweet.Web.fs
//...
module Persistence =
// ...
// System.Exception -> CreateUserError
let private mapException (ex : System.Exception) =
Error ex
We will be handling the unique constraint violation errors later in this chapter.
Great! With this, we can wrap up the implementation of the createUser function.
To make it available, first, we need to change the webPart function to receive this as
a parameter and use it for partially applying it to the createUser function
// src/FsTweet.Web/UserSignup.fs
// ...
module Suave =
// ...
open Database
91
Chapter 9 - Persisting New User
let createUser =
Persistence.createUser getDataCtx
// ...
Then in the main function call the webPart function with the getDataCtx which we
created in the beginning of this chapter.
// src/FsTweet.Web/FsTweet.Web.fs
// ...
let main argv =
// ...
let app =
choose [
// ...
UserSignup.Suave.webPart getDataCtx
]
The SQLProvider internally uses the Npgsql library to interact with PostgreSQL. As a
matter of fact, through the ResolutionPath parameter, we provided a path in which
the Npgsql DLL resides.
To infer whether the PostgresException has occurred due to the violation of the
unique constraint, we need to check the ConstraintName and the SqlState property of
this exception.
For unique constraint violation, the ConstraintName property represents the name of
the constraint that has been violated and the SqlState property, which represents
PostgreSQL error code, will have the value "23505" .
92
Chapter 9 - Persisting New User
We can find out the unique constraints name associated with the Username and the
Email by running the \d "Users" command in psql. The constraint names are
IX_Users_Username and IX_Users_Email respectively.
Now we have enough knowledge on how to capture the unique violation exceptions
and represent it as a Domain type. So, Let's start our implementation.
At the time of this writing, there is an issue with the latest version of Npgsql.
So, we are using the version 3.1.10 here.
The next step is extending the mapException function that we defined in the previous
section to map these PostgresException s to its corresponding error types.
93
Chapter 9 - Persisting New User
// src/FsTweet.Web/UserSignup.fs
// ...
module Persistence =
// ...
open Npgsql
open System
// ...
We are doing pattern matching over the exception types here. First, we check
whether the exception is of type AggregateException . If it is, then we flatten it to get
the inner exception and check whether it is PostgresException .
For all the type mismatch on the exceptions, we return it as an Error case with the
actual exception.
Yes, We Can!
94
Chapter 9 - Persisting New User
Let's add a partial active pattern, UniqueViolation , in the Database module which
does the pattern matching over the exception types and parameterizes the check on
the constraint name.
// src/FsTweet.Web/Db.fs
module Database
// ...
open Npgsql
open System
Then with the help of this partial active pattern, we can rewrite the mapException as
Summary
Excellent, We learned a lot of things in this chapter!
95
Chapter 9 - Persisting New User
Finally, we transformed the return type of SQLProvider to our custom Domain type!
96
Chapter 10 - Sending Verification Email
Hi there!
In this chapter, we are going to add support for sending an email to verify the email
address of a new signup, which we faked earlier.
Setting Up Postmark
To send email, we are going to use Postmark, a transactional email service provider
for web applications.
There are three prerequisites that we need to do before we use it in our application.
You make use of this Getting started guide from postmark to get these three
prerequisites done.
Hi {{ username }},
Welcome to FsTweet!
http://localhost:8080/signup/verify/{{ verification_code }}
Cheers,
www.demystifyfp.com
97
Chapter 10 - Sending Verification Email
The username and the verification_code are placeholders in the template, that will
be populated with the actual value while sending the email.
Upon saving the template, you will get a unique identifier, like 3160924 . Keep a note
of it as we will be using it shortly.
As a first step, we have to add its NuGet package in our web project.
Then, create a new file Email.fs in the web project and move it above UserSignup.fs
file
Let's add some basic types that we required for sending an email
// FsTweet.Web/Email.fs
module Email
open Chessie.ErrorHandling
open System
type Email = {
To : string
TemplateId : int64
PlaceHolders : Map<string,string>
}
98
Chapter 10 - Sending Verification Email
The Email record represents the required details for sending an email, and the
SendEmail represents the function signature of a send email function.
The next step is adding a function which sends an email using Postmark.
// ...
open PostmarkDotNet
// ...
The sendEmailViaPostmark function takes the sender email address that we created as
part of the third prerequisite while setting up Postmark, a PostmarkClient and a value
of the Email type that we just created.
99
Chapter 10 - Sending Verification Email
By making use of the AwaitTask and the Catch function in the Async module, we
transformed Task<PostmarkResponse> to Choice<PostmarkResponse, Exception> .
The PostmarkClient would populate the Status property of the PostmarkResponse with
the value Success if everything went well. We need to return a unit in this case.
If the Status property doesn't have the Success value, the Message property of the
PostmarkResponse communicates what went wrong.
// FsTweet.Web/Email.fs
// ...
open System
// ...
let mapPostmarkResponse response =
match response with
| Choice1Of2 ( postmarkRes : PostmarkResponse) ->
match postmarkRes.Status with
| PostmarkStatus.Success ->
ok ()
| _ ->
let ex = new Exception(postmarkRes.Message)
fail ex
| Choice2Of2 ex -> fail ex
100
Chapter 10 - Sending Verification Email
// FsTweet.Web/Email.fs
// ...
let initSendEmail senderEmailAddress serverToken =
let client = new PostmarkClient(serverToken)
sendEmailViaPostmark senderEmailAddress client
The serverToken parameter represents the Server API token which will be used by
the PostmarkClient while communicating with the Postmark APIs to send an email.
The initSendEmail function partially applied the first two arguments of the
sendEmailViaPostmark function and returned a function having the signature Email ->
AsyncResult<unit, Exception> .
Then during the application bootstrap, get the sender email address and the
Postmark server token from environment variables and call the initSendEmail
// FsTweet.Web/FsTweet.Web.fs
// ...
open Email
// ...
let main argv =
// ...
let serverToken =
Environment.GetEnvironmentVariable "FSTWEET_POSTMARK_SERVER_TOKEN"
let senderEmailAddress =
Environment.GetEnvironmentVariable "FSTWEET_SENDER_EMAIL_ADDRESS"
// ...
The next step is adding the sendEmail function as a parameter in the sendSignupEmail
function
101
Chapter 10 - Sending Verification Email
// FsTweet.Web/UserSignup.fs
// ...
module Email =
// ...
open Email
and pass the actual sendEmail function to it from the main function.
// FsTweet.Web/FsTweet.Web.fs
// ...
let main argv =
// ...
let app =
choose [
// ...
UserSignup.Suave.webPart getDataCtx sendEmail
]
// ...
// FsTweet.Web/UserSignup.fs
// ...
module Suave =
// ...
let webPart getDataCtx sendEmail =
// ...
let sendSignupEmail = Email.sendSignupEmail sendEmail
// ...
The final task is putting the pieces together in the sendSignupEmail function.
102
Chapter 10 - Sending Verification Email
// FsTweet.Web/UserSignup.fs
// ...
module Email =
// ...
let sendSignupEmail sendEmail signupEmailReq = asyncTrial {
let verificationCode =
signupEmailReq.VerificationCode.Value
let placeHolders =
Map.empty
.Add("verification_code", verificationCode)
.Add("username", signupEmailReq.Username.Value)
let email = {
To = signupEmailReq.EmailAddress.Value
TemplateId = int64(3160924)
PlaceHolders = placeHolders
}
do! sendEmail email
|> mapAsyncFailure Domain.SendEmailError
}
Note that we are using do! as sendEmail asynchronously returing unit for
success.
As usual, we are mapping the failure type of the Async result from Exception to
SendEmailError
103
Chapter 10 - Sending Verification Email
One of the standard ways is faking the implementation and using the console as we
did earlier.
To enable this in our application, let's add a new function consoleSendEmail function
which prints the email record type in the console
// FsTweet.Web/Email.fs
// ...
let consoleSendEmail email = asyncTrial {
printfn "%A" email
}
Then in the main function, get the name of the environment from an environment
variable and initialize the signupEmail function accordingly.
// FsTweet.Web/FsTweet.Web.fs
// ...
let main argv =
// ...
let env =
Environment.GetEnvironmentVariable "FSTWEET_ENVIRONMENT"
let sendEmail =
match env with
| "dev" -> consoleSendEmail
| _ -> initSendEmail senderEmailAddress serverToken
// ...
Summary
With the help of the abstractions and design that we created in the earlier chapters,
we added the support for sending an email with ease.
104
Chapter 11 - Verifying User Email
Hi,
In the previous chapter, we added support for sending verification email using
Postmark.
In this chapter, we are going to wrap up the user signup workflow by implementing
the backend logic of the user verifcation link that we sent in the email.
// src/FsTweet.Web/UserSignup.fs
// ...
module Domain =
// ...
type VerifyUser = string -> AsyncResult<Username option, System.Exception>
// ...
The Username option type will have the value if the verification code matches
otherwise it would be None .
// src/FsTweet.Web/UserSignup.fs
// ...
module Persistence =
// ...
let verifyUser
(getDataCtx : GetDataContext)
(verificationCode : string) = asyncTrial {
// TODO
}
105
Chapter 11 - Verifying User Email
The first parameter getDataCtx represents the factory function to get the
SQLProvider's datacontext that we implemented while persisting a new user. When
we partially applying this argument alone, we will get a function of type VerifyUser
We first need to query the Users table to get the user associated with the verification
code provided.
let verifyUser
(getDataCtx : GetDataContext)
(verificationCode : string) = asyncTrial {
To get the first item from this IQueryable asynchronously, we need to call
Seq.tryHeadAsync function (an extension function provided by the SQLProvider)
106
Chapter 11 - Verifying User Email
// src/FsTweet.Web/Db.fs
// ...
let toAsyncResult queryable =
queryable // Async<'a>
|> Async.Catch // Async<Choice<'a, Exception>>
|> Async.map ofChoice // Async<Result<'a, Exception>>
|> AR // AsyncResult<'a, Exception>
Now, with the help of this toAsyncResult function, we can now do the exception
handling in the verifyUser function.
// src/FsTweet.Web/UserSignup.fs
// ...
module Persistence =
// ...
let verifyUser ... = asyncTrial {
let! userToVerify =
query {
// ...
} |> Seq.tryHeadAsync |> toAsyncResult
// TODO
}
Note that, We have changed let to let! to retrieve the UsersEntity option from
AsyncResult<DataContext.public.UsersEntity option> .
Great!
107
Chapter 11 - Verifying User Email
If the user exists, then we need to set the verification code to empty (to prevent from
using it multiple times) and mark the user as verified and persist the changes.
The last step is returning the username of the User to let the caller of the verifyUser
function know that the user has been verified and greet the user with the username.
We already have a domain type Username to represent the username. But the type of
the username that we retrieved from the database is a string .
We could use this function here but before committing, let's ponder over the
scenario.
While creating the user we used the TryCreate function to validate and create the
corresponding Username type. In case of any validation errors, we populated the
Failure part of the Result type with the appropriate error message of type string .
Now, when we read the user from the database, ideally there shouldn't be any
validation errors. But we can't guarantee this behavior as the underlying the
database table can be accessed and modified without using our validation pipeline.
We may not need this level of robustness, but the objective here is to
demonstrate how to build a robust system using F#.
So, the function that we need has to have the following signature
108
Chapter 11 - Verifying User Email
we can get a clue that we just need to map the failure type to Exception from string
We already have a function called mapFailure to map the failure type, but it is
defined after the definition of Username . To use it, we first move it before the
Username type definition.
// src/FsTweet.Web/UserSignup.fs
// ...
module Domain =
// ...
let mapFailure f aResult =
let mapFirstItem xs =
List.head xs |> f |> List.singleton
mapFailure mapFirstItem aResult
Back to the verifyUser function, we can now return the Username if user verification
succeeds
109
Chapter 11 - Verifying User Email
// src/FsTweet.Web/UserSignup.fs
// ...
module Persistence =
// ...
let verifyUser ... = asyncTrial {
// ...
| Some user ->
// ...
let! username =
Username.TryCreateAsync user.Username
return Some username
}
The next step is wiring up this persistence logic with the presentation layer.
// src/FsTweet.Web/UserSignup.fs
// ...
module Suave =
// ...
FsTweet.Web/views/user/verification_success.liquid
110
Chapter 11 - Verifying User Email
{% extends "master_page.liquid" %}
{% block head %}
<title> Email Verified </title>
{% endblock %}
{% block content %}
{% endblock %}
FsTweet.Web/views/not_found.liquid
{% extends "master_page.liquid" %}
{% block head %}
<title> Not Found :( </title>
{% endblock %}
{% block content %}
{{model}}
{% endblock %}
In case of errors during user verification, we need to log the error in the console and
render a generic error page to user
module Suave =
// ...
The input parameter errs is of type System.Exception list as the failure type
of Result is a list of error type, and we are using it as a list with the single
value.
Then add the liquid template for the showing the server error
111
Chapter 11 - Verifying User Email
FsTweet.Web/views/server_error.liquid
{% extends "master_page.liquid" %}
{% block head %}
<title> Internal Error :( </title>
{% endblock %}
{% block content %}
{{model}}
{% endblock %}
Now we have functions that map success and failure parts of the Result to its
corresponding WebPart .
The next step is using these two functions to map AsyncResult<Username option,
Exception> to Async<WebPart>
module Suave =
// ...
let handleVerifyUserAsyncResult aResult =
aResult // AsyncResult<Username option, Exception>
|> Async.ofAsyncResult // Async<Result<Username option, Exception>>
|> Async.map
(either onVerificationSuccess onVerificationFailure) // Async<WebPart>
Now the presentation side is ready; the next step is wiring the persistence and the
presentation layer.
112
Chapter 11 - Verifying User Email
module Suave =
// ...
let webPart getDataCtx sendEmail =
// ...
let verifyUser = Persistence.verifyUser getDataCtx
choose [
// ...
pathScan "/signup/verify/%s" (handleSignupVerify verifyUser)
]
The handleSignupVerify is not defined yet, so let's add it above the webPart function
module Suave =
// ...
let handleSignupVerify
(verifyUser : VerifyUser) verificationCode ctx = async {
// TODO
}
113
Chapter 11 - Verifying User Email
Summary
With this chapter, we have completed the user signup workflow.
I hope you found it useful and learned how to put the pieces together to build fully
functional feature robustly.
Exercise
How about sending a welcome email to the user upon successful verification of
his/her email?
114
Chapter 12 - Reorganising Code and Refactoring
Hi there!
We have come a long way so far, and we have lot more things to do!
Before we get going, Let's spend some time to reorganize some of the code that we
wrote and refactor specific functions to help ourselves to move faster.
The UserSignup.fs file has some helper functions for working with the Chessie
library. As a first step, we will move them to a separate file.
You can do it from the command line as we did before by executing the forge moveUp
In Windows, as the repeat command is not available in cmder out of the box, my
first preference is manually manipulating the file order in the fsproj file directly.
However, if you would like to do it via cmder, we can achieve it by following the
below steps
115
Chapter 12 - Reorganising Code and Refactoring
These steps assume that you are using cmder in bash mode as mentioned in the
first chapter.
> cd $CMDER_ROOT/config
#!/bin/bash
repeat() {
number=$1
shift
for i in `seq $number`; do
$@
done
}
> bash
If you are getting command not found error, execute the below command
source $CMDER_ROOT/config/user-profile.sh
116
Chapter 12 - Reorganising Code and Refactoring
So, let's move this function to the Chessie.fs file that we just created.
// FsTweet.Web/Chessie.fs
module Chessie
open Chessie.ErrorHandling
After we move this function to here, we need to refer this Chessie module in the
Domain module.
// FsTweet.Web/UserSignup.fs
namespace UserSignup
module Domain =
// ...
open Chessie
Make sure we are opening this Chessie module after opening Chessie.ErrorHandling
To fix this, let's have a look at the signature of the either function
('b -> 'c) -> ('d -> 'c) -> (Result<'b, 'd> -> 'c)
It takes a function to map the success part ('b -> 'c) and an another function to
map the failure part ('d -> 'c) and returns a function that takes a Result<'b, 'd>
117
Chapter 12 - Reorganising Code and Refactoring
It is the same thing that we needed, but the problem is the actual type of 'b and
'd
The success part 'b has a type ('TSuccess, 'TMessage list) to represent both the
success and the warning part. As we are not making use of warnings in FsTweet,
instead of this tuple and we just need the success part 'TSuccess alone.
To achieve it let's add a onSuccess adapter function which maps only the success
type and drops the warnings list in the tuple.
// FsTweet.Web/Chessie.fs
module Chessie
// ...
Then move our attention to the failure part d which has a type 'TMessage list
Like onSuccess , we can have a function onFailure to map the first item of the list
alone.
module Chessie
// ...
The onFailure function takes the first item from the list and uses it as the argument
while calling the map function f .
Now with the help of these two functions, onSuccess and onFailure , we can override
the either function.
module Chessie
// ...
// ('b -> 'c) -> ('d -> 'c) -> (Result<'b, 'd> -> 'c)
118
Chapter 12 - Reorganising Code and Refactoring
The overrided version either has the same signature but treats the success part
without warnings and the failure part as a single item instead of a list.
Let's use this in the Suave module in the functions that transform Result<Username
// FsTweet.Web/UserSignup.fs
namespace UserSignup
// ...
module Suave =
// ...
open Chessie
// ...
// ...
Thanks to the adapter functions, onSuccess and onFailure , now the function
signatures are expressing our intent without any compromises.
Let's do the same thing for the functions that map Result<UserId, UserSignupError> to
WebPart .
module Suave =
// ...
- let handleUserSignupError viewModel errs =
119
Chapter 12 - Reorganising Code and Refactoring
- either
- (handleUserSignupSuccess viewModel)
- (handleUserSignupError viewModel) result
+ either
+ (onUserSignupSuccess viewModel)
+ (onUserSignupFailure viewModel) result
// ...
While changing the function signature, we have also modified the prefix
handle to on to keep it consistent with the nomenclature that we are using to
the functions that are mapping the success and failure parts of a Result type.
// FsTweet.Web/UserSignup.fs
// ...
module Suave =
// ...
let handleUserSignup ... = async {
match result with
| Ok (userSignupReq, _) ->
// ...
| Bad msgs ->
let viewModel = {vm with Error = Some (List.head msgs)}
// ...
// ...
}
// ...
120
Chapter 12 - Reorganising Code and Refactoring
Like the adapter functions, onSuccess and onFailure , we need adapters while
pattern matching on the Result type.
// FsTweet.Web/Chessie.fs
module Chessie
// ...
With the help of this active pattern, we can now rewrite the handleUserSignup function
as
// FsTweet.Web/UserSignup.fs
// ...
module Suave =
// ...
let handleUserSignup ... = async {
match result with
- | Ok (userSignupReq, _) ->
+ | Success userSignupReq ->
// ...
- | Bad msgs ->
- let viewModel = {vm with Error = Some (List.head msgs)}
+ | Failure msg ->
+ let viewModel = {vm with Error = Some msg}
// ...
// ...
}
// ...
121
Chapter 12 - Reorganising Code and Refactoring
It takes a maping function ('a -> 'b) and an AsyncResult and maps the failure side
of the AsyncResult . But the name mapAsyncFailure not clearly communicates this.
We can also use an abbreviation AR to represent AsyncResult , and we can call the
function as AR.mapFailure .
To enable this, we need to create a new module AR and decorate it with the
RequireQualifiedAccess Attribute so that functions inside this module can't be called
without the module name.
// FsTweet.Web/Chessie.fs
module Chessie
// ...
[<RequireQualifiedAccess>]
module AR =
// TODO
Then move the mapAsyncFailure function to this module and rename it to mapFailure
module AR =
let mapFailure f aResult =
aResult
|> Async.ofAsyncResult
|> Async.map (mapFailure f) |> AR
And finally, we need to use this moved and renamed function in the Persistence and
Email modules in the UserSignup.fs file.
// FsTweet.Web/UserSignup.fs
// ...
module Persistence =
// ...
open Chessie
// ...
122
Chapter 12 - Reorganising Code and Refactoring
// ...
// FsTweet.Web/UserSignup.fs
// ...
module Email =
// ...
open Chessie
// ...
// FsTweet.Web/Db.fs
// ...
let submitUpdates ... =
// ...
|> Async.Catch
|> Async.map ofChoice
|> AR
The repeated three lines of code take an asynchronous computation Async<'a> and
execute it with exception handling using Async.Catch function and then map the
Async<Choice<'a, Exception>> to AsyncResult<'a, Exception> .
123
Chapter 12 - Reorganising Code and Refactoring
In other words, we can extract these three lines to a separate function which has the
signature
// FsTweet.Web/Chessie.fs
// ...
module AR =
// ...
Then use it to redefine the submitUpdates function and remove the toAsyncResult
// FsTweet.Web/Db.fs
// ...
open Chessie
// ...
let submitUpdates (ctx : DataContext) =
ctx.SubmitUpdatesAsync() // Async<unit>
|> AR.catch
// ...
Finally, change the verifyUser function to use this function instead of the removed
function toAsyncResult
// FsTweet.Web/UserSignup.fs
// ...
module Persistence =
// ...
open Chessie
// ...
124
Chapter 12 - Reorganising Code and Refactoring
// ...
With these, we are done with the refactoring and reorganizing of the functions
associated with the Chessie library.
Username
UserId
EmailAddress
Password
PasswordHash
So, let's put these types in a separate namespace User and use it in the Domain
module of UserSignup .
Create a new file, User.fs, in the web project and move it above UserSignup.fs file
// FsTweet.Web/User.fs
module User
open Chessie.ErrorHandling
open Chessie
type Username = ...
type UserId = ...
type EmailAddress = ...
type Password = ...
type PasswordHash = ...
125
Chapter 12 - Reorganising Code and Refactoring
// FsTweet.Web/UserSignup.fs
namespace UserSignup
module Domain =
// ...
open User
// ...
module Persistence =
// ...
open User
// ...
// ...
module Suave =
// ...
open User
// ...
Summary
In this chapter, we learned how to create adapter functions and override a
functionality provided by a library to fit our requirements. The key to this refactoring
is the understanding of the function signatures.
126
Chapter 13 - Adding Login Page
In this chapter, we are going to start the implementation of a new feature, enabling
users to log in to FsTweet.
Let's get started by creating a new file Auth.fs in the web project and move it above
FsTweet.Web.fs
To start with, let's create a module Suave with a view model for the login page.
// FsTweet.Web/Auth.fs
namespace Auth
module Suave =
type LoginViewModel = {
Username : string
Password : string
Error : string option
}
module Suave =
// ...
let emptyLoginViewModel = {
Username = ""
Password = ""
Error = None
}
127
Chapter 13 - Adding Login Page
FsTweet.Web/views/user/login.liquid
{% extends "master_page.liquid" %}
{% block head %}
<title> Login </title>
{% endblock %}
{% block content %}
<div>
{% if model.Error %}
<p class="alert alert-danger">
{{ model.Error.Value }}
</p>
{% endif %}
<form method="POST" action="/login">
<input
type="text" id="Username" name="Username"
value="{{model.Username}}" required>
<input
type="password" id="Password" name="Password"
value="{{model.Password}}" required>
<button type="submit">Login</button>
</form>
</div>
{% endblock %}
For brevity, the styles and some HTML tags are ignored.
The next step is creating a new function to render this template with a view model.
// FsTweet.Web/Auth.fs
module Suave =
open Suave.DotLiquid
// ...
let loginTemplatePath = "user/login.liquid"
Then create a new function webpart to wire this function with the /login path
128
Chapter 13 - Adding Login Page
module Suave =
// ...
open Suave.Filters
open Suave.Operators
// ...
let webpart () =
path "/login"
>=> renderLoginPage emptyLoginViewModel
The last step is calling this webpart function from the main function and append this
webpart to the application's webpart list.
// FsTweet.Web/FsTweet.Web.fs
// ...
let main argv =
// ...
let app =
choose [
// ...
Auth.Suave.webpart ()
]
// ...
That's it!
If we run the application now, you can see a beautiful login page
129
Chapter 13 - Adding Login Page
// FsTweet.Web/Auth.fs
module Suave =
// ...
let handleUserLogin ctx = async {
// TODO
}
// ...
module Suave =
+ open Suave
// ...
let webpart () =
- path "/login"
- >=> renderLoginPage emptyLoginViewModel
+ path "/login" >=> choose [
+ GET >=> renderLoginPage emptyLoginViewModel
+ POST >=> handleUserLogin
+ ]
To handle the request for login, we first need to bind the submitted form values to a
value of LoginViewModel
// ...
open Suave.Form
// ...
let handleUserLogin ctx = async {
match bindEmptyForm ctx.request with
| Choice1Of2 (vm : LoginViewModel) ->
// TODO
| Choice2Of2 err ->
// TODO
}
If there is an error while doing model binding, we can populate the Error field of an
empty LoginViewModel and rerender the login page.
130
Chapter 13 - Adding Login Page
Let's define a new module Domain in Auth.fs above Suave and define a domain type
for the login request.
// FsTweet.Web/Auth.fs
namespace Auth
module Domain =
open User
type LoginRequest = {
Username : Username
Password : Password
}
module Suave =
// ...
Then define a static member function TryCreate which creates LoginRequest using
the trial computation expression and the TryCreate functions of Username and
Password type.
131
Chapter 13 - Adding Login Page
module Domain =
open Chessie.ErrorHandling
// ...
type LoginRequest = // ...
Then in the handleUserLogin function, we can make use of this function to validate
the LoginViewModel .
module Suave =
open Domain
open Chessie
// ...
let handleUserLogin ctx = async {
// ...
| Choice1Of2 (vm : LoginViewModel) ->
let result =
LoginRequest.TryCreate (vm.Username, vm.Password)
match result with
| Success req ->
return! Successful.OK "TODO" ctx
| Failure err ->
let viewModel = {vm with Error = Some err}
return! renderLoginPage viewModel ctx
// ...
}
The Success and Failure active pattern that we defined in the previous chapter made
our job easier here to pattern match on the Result<LoginRequest,string> type.
If there is any error, we populate the view model with the error message and
rerender the login page.
132
Chapter 13 - Adding Login Page
For a valid login request, we need to implement the actual behavior. Let's leave this
as a TODO and revisit it in the next chapter.
Summary
In this chapter, we added implementations for rending the login page. Then we
added functions to handle and validate the login request from the user.
133
Chapter 14 - Handling Login Request
In the previous chapter, we have validated the login request from the user and
mapped it to a domain type LoginRequest . The next step is authenticating the user to
login to the application.
We are going to implement all the above steps except creating a user session in this
chapter.
So, as a first step, let's create a record type for representing the User .
// FsTweet.Web/User.fs
// ...
type User = {
UserId : UserId
Username : Username
PasswordHash : PasswordHash
}
// FsTweet.Web/User.fs
// ...
type UserEmailAddress =
| Verified of EmailAddress
| NotVerified of EmailAddress
134
Chapter 14 - Handling Login Request
To retrieve the string representation of the EmailAddress in both the cases, let's add a
member property Value
type UserEmailAddress =
// ...
with member this.Value =
match this with
| Verified e | NotVerified e -> e.Value
type User = {
// ...
EmailAddress : UserEmailAddress
}
Now we have a domain type to represent the user. The next step is defining a type
for the function which retireves User by Username
// FsTweet.Web/User.fs
// ...
type FindUser =
Username -> AsyncResult<User option, System.Exception>
As the user may not exist for a given Username , we are using User option .
Create a new module Persistence in the User.fs and add a findUser function
// FsTweet.Web/User.fs
// ...
module Persistence =
open Database
135
Chapter 14 - Handling Login Request
Finding the user by Username is very similar to what we did in the verifyUser
function. There we found the user by verification code, and here we need to find by
Username .
module Persistence =
// ...
open FSharp.Data.Sql
open Chessie
If the user exists, we need to transform that user that we retrieved to its
corresponding User domain model. To do it, we need a function that has the
signature
136
Chapter 14 - Handling Login Request
// FsTweet.Web/User.fs
// ...
module Persistence =
// ...
let mapUser (user : DataContext.``public.UsersEntity``) =
// TODO
// ...
But we didn't have one for the PasswordHash . As we need it in this mapUser function,
let's define it.
// FsTweet.Web/User.fs
module User
// ...
type PasswordHash = ...
// ...
The InterrogateHash function from the BCrypt library takes a hash and outputs its
components if it is valid. In case of invalid hash, it throws an exception.
Now, coming back to the mapUser that we just started, let's map the username, the
password hash, and the email address of the user
137
Chapter 14 - Handling Login Request
// FsTweet.Web/User.fs
// ...
module Persistence =
let mapUser (user : DataContext.``public.UsersEntity``) =
let userResult = trial {
let! username = Username.TryCreate user.Username
let! passwordHash = PasswordHash.TryCreate user.PasswordHash
let! email = EmailAddress.TryCreate user.Email
// TODO
}
// TODO
// ...
Then we need to check whether the user email address is verified or not and create
the corresponding UserEmailAddress type.
Now we have all the individual fields of the User record; we can return it from trial
computation expression
138
Chapter 14 - Handling Login Request
The userResult is of type Result<User, string> with the failure (of string type) side
representing the validation error that may occur while mapping the user
representation from the database to the domain model. It also means that data that
we retrieved is not consistent, and hence we need to treat this failure as Exception.
With the help of this mapUser function, we can now return the User domain type
from the findUser function if the user exists for the given username
// FsTweet.Web/User.fs
// ...
module Persistence =
// ...
let mapUser ... = ...
139
Chapter 14 - Handling Login Request
// FsTweet.Web/User.fs
// ...
type PasswordHash = ...
// ...
// ...
The Verify function from the BCrypt library takes care of verifying the password
with the hash and returns true if there is a match and false otherwise.
Now we have the required functions for implementing the login function.
Let's start our implementation of the login function by defining a type for it.
// FsTweet.Web/Auth.fs
module Domain =
// ...
type Login =
FindUser -> LoginRequest -> AsyncResult<User, LoginError>
module Domain =
// ...
type LoginError =
| UsernameNotFound
| EmailNotVerified
| PasswordMisMatch
| Error of System.Exception
The LoginError discriminated union elegantly represents all the possible errors that
may happen while performing the login operation.
The implementation of the login function starts with finding the user and mapping
its failure to the Error union case if there is any error.
140
Chapter 14 - Handling Login Request
module Domain =
// ...
open Chessie
If the user to find didn't exist, we need to return the UsernameNotFound error.
The F# Compiler infers the failure part of the AsyncResult as LoginError from the
below expression
asyncTrial {
let! userToFind =
findUser req.Username // AsyncResult<User, Exception>
|> AR.mapFailure Error // AsyncResult<User, LoginError>
}
141
Chapter 14 - Handling Login Request
asyncTrial {
return UsernameNotFound // LoginError
}
It is because the return keyword behind the scenes calls the Return function of the
AsyncTrialBuilder type and this Return function populates the success side of the
AsyncResult .
Here is the code snippet of the Return function copied from the Chessie
library for your reference
type AsyncTrialBuilder() =
member __.Return value : AsyncResult<'a, 'b> =
value
|> ok
|> Async.singleton
|> AR
To fix this type mismatch we need to do what the Return function does but for the
failure side.
The let! expression followed by return can be replaced with return! which does
the both.
142
Chapter 14 - Handling Login Request
The next thing that we have to do in the login function, checking whether the user's
email is verified or not. If it is not verified, we return the EmailNotVerified error.
If the user's email address is verified, then we need to verify his/her password and
return PasswordMisMatch error if there is a mismatch.
143
Chapter 14 - Handling Login Request
I am sure you would be thinking about refactoring the following piece of code which
is getting repeated in all the three places when we return a failure from the
asyncTrial computation expression.
|> fail
|> Async.singleton
|> AR
To refactor it, let's have a look at the signature of the fail function from the
Chessie library.
The three lines of code that was getting repeated do the same transformation but on
the AsyncResult instead of Result
144
Chapter 14 - Handling Login Request
So, let's create fail function in the AR module which implements this logic
// FsTweet.Web/Chessie.fs
// ...
module AR =
// ...
let fail x =
x // 'b
|> fail // Result<'a, 'b>
|> Async.singleton // Async<Result<'a, 'b>>
|> AR // AsyncResult<'a, 'b>
With the help of this new function, we can simplify the login function as below
+ open Chessie
...
- return!
- UsernameNotFound
- |> fail
- |> Async.singleton
- |> AR
+ return! AR.fail UsernameNotFound
...
- return!
- EmailNotVerified
- |> fail
- |> Async.singleton
- |> AR
+ return! AR.fail EmailNotVerified
...
- return!
- PasswordMisMatch
- |> fail
- |> Async.singleton
- |> AR
+ return! AR.fail PasswordMisMatch
Coming back to the login function, if the password does match, we just need to
return the User .
145
Chapter 14 - Handling Login Request
The presentation layer can take this value of User type and send it to the end user
either as an HTTP Cookie or a JWT.
146
Chapter 14 - Handling Login Request
// FsTweet.Web/Auth.fs
// ...
module Suave =
// ...
In case of login success, we return the username as a response. In the next chapter,
we will be revisiting this piece of code.
// FsTweet.Web/Auth.fs
// ...
module Suave =
// ...
open User
// ...
// User -> WebPart
let onLoginSuccess (user : User) =
Successful.OK user.Username.Value
// ...
147
Chapter 14 - Handling Login Request
With the help of these two function, we can transform the Result<User,LoginError> to
WebPart using the either function
module Suave =
// ...
// ...
The next piece of work is transforming the async version of login result
module Suave =
// ...
open Chessie.ErrorHandling // Make sure this open statement is above "open Chessie"
The final step is wiring the domain, persistence and the presentation layers
associated with the login.
First, pass the getDataCtx function from the main function to the webpart function
// FsTweet.Web/FsTweet.Web.fs
- Auth.Suave.webpart ()
+ Auth.Suave.webpart getDataCtx
Then in the webpart function in the add getDataCtx as its parameter and use it to
partially apply in the findUser function
- let webpart () =
+ let webpart getDataCtx =
+ let findUser = Persistence.findUser getDataCtx
148
Chapter 14 - Handling Login Request
function.
Finally in the handleUserLogin function, if the login request is valid, call the login
function with the provided findUser function and the validated login request and
transform the result of the login function with to WebPart using the
handleLoginAsyncResult defined earlier.
That's it!
Summary
We covered a lot of ground in this chapter. We started with finding the user by
username and then we moved to implement the login function. And finally, we
transformed the result of the login function to the corresponding webparts.
149
Chapter 14 - Handling Login Request
150
Chapter 15 - Creating User Session and Authenticating User
In the last chapter, we have implemented the backend logic to verify the login
credentials of a user. Upon successful verification of the provided credentials, we
just responded with a username.
In this chapter, we are going to replace this placeholder with the actual
implementation.
This session cookie will be present in all the subsequent requests from the user and
we can use it to authenticate the user instead of prompting the username and the
password for each request.
To create this session id and the cookie, we are going to leverage the authenticated
function from Suave.
The CookieLife defines the lifespan of a cookie in the user's browser, and the bool
module Suave =
+ open Suave.Authentication
+ open Suave.Cookie
// ...
// ...
The CookieLife.Session defines that the cookie will be present till the user he quits
the browser. There is another option MaxAge of TimeSpan to specify the lifespan of the
cookie using TimeSpan. And as we are not going to use HTTPS, we are setting the
151
Chapter 15 - Creating User Session and Authenticating User
The session id in the cookie doesn't personally identify the user. So, we need to
store the associated user information in some other place.
+------------------+--------------------+
| SessionId | User |
+---------------------------------------+
| | |
+------------------+--------------------+
Every approach has its pros and cons, and we need to pick the opt one.
The Suave library has an abstraction to deal with this state management.
https://github.com/SuaveIO/suave/blob/master/src/Suave/State.fs
type StateStore =
/// Get an item from the state store
abstract get<'T> : string -> 'T option
/// Set an item in the state store
abstract set<'T> : string -> 'T -> WebPart
152
Chapter 15 - Creating User Session and Authenticating User
// FsTweet.Web/Auth.fs
// ...
module Suave =
// ...
open Suave.State.CookieStateStore
// ...
// string -> 'a -> HttpContext -> WebPart
let setState key value ctx =
match HttpContext.state ctx with
| Some state ->
state.set key value
| _ -> never
// ...
The statefulForSession function, a WebPart from Suave, initializes the state in the
HttpContext with CookieStateStore .
The setState function takes a key and value, along with a HttpContext . If there is a
state store present in the HttpContext , it stores the key and the value pair. In the
absence of a state store, it does nothing, and we are making the never WebPart
from Suave to denote it.
An important thing that we need to notice here is setState function doesn't know
what is the underlying StateStore that we are using.
We are making use of the context function (aka combinator) while calling the
setState . The context function from the Suave library is having the following
signature.
153
Chapter 15 - Creating User Session and Authenticating User
The final step in calling this createUserSession function from the onLoginSuccess
Let's get started by creating a new fsharp file Wall.fs and move it above
FsTweet.Web.fs
Then in the Wall.fs file add this initial implementation of User's wall.
// FsTweet.Web/Wall.fs
namespace Wall
module Suave =
open Suave
open Suave.Filters
open Suave.Operators
let webpart () =
path "/wall" >=> renderWall
And finally, call this webpart function from the main function
154
Chapter 15 - Creating User Session and Authenticating User
// FsTweet.Web/FsTweet.Web.fs
// ...
let main argv =
// ...
let app =
choose [
// ...
Wall.Suave.webpart ()
]
// ...
Now if we run the application and log in using a registered account, we will be
redirected to the wall page, and we can find the cookies for auth and state in the
browser.
The values of these cookies are encrypted using a randomly generated key on the
server side by Suave. We can either provide this key or let the suave to create one.
The downside of letting suave to generate the key is, it will create a new key
whenever the server restarts. And also if we run multiple instances of FsTweet.Web
behind a load balancer, each instance will have a different server key.
So, the ideal thing would be explicitly providing the server key.
155
Chapter 15 - Creating User Session and Authenticating User
// FsTweet/script.fsx
#r "./packages/Suave/lib/net40/Suave.dll"
open Suave.Utils
open System
Crypto.generateKey Crypto.KeyLength
|> Convert.ToBase64String
|> printfn "%s"
The next step is passing this a key as an environment variable to the application and
configuring the suave web server to use this key.
// FsTweet.Web/FsTweet.Web.fs
Protecting WebParts
Currently, the Wall page can be accessed even without login as we are not
protecting it.
156
Chapter 15 - Creating User Session and Authenticating User
For validating the auth token present in the cookie, Suave.Authentication module has
a function called authenticate .
// FsTweet.Web/Auth.fs
module Suave =
// ...
let redirectToLoginPage =
Redirection.FOUND "/login"
For both missingCookie and decryptionFailure , we are redirecting the user to the
login page, and for a valid auth session cookie, we need to give some thoughts.
We first have to retrieve the User value from the state cookie, and then we have to
call the provided fSuccess . If there is an error while retrieving the user from the
cookie, we need to redirect to the login page.
157
Chapter 15 - Creating User Session and Authenticating User
module Suave =
// ...
// ...
// ...
In the userSession function, we are initializing the user state from the
CookieStateStore by calling the statefulForSession function, and then we retrieve the
logged in user from the state cookie.
With the help of the requiresAuth function, now we can define a WebPart that can be
accessed only by the authenticated user.
Going back to renderWall function in the Wall.fs, we can now make it accessible only
to the authenticated user by doing the following changes.
158
Chapter 15 - Creating User Session and Authenticating User
// FsTweet.Web/Wall.fs
module Suave =
// ...
+ open User
+ open Auth.Suave
let webpart () =
- path "/wall" >=> renderWall
+ path "/wall" >=> requiresAuth renderWall
Instead of displaying a plain text, TODO , we have replaced it with the username of
the logged in user. We will be revisiting this renderWall function in the later chapters.
But better user experience would be redirecting the user to the wall page.
// FsTweet.Web/Auth.fs
159
Chapter 15 - Creating User Session and Authenticating User
module Suave =
// ...
// ...
The next step is changing the renderLoginPage function to accommodate this new
requirement.
// FsTweet.Web/Auth.fs
module Suave =
// ...
- let renderLoginPage (viewModel : LoginViewModel) =
- page loginTemplatePath viewModel
+ let renderLoginPage (viewModel : LoginViewModel) hasUserLoggedIn =
+ match hasUserLoggedIn with
+ | Some _ -> Redirection.FOUND "/wall"
+ | _ -> page loginTemplatePath viewModel
// ...
160
Chapter 15 - Creating User Session and Authenticating User
...
- renderLoginPage vm
+ renderLoginPage vm None
...
Summary
In this chapter, we learned how to do authentication in Suave and manage state
using cookies. The source code associated with this part is available on GitHub
Exercises
1. Instead of storing the user information in a cookie, store and retrieve it from a
new table in the PostgreSQL database. You can get the session id from the auth
cookie by using the HttpContext.sessionId function in the Suave.Authentication
module.
2. Suave supports cookies with sliding expiry. Replace the CookieLife.Session with
CookieLife.MaxAge and implement sliding expiry.
161
Chapter 16 - Posting New Tweet
Hi there!
In this chapter, we are going to implement the core feature of Twitter, posting a
tweet.
This initial version of user's wall page will display a textarea to capture the tweet.
It will also greet the user with a message Hi {username} along with links to go his/her
profile page and log out. We will be adding implementations for profile and log out in
the later chapters.
namespace Wall
module Suave =
// ...
open Suave.DotLiquid
type WallViewModel = {
Username : string
}
// ...
Create a new dotliqud template wall.liquid in the views/user directory and update it
as below
162
Chapter 16 - Posting New Tweet
{% extends "master_page.liquid" %}
{% block head %}
<title> {{model.Username}} </title>
{% endblock %}
{% block content %}
<div>
<div>
<p class="username">Hi {{model.Username}}</p>
<a href="/{{model.Username}}">My Profile</a>
<a href="/logout">Logout</a>
</div>
<div>
<div>
<form id="tweetForm">
<textarea id="tweet"></textarea>
<button> Tweet </button>
</form>
</div>
</div>
</div>
{% endblock %}
Now, if you run the application, you will be able to see the updated wall page after
login.
The better option would be the javascript code on the wall page doing an AJAX
POST request with a JSON payload when the user clicks the Tweet button.
163
Chapter 16 - Posting New Tweet
Currently, we are redirecting the user to login page, if the user didn't have access.
But this approach will not work out for AJAX requests, as it doesn't do full page
refresh.
What we want is an HTTP response from the server with a status code 401
// FsTweet.Web/Auth.fs
// ...
module Suave =
// ...
// WebPart -> WebPart -> WebPart
let onAuthenticate fSuccess fFailure =
authenticate CookieLife.Session false
(fun _ -> Choice2Of2 fFailure)
(fun _ -> Choice2Of2 fFailure)
(userSession fFailure fSuccess)
164
Chapter 16 - Posting New Tweet
We have extracted the requiresAuth function into a new function onAuthenticate and
added a new parameter fFailure to parameterize what to do when authentication
fails.
Then in the requiresAuth function, we are calling the onAuthenticate function with the
redirectToLoginPage webpart for authentication failures.
Now with the help of the new function onAuthenticate , we can send an unauthorized
response in case of an authentication failure using a new function requiresAuth2
There are only two hard things in Computer Science: cache invalidation and
naming things. -- Phil Karlton
However, we can do it with ease with the fundamental HTTP abstractions provided
by Suave.
We just need to serialize the return value to the JSON string representation and
send the response with the header Content-Type populated with application/json
value.
To do the JSON serialization and deserialization (which we will be doing later in this
chapter), let's add a Chiron Nuget Package to the FsTweet.Web project.
165
Chapter 16 - Posting New Tweet
Chiron is a JSON library for F#. It can handle all of the usual things you’d want
to do with JSON, (parsing and formatting, serialization and deserialization).
Chiron works rather differently to most .NET JSON libraries with which you
might be familiar, using neither reflection nor annotation, but instead uses a
simple functional style to be very explicit about the relationship of types to
JSON. This gives a lot of power and flexibility - Chrion Documentation
Then create a new fsharp file Json.fs to put all the JSON related functionalities.
To send an error message to the front-end, we are going to use the following JSON
structure
{
"msg" : "..."
}
Let's add a function, unauthorized , in the Json.fs file that returns a WebPart having a
401 Unauthorized response with a JSON body.
166
Chapter 16 - Posting New Tweet
// FsTweet.Web/Json.fs
[<RequireQualifiedAccess>]
module JSON
open Suave
open Suave.Operators
open Chiron
// WebPart
let unauthorized =
["msg", String "login required"] // (string * Json) list
|> Map.ofList // Map<string,Json>
|> Object // Json
|> Json.format // string
|> RequestErrors.UNAUTHORIZED // Webpart
>=> Writers.addHeader
"Content-type" "application/json; charset=utf-8"
The String and Object are the union cases of the Json discriminated type in the
Chiron library.
The Json.format function creates the string representation of the underlying Json
header.
With this we are done with the authentication side of HTTP endpoints serving JSON
response.
167
Chapter 16 - Posting New Tweet
// FsTweet.Web/Wall.fs
module Suave =
// ...
let handleNewTweet (user : User) ctx = async {
// TODO
}
// ...
// FsTweet.Web/Wall.fs
module Suave =
// ...
- let webpart () =
- path "/wall" >=> requiresAuth renderWall
+ let webpart () =
+ choose [
+ path "/wall" >=> requiresAuth renderWall
+ POST >=> path "/tweets"
+ >=> requiresAuth2 handleNewTweet
+ ]
The first step in handleNewTweet is parsing the incoming JSON body, and the next
step is deserializing it to a fsharp type. Chiron library has two functions
Json.tryParse and Json.tryDeserialize to do these two steps respectively.
Let's add a new function parse in Json.fs to parse the JSON request body in the
HttpRequest to Chiron's Json type.
168
Chapter 16 - Posting New Tweet
// FsTweet.Web/Json.fs
// ...
open System.Text
open Chessie.ErrorHandling
Then in the handleNewTweet function, we can call this function to parse the incoming
the HTTP request.
// ...
open Chessie
// ...
let handleNewTweet (user : User) ctx = async {
match JSON.parse ctx.request with
| Success json ->
// TODO
| Failure err ->
// TODO
}
If there is any parser error, we need to return bad request with a JSON body. To do
it, let's leverage the same JSON structure that we have used for sending JSON
response for unauthorized requests.
// FsTweet.Web/Json.fs
// ...
169
Chapter 16 - Posting New Tweet
The badRequest function and the unauthorized binding both have some common
code. So, let's extract the common part out.
// FsTweet.Web/Json.fs
// ...
Then change the unauthorized and badRequest functions to use this new function
Going back to the handleNewTweet function, if there is an error while parsing the
request JSON, we can return a bad request as a response.
// FsTweet.Web/Wall.fs
// ...
module Suave =
// ...
let handleNewTweet (user : User) ctx = async {
match JSON.parse ctx.request with
| Success json ->
// TODO
| Failure err ->
- // TODO
+ return! JSON.badRequest err ctx
}
// ...
170
Chapter 16 - Posting New Tweet
Let's switch our focus to handle a valid JSON request from the user.
{
"post" : "Hello, World!"
}
To represent this JSON on the server side (like View Model), Let's create a new type
PostRequest .
// FsTweet.Web/Wall.fs
module Suave =
// ...
type PostRequest = PostRequest of string
// ...
To deserialize the Json type (that we get after parsing) to PostRequest , Chiron
library requires PostRequest type to have a static member function FromJson with the
signature PostRequest -> Json<PostRequest>
module Suave =
// ...
open Chiron
// ...
type PostRequest = PostRequest of string with
// PostRequest -> Json<PostRequest>
static member FromJson (_ : PostRequest) = json {
let! post = Json.read "post"
return PostRequest post
}
// ...
We are making use of the json computation expression from Chrion library to
create PostRequest from Json .
171
Chapter 16 - Posting New Tweet
The Json.tryDeserialize function takes Json as its input and return Choice<'a,
string> where the actual type of 'a is inferred from the usage of Choice and also
the actual type of 'a should have a static member function FromJson .
In case of any deserialization error, we are returning it as a bad request using the
JSON.badRequest function that we created earlier.
Now we have the server side representation of a tweet post in form of PostRequest .
The next step is validating this new tweet post.
As we did for making illegal states unrepresentable in user signup, let's create a new
type Post , a domain model of the tweet post.
172
Chapter 16 - Posting New Tweet
// FsTweet.Web/Tweet.fs
namespace Tweet
open Chessie.ErrorHandling
We can now use this Post.TryCreate static member function to validate the
PostRequest in the handleNewTweet function.
// FsTweet.Web/Wall.fs
module Suave =
open Tweet
// ...
let handleNewTweet (user : User) ctx = async {
match JSON.parse ctx.request with
| Success json ->
match Json.tryDeserialize json with
| Choice1Of2 (PostRequest post) ->
- // TODO
+ match Post.TryCreate post with
+ | Success post ->
+ return! Successful.OK "TODO" ctx
+ | Failure err ->
+ return! JSON.badRequest err ctx
// ...
// ...
173
Chapter 16 - Posting New Tweet
// FsTweet.Db.Migrations/FsTweet.Db.Migrations.fs
// ...
override this.Up() =
base.Create.Table("Tweets")
.WithColumn("Id").AsGuid().PrimaryKey()
.WithColumn("Post").AsString(144).NotNullable()
.WithColumn("UserId").AsInt32().ForeignKey("Users", "Id")
.WithColumn("TweetedAt").AsDateTimeOffset().NotNullable()
|> ignore
override this.Down() =
base.Delete.Table("Tweets") |> ignore
Then run the migration using forge fake RunMigrations command to create the
Tweets table.
FsTweet=# \d "Tweets";;
Table "public.Tweets"
Column | Type | Modifiers
-----------+--------------------------+-----------
Id | uuid | not null
Post | character varying(144) | not null
UserId | integer | not null
TweetedAt | timestamp with time zone | not null
Indexes:
"PK_Tweets" PRIMARY KEY, btree ("Id")
Foreign-key constraints:
"FK_Tweets_UserId_Users_Id"
FOREIGN KEY ("UserId") REFERENCES "Users"("Id")
174
Chapter 16 - Posting New Tweet
Then define a type for representing the function for persisting a new tweet.
// FsTweet.Web/Tweet.fs
// ...
open User
open System
// ...
type TweetId = TweetId of Guid
type CreateTweet =
UserId -> Post -> AsyncResult<TweetId, Exception>
Then create a new module Persistence in Tweet.fs and define the createTweet
// FsTweet.Web/Tweet.fs
// ...
module Persistence =
open User
open Database
open System
To use this persistence logic with the handleNewTweet function, we need to transform
the AsyncResult<TweetId, Exception> to WebPart .
175
Chapter 16 - Posting New Tweet
Before we go ahead and implement it, let's add few helper functions in Json.fs to
send Ok and InternalServerError responses with JSON body
// FsTweet.Web/Json.fs
// ...
// WebPart
let internalError =
error ServerErrors.INTERNAL_ERROR "something went wrong"
Then define what we need to for both Success and Failure case.
// FsTweet.Web/Wall.fs
// ...
module Suave =
// ...
open Chessie.ErrorHandling
open Chessie
// ...
176
Chapter 16 - Posting New Tweet
The final piece is passing the dependency getDataCtx for the createTweet function
from the application's main function.
// FsTweet.Web/FsTweet.Web.fs
// ...
- Wall.Suave.webpart ()
+ Wall.Suave.webpart getDataCtx
]
// FsTweet.Web/Wall.fs
// ...
- let handleNewTweet (user : User) ctx = async {
+ let handleNewTweet createTweet (user : User) ctx = async {
// ...
- let webpart () =
+ let webpart getDataCtx =
+ let createTweet = Persistence.createTweet getDataCtx
choose [
path "/wall" >=> requiresAuth renderWall
POST >=> path "/tweets"
- >=> requiresAuth2 handleNewTweet
+ >=> requiresAuth2 (handleNewTweet createTweet)
]
And then invoke the createTweet function in the handleNewTweet function and
transform the result to WebPart using the handleAsyncCreateTweetResult function.
With this, we have successfully added support for creating a new tweet.
177
Chapter 16 - Posting New Tweet
To invoke this HTTP API from the front end, let's create a new javascript file
FsTweet.Web/assets/js/wall.js and update it as below
$(function(){
$("#tweetForm").submit(function(event){
event.preventDefault();
$.ajax({
url : "/tweets",
type: "post",
data: JSON.stringify({post : $("#tweet").val()}),
contentType: "application/json"
}).done(function(){
alert("successfully posted")
}).fail(function(jqXHR, textStatus, errorThrown) {
console.log({
jqXHR : jqXHR,
textStatus : textStatus,
errorThrown: errorThrown})
alert("something went wrong!")
});
});
});
We are making use of the scripts block defined the master_page.liquid here.
<div id="scripts">
<!-- ... -->
{% block scripts %}
{% endblock %}
</div>
Let's run the application and do a test drive to verify this new feature.
178
Chapter 16 - Posting New Tweet
// FsTweet.Web/Wall.fs
// FsTweet.Web/Auth.fs
// FsTweet.Web/UserSignup.fs
// ...
179
Chapter 16 - Posting New Tweet
('a -> 'b) -> ('c -> 'b) -> AsyncResult<'a, 'c> -> Async<'b>
// onSuccess onFailure aResult aWebPart
('a -> 'b) -> ('c -> 'b) -> Result<'a, 'c> -> 'b
The only difference is, the function that we need should work with AsyncResult
instead of Result . In other words, we need the either function for AsyncResult .
// FsTweet.Web/Chessie.fs
// ...
module AR =
// ...
let either onSuccess onFailure aResult =
aResult
|> Async.ofAsyncResult
|> Async.map (either onSuccess onFailure)
180
Chapter 16 - Posting New Tweet
// FsTweet.Web/Wall.fs
// ...
- let handleCreateTweetResult result =
- either onCreateTweetSuccess onCreateTweetFailure result
-
- let handleAsyncCreateTweetResult aResult =
- aResult
- |> Async.ofAsyncResult
- |> Async.map handleCreateTweetResult
// ...
let handleNewTweet createTweet (user : User) ctx = async {
// ...
match Post.TryCreate post with
| Success post ->
- let aCreateTweetResult = createTweet user.UserId post
let! webpart =
- handleAsyncCreateTweetResult aCreateTweetResult
+ createTweet user.UserId post
+ |> AR.either onCreateTweetSuccess onCreateTweetFailure
// ...
If there is any error while doing any of these, we are returning bad request as a
response.
181
Chapter 16 - Posting New Tweet
We can unify these two functions together that has the following signature
Let' name this function deserialize and add the implementation in Json.fs
// FsTweet.Web/Json.fs
// ...
// HttpRequest -> Result<^a, string>
let inline deserialize< ^a when (^a or FromJsonDefaults)
: (static member FromJson: ^a -> ^a Json)>
req : Result< ^a, string> =
Chiron library has FromJsonDefaults type to extend the fsharp primitive types to have
the FromJson static member function.
The bind function is from Chessie library, which maps the success part of the
Result with the provided function.
With this new function, we can rewrite the handleNewTweet function as below
182
Chapter 16 - Posting New Tweet
Summary
In this chapter, we saw how to expose JSON HTTP endpoints in Suave and also
learned how to use the Chiron library to deal with JSON.
183
Chapter 17 - Adding User Feed
In the last chapter, we saw to how to persist a new tweet from the user. But after
persisting the tweet, we haven't do anything. In real twitter, we have a user feed,
which shows a timeline with tweets from him/her and from others whom he/she
follows.
In this chapter, we are going to address the first part of user's timeline, viewing
his/her tweets on the Wall page.
As we did for orchestrating the user signup, we need to define a new function which
carries out both of the mentioned operations.
// FsTweet.Web/Tweet.fs
// ...
type Tweet = {
UserId : UserId
Username : Username
Id : TweetId
Post : Post
}
Create a new module Domain in Wall.fs and define a type for notifying the arrival of a
new tweet. Make sure that this new module is above the Suave module
// FsTweet.Web/Wall.fs
module Domain =
open Tweet
open System
open Chessie.ErrorHandling
184
Chapter 17 - Adding User Feed
The NotifyTweet typifies a notify tweet function that takes Tweet and returns either
unit or Exception asynchronously.
Then create a new type PublishTweet to represent the signature of the orchestration
function with its dependencies partially applied.
module Domain =
// ...
open User
// ...
type PublishTweet =
User -> Post -> AsyncResult<TweetId, PublishTweetError>
The SignupUser type that we defined in the orchestrating user signup chapter
has the signature that contains both the dependencies and the actual
parameters. As mentioned there, it was for illustration, and we haven't used it
anywhere.
Here, we are getting rid of the dependencies and using only the parameters
required to specify what we want. Later in the presentation layer, we'll be
using it explicitly like
In the presentation layer of user signup, the similar function has defined like
We don't have the PublishTweetError type defined yet. So, let's add it first.
185
Chapter 17 - Adding User Feed
type PublishTweetError =
| CreateTweetError of Exception
| NotifyTweetError of (TweetId * Exception)
// FsTweet.Web/Wall.fs
module Domain =
// ...
open Chessie
// ...
let! tweetId =
createTweet user.UserId post
|> AR.mapFailure CreateTweetError
let tweet = {
Id = tweetId
UserId = user.UserId
Username = user.Username
Post = post
}
do! notifyTweet tweet
|> AR.mapFailure (fun ex -> NotifyTweetError(tweetId, ex))
return tweetId
}
The publishTweet function is making use of the abstractions that we built earlier and
implements the publish tweet logic.
There is no function implementing the NotifyTweet type yet in our application, and
our next step is adding it.
186
Chapter 17 - Adding User Feed
GetStream.IO
To implement newsfeed and timeline, we are going to use GetStream.
The Stream Framework is an open source solution, which allows you to build
scalable news feed, activity streams, and notification systems.
GetStream.io is the SASS provider of the stream framework and we are going to use
its free plan.
After completing this documentation (roughly take 5-10 minutes), if you navigate to
the dashboard, you can find the following UI component
The GetStream.io creates an application for you to enable the in-browser interactive
guide. Keep an of note the App Id, Key, and Secret. We will be using it shortly while
integrating it.
It also creates some feed groups in the created applications, and we will be using the
user and timeline feed group
187
Chapter 17 - Adding User Feed
If you are not going through the in-browser tutorial in GetStream.io, you have
to create the application and these two feed groups manually.
Configuring GetStream.io
Let's create a new file Stream.fs in the web project
Then add the stream-net NuGet package. stream-net is a .NET library for building
newsfeed and activity stream applications with Getstream.io
// FsTweet.Web/Stream.fs
[<RequireQualifiedAccess>]
module GetStream
type Config = {
ApiSecret : string
ApiKey : string
AppId : string
}
188
Chapter 17 - Adding User Feed
We also need a Client record type to hold the actual GetStream.io client and this
config.
// FsTweet.Web/Stream.fs
// ...
open Stream
type Client = {
Config : Config
StreamClient : StreamClient
}
The final step is creating a new stream client during the application bootstrap.
// FsTweet.Web/FsTweet.Web.fs
// ...
let main argv =
// ...
+
+ let getStreamClient = GetStream.newClient streamConfig
189
Chapter 17 - Adding User Feed
2. Create a new activity of type tweet and add it to the user feed.
To retrieve the user feed of the user, let's add a function userFeed in the Stream.fs.
The hardcoded string "user" is the name of the feed group that we saw earlier
// FsTweet.Web/Stream.fs
// ...
Then in the Wall.fs, create a new module GetStream above Suave module and add a
new function notifyTweet to add a new activity to the user feed.
190
Chapter 17 - Adding User Feed
// FsTweet.Web/Wall.fs
// ...
module GetStream =
open Tweet
open User
open Stream
open Chessie.ErrorHandling
let activity =
new Activity(userId.ToString(), "tweet", tweetId.ToString())
userFeed.AddActivity(activity) // Task<Activity>
|> Async.AwaitTask // Async<Activity>
|> Async.Catch // Async<Choice<Activity,Exception>>
|> Async.map ofChoice // Async<Result<Activity,Exception>>
|> AR // AsyncResult<Activity,Exception>
// ...
The AddActivity function adds an Activity to the user feed and returns
Task<Activity> , and we are transforming it to AsyncResult<Activity,Exception> .
The NotifyTweet type that we defined earlier has the function signature returning
AsyncResult<unit, Exception> but the implemenation function notifyTweet returns
AsyncResult<Activity, Exception> .
So, while transforming, we need to ignore the Activity and map it to unit instead.
To do it add a new function mapStreamResponse
191
Chapter 17 - Adding User Feed
// FsTweet.Web/Wall.fs
// ...
module GetStream =
// ...
open Chessie.ErrorHandling
// ...
...
userFeed.AddActivity(activity) // Task<Activity>
|> Async.AwaitTask // Async<Activity>
|> Async.Catch // Async<Choice<Activity,Exception>>
- |> Async.map ofChoice // Async<Result<Activity,Exception>>
+ |> Async.map GetStream.mapStreamResponse // Async<Result<unit,Exception>>
|> AR // AsyncResult<unit,Exception>
192
Chapter 17 - Adding User Feed
// FsTweet.Web/Wall.fs
module Suave =
+ open Domain
...
For NotifyTweetError , we are just printing the error for simplicity, and assumes
it as fire and forget.
193
Chapter 17 - Adding User Feed
// FsTweet.Web/Wall.fs
module Suave =
...
- let webpart getDataCtx =
+ let webpart getDataCtx getStreamClient =
// ...
choose [
path "/wall" >=> requiresAuth renderWall
POST >=> path "/tweets"
- >=> requiresAuth2 (handleNewTweet createTweet)
+ >=> requiresAuth2 (handleNewTweet publishTweet)
// FsTweet.Web/FsTweet.Web.fs
// ...
- Wall.Suave.webpart getDataCtx
+ Wall.Suave.webpart getDataCtx getStreamClient
]
Now if you run the app with the GetStream environment variables populated with
their correpsonding values and post a tweet, it will be added to the user feed.
194
Chapter 17 - Adding User Feed
Then in the wall.liquid template, add a reference to this getstream.fs file in the
scripts block.
FsTweet.Web/views/user/wall.liquid
{% block scripts %}
+ <script src="/assets/js/lib/getstream.js"> </script>
<script src="/assets/js/wall.js"></script>
{% endblock %}
We are going to use the second option as it is simpler. To enable it we first need to
pass the getStreamClient from the webpart function to the renderWall function.
// FsTweet.Web/Wall.fs
module Suave =
Then we need to extend the WallViewModel to have two more properties and populate
it with the getStreamClient 's config values.
195
Chapter 17 - Adding User Feed
type WallViewModel = {
// ...
ApiKey : string
AppId : string
}
// ...
let renderWall ... =
// ...
let vm = {
// ...
ApiKey = getStreamClient.Config.ApiKey
AppId = getStreamClient.Config.AppId}
// ...
The next step is a populating a javascript object with these values in the wall.liquid
template.
{% block scripts %}
<!-- ... -->
<script type="text/javascript">
window.fsTweet = {
stream : {
appId : "{{model.AppId}}",
apiKey : "{{model.ApiKey}}"
}
}
</script>
<!-- before <script src="/assets/js/wall.js"></script> -->
{% endblock %}
Finally, in the wall.js file, initialize the getstream client with these values.
// src/FsTweet.Web/assets/js/wall.js
$(function(){
// ...
let client =
stream.connect(fsTweet.stream.apiKey, null, fsTweet.stream.appId);
});
196
Chapter 17 - Adding User Feed
As we did for the passing API key and App Id, we first need to extend the view model
with the required properties
// src/FsTweet.Web/Wall.fs
module Suave =
// ...
type WallViewModel = {
// ...
UserId : int
UserFeedToken : string
}
// ...
let userFeed =
GetStream.userFeed getStreamClient userId
let vm = {
// ...
UserId = userId
UserFeedToken = userFeed.ReadOnlyToken
}
// ...
Note: We are passing the ReadOnlyToken as the client side just going to listen
to the new tweet.
197
Chapter 17 - Adding User Feed
{% block scripts %}
<!-- ... -->
<script type="text/javascript">
window.fsTweet = {
user : {
id : "{{model.UserId}}",
name : "{{model.Username}}",
feedToken : "{{model.UserFeedToken}}"
},
// before stream : {
}
</script>
<!-- ... -->
{% endblock %}
On the client side, use these values to initialize the user feed and subscribe to the
new tweet and print to the console.
// src/FsTweet.Web/assets/js/wall.js
$(function(){
// ...
let userFeed =
client.feed("user", fsTweet.user.id, fsTweet.user.feedToken);
userFeed.subscribe(function(data){
console.log(data.new[0])
});
});
Now if you post a tweet, you will get a console log of the new tweet.
198
Chapter 17 - Adding User Feed
The last thing that we need to add is rendering the user wall and put the tweets there
instead of the console log. To do it, first, we need to have a placeholder on the
wall.liquid page.
Then add a new file tweet.js to render the new tweet in the wall.
// src/FsTweet.Web/assets/js/tweet.js
$(function(){
var template = `
<div class="tweet_read_view bg-info">
<span class="text-muted">
@{{tweet.username}} - {{#timeAgo}}{{tweet.time}}{{/timeAgo}}
</span>
<p>{{tweet.tweet}}</p>
</div>
`
});
The renderTweet function takes the parent DOM element and the tweet object as its
inputs.
199
Chapter 17 - Adding User Feed
It generates the HTML elements of the tweet view using Mustache and Moment.js
(for displaying the time). And then it prepends the created HTML elements to the
parents DOM using the jQuery's prepend method.
And then refer the Mustache and Moment.js libraries in the master_page.liquid.
Finally, replace the console log with the call to the renderTweet function.
// src/FsTweet.Web/assets/js/wall.js
...
userFeed.subscribe(function(data){
- console.log(data.new[0]);
+ renderTweet($("#wall"),data.new[0]);
});
})
Now if we tweet, we can see the wall is being populated with the new tweet.
200
Chapter 17 - Adding User Feed
We made it!!
Summary
In this chapter, we learned how to integrate GetStream.io in FsTweet to notify the
new tweets and also added the initial version of user wall.
201
Chapter 18 - Adding User Profile Page
We are on the verge of completing the initial version of FsTweet. To say FsTweet as
a Twitter clone, we should be able to follow other users and view their tweets in our
wall page. To do it, we first need to have a user profile page where we can go and
follow the user.
The components three and four will be addressed in the later chapters.
In addition to it, we also have to address the following three scenarios on the profile
page.
1. Anyone should be able to view a profile of anybody else without logging in to the
application. The anonymous user can only view the page.
202
Chapter 18 - Adding User Profile Page
2. If a logged in user visits another user profile page, he/she should be able to
follow him/her
3. If a logged in user visits his/her profile page, there should not be any provision
to follow himself/herself.
203
Chapter 18 - Adding User Profile Page
To start with we are going to implement the first UI Component, the gravatar image
along with the username and we will also be addressing the above three scenarios.
204
Chapter 18 - Adding User Profile Page
{% extends "master_page.liquid" %}
{% block head %}
<title> {{model.Username}} - FsTweet </title>
{% endblock %}
{% block content %}
<div>
<img src="{{model.GravatarUrl}}" alt="" class="gravatar" />
<p class="gravatar_name">@{{model.Username}}</p>
{% if model.IsLoggedIn %}
{% unless model.IsSelf %}
<a href="#" id="follow">Follow</a>
{% endunless %}
<a href="/logout">Logout</a>
{% endif %}
</div>
<p id="followingCount"/><div id="following"/>
<p id="followersCount"/><div id="followers"/>
{% endblock %}
We are using two boolean properties IsLoggedIn and IsSelf to show/hide the UI
elements that we saw above.
The next step is adding the server side logic to render this template.
205
Chapter 18 - Adding User Profile Page
// src/FsTweet.Web/UserProfile.fs
namespace UserProfile
module Domain =
open User
type UserProfile = {
User : User
GravatarUrl : string
IsSelf : bool
}
Then add the gravatarUrl function that creates the gravatar URL from the user's
email address.
module Domain =
// ...
open System.Security.Cryptography
// ...
The next step is adding the findUserProfile function, which finds the user profile by
username.
206
Chapter 18 - Adding User Profile Page
If the Username of the logged in user matches with the Username that we are looking
to find, we don't need to call the findUserProfile . Instead, we can use the User
value that we get from the session cookie and then call newProfile function with the
logged in user to get the profile and modify its IsSelf property to true .
open Chessie.ErrorHandling
// ...
type FindUserProfile =
Username -> User option
-> AsyncResult<UserProfile option, System.Exception>
We are making use of the findUser function that we created while handling user
login request.
Now we have the domain logic for finding user profile in place and let's turn our
attention to the presentation logic!
As we did for other pages, create a new module Suave and define a view model for
the profile page.
207
Chapter 18 - Adding User Profile Page
// src/FsTweet.Web/UserProfile.fs
namespace UserProfile
//...
module Suave =
type UserProfileViewModel = {
Username : string
GravatarUrl : string
IsLoggedIn : bool
IsSelf : bool
}
module Suave =
open Domain
// ...
// src/FsTweet.Web/UserProfile.fs
// ...
module Suave =
// ...
open Suave.DotLiquid
open Chessie
open System
open User
// ...
208
Chapter 18 - Adding User Profile Page
The final step is exposing this function and adding an HTTP route.
209
Chapter 18 - Adding User Profile Page
// src/FsTweet.Web/UserProfile.fs
// ...
module Suave =
// ...
open Database
open Suave.Filters
open Auth.Suave
// ...
// FsTweet.Web/FsTweet.Web.fs
// ...
let main argv =
// ...
let app =
choose [
// ...
+ UserProfile.Suave.webpart getDataCtx
]
// ...
We need to make sure that this webpart should be the last item in the choose
list as the path /%s matches every path that has this pattern.
To test drive this new feature, run the application and view the user profile as an
anonymous user. Then signup some new users (make sure you verify their email id)
and then log in and see other users profile.
We haven't added the log out yet. So, to log in as a new user either clear the
cookies in the browser or restart your browser.
210
Chapter 18 - Adding User Profile Page
To enable it we have to pass the GetStream.io's configuration and user details to the
client side. Let's add them as properties in the UserProfileViewModel .
// src/FsTweet.Web/UserProfile.fs
// ...
module Suave =
// ...
type UserProfileViewModel = {
// ...
UserId : int
UserFeedToken : string
ApiKey : string
AppId : string
}
// ...
+ let newUserProfileViewModel
+ (getStreamClient : GetStream.Client) (userProfile : UserProfile) =
+
+ let (UserId userId) = userProfile.User.UserId
+ let userFeed = GetStream.userFeed getStreamClient userId
+ {
+ Username = userProfile.User.Username.Value
+ GravatarUrl = userProfile.GravatarUrl
+ IsLoggedIn = false
+ IsSelf = userProfile.IsSelf
+ UserId = userId
+ UserFeedToken = userFeed.ReadOnlyToken
+ ApiKey = getStreamClient.Config.ApiKey
+ AppId = getStreamClient.Config.AppId
+ }
211
Chapter 18 - Adding User Profile Page
The final step is passing the getStreamClient from the application's main function.
// FsTweet.Web/FsTweet.Web.fs
// ...
let main argv =
// ...
let app =
choose [
// ...
- UserProfile.Suave.webPart getDataCtx
+ UserProfile.Suave.webPart getDataCtx getStreamClient
]
// ...
With this, we are done with the server side changes for showing a user feed in the
user profile page.
212
Chapter 18 - Adding User Profile Page
Then as we did in the last chapter, define a scripts block and pass the
GetStream.io's initialization values to the client side.
{% block scripts %}
<script src="/assets/js/lib/getstream.js"> </script>
<script type="text/javascript">
window.fsTweet = {
user : {
id : "{{model.UserId}}",
name : "{{model.Username}}",
feedToken : "{{model.UserFeedToken}}"
},
stream : {
appId : "{{model.AppId}}",
apiKey : "{{model.ApiKey}}"
}
}
</script>
<script src="/assets/js/tweet.js"></script>
<script src="/assets/js/profile.js"></script>
{% endblock %}
The profile.js that we are referring here is not added yet. So, let's add it
213
Chapter 18 - Adding User Profile Page
// assets/js/profile.js
$(function(){
let client =
stream.connect(fsTweet.stream.apiKey, null, fsTweet.stream.appId);
let userFeed =
client.feed("user", fsTweet.user.id, fsTweet.user.feedToken);
userFeed.get({
limit: 25
}).then(function(body) {
$(body.results.reverse()).each(function(index, tweet){
renderTweet($("#tweets"), tweet);
});
})
});
The code is straight-forward, we are initializing the GetStream.io's client and the user
feed. And then we are retrieving the last 25 tweets of the user.
Awesome!.
Now if we run the app and visits a user profile, we can see his/her tweets!
Summary
In this chapter, we implemented the user profile page with the help of the
abstractions that we built earlier. Then we added the logout functionality.
214
Chapter 19 - Following a User
So, as part of this feature implementation, let's get started with implementing log out.
Let's add a new path /logout in Auth.fs and handle the logout request as
mentioned.
// src/FsTweet.Web/Auth.fs
...
module Suave =
...
Following A User
Let's get started by creating a new file Social.fs in the FsTweet.Web project and
move it above UserProfile.fs
215
Chapter 19 - Following a User
As we did for the other features, let's add a Domain module and orchestrate this
functionality.
// FsTweet.Web/Social.fs
namespace Social
module Domain =
open System
open Chessie.ErrorHandling
open User
The CreateFollowing and the Subscribe types represent the function signatures of
the two tasks that we need to do while following a user.
The next step is defining functions which implement these two functionalities.
216
Chapter 19 - Following a User
// src/FsTweet.Db.Migrations/FsTweet.Db.Migrations.fs
// ...
override this.Up() =
base.Create.Table("Social")
.WithColumn("Id").AsGuid().PrimaryKey().Identity()
.WithColumn("FollowerUserId").AsInt32().ForeignKey("Users", "Id").NotNullable()
.WithColumn("FollowingUserId").AsInt32().ForeignKey("Users", "Id").NotNullable()
|> ignore
base.Create.UniqueConstraint("SocialRelationship")
.OnTable("Social")
.Columns("FollowerUserId", "FollowingUserId") |> ignore
override this.Down() =
base.Delete.Table("Tweets") |> ignore
Then run the build script command forge fake RunMigrations command to creates this
database table
Make sure to verify the underlying schema after running the build script.
The next step is defining the function which persists the social connection in this
table.
Create a new module Persistence in the Social.fs file and define the createFollowing
function as below
217
Chapter 19 - Following a User
// FsTweet.Web/Social.fs
// ...
module Persistence =
open Database
open User
// GetDataContext -> User -> UserId -> AsyncResult<unit, Exception>
let createFollowing (getDataCtx : GetDataContext) (user : User) (UserId userId) =
let ctx = getDataCtx ()
let social = ctx.Public.Social.Create()
let (UserId followerUserId) = user.UserId
social.FollowerUserId <- followerUserId
social.FollowingUserId <- userId
submitUpdates ctx
We are using the term follower to represent the current logged in user and the
following user to represent the user that the logged in user about to follow.
// src/FsTweet.Web/Stream.fs
let timeLineFeed getStreamClient (userId : int) =
getStreamClient.StreamClient.Feed("timeline", userId.ToString())
The hard coded string "timeline" is the name of the feed group that we
created (or GetStream.io created for us) in the seventeenth chapter.
As we did for notifying a new tweet, let's create a new module GetStream and add
the subscribe function.
218
Chapter 19 - Following a User
// FsTweet.Web/Social.fs
// ...
module GetStream =
open User
open Chessie
let timelineFeed =
GetStream.timeLineFeed getStreamClient followerUserId
let userFeed =
GetStream.userFeed getStreamClient userId
timelineFeed.FollowFeed(userFeed) // Task
|> Async.AwaitTask // Async<uint>
|> AR.catch // AsyncResult<unit, Exception>
Let's start with defining the sample JSON that the follow user endpoint should
support.
{
"userId" : 123
}
219
Chapter 19 - Following a User
// FsTweet.Web/Social.fs
// ...
module Suave =
open Chiron
// FsTweet.Web/Social.fs
// ...
module Suave =
// ...
open Suave
// ...
let onFollowUserSuccess () =
Successful.NO_CONTENT
Then we have to define the request handler which handles the request to follow the
user.
220
Chapter 19 - Following a User
module Suave =
// ...
open Domain
open User
open Chessie
// ...
The last piece is wiring this handler with the /follow endpoint.
// FsTweet.Web/Social.fs
// ...
module Suave =
// ...
open Suave.Filters
open Persistence
open Domain
open Suave.Operators
open Auth.Suave
// ...
221
Chapter 19 - Following a User
// FsTweet.Web/FsTweet.Web.fs
// ...
let main argv =
// ...
let app =
choose [
// ...
+ Social.Suave.webpart getDataCtx getStreamClient
UserProfile.Suave.webPart getDataCtx getStreamClient
]
// ...
To follow a user, we need his/her user id. To retrieve it on the client-side, let's add a
data attribute to the follow button in the profile.liquid template.
// views/user/profile.liquid
- <a id="follow">Follow</a>
+ <a id="follow" data-user-id="{{model.UserId}}">Follow</a>
We are already having the user id of the profile being viewed as a global
variable fsTweet.user.id in the JS side. This approach is to demonstrate
another method to share data between client and server.
Then add a new javascript file social.js which handles the client side activities for
following a user.
222
Chapter 19 - Following a User
// assets/js/social.js
$(function(){
$("#follow").on('click', function(){
var $this = $(this);
var userId = $this.data('user-id');
$this.prop('disabled', true);
$.ajax({
url : "/follow",
type: "post",
data: JSON.stringify({userId : userId}),
contentType: "application/json"
}).done(function(){
alert("successfully followed");
$this.prop('disabled', false);
}).fail(function(jqXHR, textStatus, errorThrown) {
console.log({
jqXHR : jqXHR,
textStatus : textStatus,
errorThrown: errorThrown});
alert("something went wrong!")
});
});
});
This javascript snippet fires an AJAX POST request with the user id using jQuery
upon clicking the follow button and it shows an alert for both success and failure
cases of the response.
<script src="/assets/js/profile.js"></script>
+ <script src="/assets/js/social.js"></script>
{% endblock %}
That's it! We can follow a user, by clicking the follow button in his/her profile page.
223
Chapter 19 - Following a User
Ideally, it should display the user's timeline where he/she can see the tweets from
his/her followers. And also, we need a real-time update when the timeline receives a
new tweet from the follower.
GetStream.io's javascript client library already supports these features. So, we just
have to enable it.
As a first step, in addition to passing the user feed token, we have to share the
timeline token. Update the view model of the Wall page with a new property
TimelineToken and update this property with the read-only token of the user's timeline
feed.
// src/FsTweet.Web/Wall.fs
...
type WallViewModel = {
...
+ TimelineToken : string
...}
...
+
+ let timeLineFeed =
+ GetStream.timeLineFeed getStreamClient userId
let vm = {
...
+ TimelineToken = timeLineFeed.ReadOnlyToken
...}
To pass this Timeline token with the javascript code, add a new property
timelineToken in the fsTweet.user object in the wall.liquid template.
<script type="text/javascript">
window.fsTweet = {
user : {
...
- feedToken : "{{model.UserFeedToken}}"
+ feedToken : "{{model.UserFeedToken}}",
+ timelineToken : "{{model.TimelineToken}}"
},
stream : {...}
}
224
Chapter 19 - Following a User
The last step is initializing a timeline feed using this token and subscribe to it.
// assets/js/wall.js
$(function(){
// ...
let timelineFeed =
client.feed("timeline", fsTweet.user.id, fsTweet.user.timelineToken);
timelineFeed.subscribe(function(data){
renderTweet($("#wall"),data.new[0]);
});
});
This would update the wall page when the timeline feed receives a new tweet.
To have the wall page with a populate timeline, we need to fetch the tweets from the
timeline feed just like what we did for getting the user's tweet on the user profile
page.
// assets/js/wall.js
$(function(){
// ...
timelineFeed.get({
limit: 25
}).then(function(body) {
$(body.results.reverse()).each(function(index, tweet){
renderTweet($("#wall"), tweet);
});
});
});
In GetStream.io, the timeline feed of a user will not have the user's tweets. So, the
populated wall page here will not have user's tweet. To show both the user's tweets
and his/her timeline tweets, we can fetch the user's tweets as well and merge both
the feeds and then sort with time.
225
Chapter 19 - Following a User
// assets/js/wall.js
$(function(){
// ...
timelineFeed.get({
limit: 25
}).then(function(body) {
var timelineTweets = body.results
userFeed.get({
limit : 25
}).then(function(body){
var userTweets = body.results
var allTweets = $.merge(timelineTweets, userTweets)
allTweets.sort(function(t1, t2){
return new Date(t2.time) - new Date(t1.time);
})
$(allTweets.reverse()).each(function(index, tweet){
renderTweet($("#wall"), tweet);
});
})
})
});
Cool!
Now run the app, open two browser windows, log in as two different users and follow
the other user.
After following the other user, you can get the live updates.
We made it!
As we have added support for following a user, while rendering the user profile page,
we can now check whether the logged in user follows the given user or not and show
either the follow button or following button accordingly.
226
Chapter 19 - Following a User
To enable this, let's get add a new type UserProfileType to represent all the three
possible cases while serving the user profile page.
// src/FsTweet.Web/UserProfile.fs
// ...
type UserProfileType =
| Self
| OtherNotFollowing
| OtherFollowing
// ...
Then we need to use this type in the place of the IsSelf property.
type UserProfile = {
User : User
GravatarUrl : string
- IsSelf : bool
+ UserProfileType : UserProfileType
}
Now we are getting a set of compiler warnings, showing us the directions of the
places where we have to go and fix this property change.
The first place that we need to fix is the newProfile function. Let's change it to
accept a one more parameter userProfileType and use it to set UserProfileType of
the new user profile.
Then in the places where we are calling this newProfile function, pass the
appropriate user profile type.
227
Chapter 19 - Following a User
For an anonymous user, the user profile will always be other whom he/she is not
following. But for a logged in user who is viewing an another user's profile, we need
to check the Social table and set the type to either OtherNotFollowing or
OtherFollowing .
Let's keep it as OtherNotFollowing for the time being and we'll implement this check
shortly.
The next place that we need to fix is where we are populating the
UserProfileViewModel . To do it, we first have to add a new property IsFollowing in the
view model.
type UserProfileViewModel = {
// ...
IsFollowing : bool
}
And then in the newUserProfileViewModel function, populate this and the IsSelf
228
Chapter 19 - Following a User
{
// ...
IsSelf = isSelf
IsFollowing = isFollowing
}
Now we are right except the following check. The last piece that we need to change
before implementing this check is updating the profile.liquid show either follow or
following link based on the IsFollowing property.
{% unless model.IsSelf %}
- <a href="#" id="follow">Follow</a>
+ {% if model.IsFollowing %}
+ <a href="#" id="unfollow">Following</a>
+ {% else %}
+ <a href="#" id="follow" data-user-id="{{model.UserId}}">Follow</a>
+ {% endif %}
{% endunless %}
// src/FsTweet.Web/Social.fs
module Domain =
// ...
type IsFollowing =
User -> UserId -> AsyncResult<bool, Exception>
// ...
229
Chapter 19 - Following a User
With this type in place, we can now change the findUserProfile to accept a new
parameter isFollowing of this type and use it to figure out the actual
UserProfileType .
module Domain =
// ...
open Social.Domain
// ...
let findUserProfile
... (isFollowing : IsFollowing) ... = asyncTrial {
match loggedInUser with
| None -> // ...
| Some (user : User) ->
// ...
else
// ...
// return Option.map (newProfile OtherNotFollowing) userMayBe
match userMayBe with
| Some otherUser ->
let! isFollowingOtherUser =
isFollowing user otherUser.UserId
let userProfileType =
if isFollowingOtherUser then
OtherFollowing
else OtherNotFollowing
let userProfile =
newProfile userProfileType otherUser
return Some userProfile
| None -> return None
}
230
Chapter 19 - Following a User
// src/FsTweet.Web/Social.fs
// ...
module Persistence =
// ...
open Chessie.ErrorHandling
open FSharp.Data.Sql
open Chessie
// ...
let! connection =
query {
for s in ctx.Public.Social do
where (s.FollowerUserId = followerUserId &&
s.FollowingUserId = userId)
} |> Seq.tryHeadAsync |> AR.catch
return connection.IsSome
}
// ...
The logic is straight-forward, we retrieve the social connection by providing both the
follower user id and following user's user id. If the relationship exists we return true ,
else we return false .
Then we need to pass this function after partially applied the first parameter
( getDataCtx ) to the findUserProfile function.
+ open Social
// ...
let webpart (getDataCtx : GetDataContext) getStreamClient =
let findUser = Persistence.findUser getDataCtx
- let findUserProfile = findUserProfile findUser
+ let isFollowing = Persistence.isFollowing getDataCtx
+ let findUserProfile = findUserProfile findUser isFollowing
// ...
That's it. Now if we run the application and views a profile that we are following, we
will be seeing the following button instead of the follow button.
231
Chapter 19 - Following a User
Summary
We covered a lot of ground in this chapter. We started with adding log out and then
we moved to adding support for following the user. Then we updated the wall page
to show the timeline, and finally we revisited the user profile page to reflect the social
connection status.
Exercise
It'd be great if we can get an email notification when someone follows us in
FsTweet.
232
Chapter 20 - Fetching Followers and Following Users
In this chapter, we are going to expose two HTTP JSON endpoints to fetch the list of
followers and following users. Then we will be updating the user profile page front-
end to consume these APIs and populate the Following and Followers Tabs.
// src/FsTweet.Web/Social.fs
module Domain =
// ...
type FindFollowers =
UserId -> AsyncResult<User list, Exception>
// ...
233
Chapter 20 - Fetching Followers and Following Users
The implementation function of this type will be leveraging the composable queries
concept to find the given user id's followers
// src/FsTweet.Web/Social.fs
// ...
module Persistence =
// ...
open System.Linq
// ...
let! followers =
query {
for u in ctx.Public.Users do
where (selectFollowersQuery.Contains(u.Id))
select u
} |> Seq.executeQueryAsync |> AR.catch
return followers
}
Using the selectFollowersQuery , we are first getting the list of followers user ids. Then
we are using these identifiers to the get corresponding user details.
Like what we did for finding the user by username, we need to map the all the user
entities in the sequence to their respective domain model.
234
Chapter 20 - Fetching Followers and Following Users
To do it, we first need to extract the mapping functionality from the mapUser function.
// src/FsTweet.Web/User.fs
// ...
module Persistence =
// ...
open System
This extracted method returns Result<User, Exception> and then modify the mapUser
module Persistence =
// ...
let mapUserEntityToUser ... = ...
// ...
235
Chapter 20 - Fetching Followers and Following Users
I just noticed that the name mapUser misleading. So, rename it to mapUserEntity to
communicate what it does.
module Persistence =
...
- let mapUser (user : DataContext.``public.UsersEntity``) =
+ let mapUserEntity (user : DataContext.``public.UsersEntity``) =
...
Exception> .
As Chessie library already supports the failure side of the Result as a list, we
don't specify the failure side as Exception list .
We are not done yet as the failure side is still a list of Exception but what we want is
single Exception. .NET already supports this through AggregateException.
236
Chapter 20 - Fetching Followers and Following Users
Using the mapFailure function from Chessie, we are transforming the list of
exceptions into an AggregateException , and then we are mapping it to AsyncResult<User
list, Exception> .
With this mapUserEntities function in place, we can now return a User list in the
findFollowers function
module Persistence =
...
+ open User.Persistence
...
The JSON response that we are going to send will have the following structure
{
"users": [
{
"username": "tamizhvendan"
}
]
}
237
Chapter 20 - Fetching Followers and Following Users
To model the corresponding server-side representation of this JSON object, let's add
some types along with the static member function ToJson which is required by the
Chiron library to serialize the type to JSON.
// src/FsTweet.Web/Social.fs
// ...
module Suave =
// ...
type UserDto = {
Username : string
} with
static member ToJson (u:UserDto) =
json {
do! Json.write "username" u.Username
}
// ...
To expose the findFollowers function as an HTTP API, we first need to specify what
we need to do for both success and failure.
module Suave =
// ...
let onFindUsersFailure (ex : System.Exception) =
printfn "%A" ex
JSON.internalError
let onFindUsersSuccess (users : User list) =
mapUsersToUserDtoList users
|> Json.serialize
|> JSON.ok
// ...
238
Chapter 20 - Fetching Followers and Following Users
Then add a new function which handles the request for fetching user's followers
// src/FsTweet.Web/Social.fs
module Domain =
// ...
type FindFollowingUsers = UserId -> AsyncResult<User list, Exception>
module Persistence =
// ...
let findFollowingUsers (getDataCtx : GetDataContext) (UserId userId) = asyncTrial {
let ctx = getDataCtx()
239
Chapter 20 - Fetching Followers and Following Users
let! followingUsers =
query {
for u in ctx.Public.Users do
where (selectFollowingUsersQuery.Contains(u.Id))
select u
} |> Seq.executeQueryAsync |> AR.catch
In the selectFollowingUsersQuery , we are selecting the list of user ids that are being
followed by the provided user id.
module Suave =
// ...
240
Chapter 20 - Fetching Followers and Following Users
Updating UI
To consume these two APIs and to render it on the client side, we need to update
the social.js
// src/FsTweet.Web/assets/js/social.js
$(function(){
// ...
var usersTemplate = `
{{#users}}
<div class="well user-card">
<a href="/{{username}}">@{{username}}</a>
</div>
{{/users}}`;
(function loadFollowers () {
var url = "/" + fsTweet.user.id + "/followers"
$.getJSON(url, function(data){
renderUsers(data, $("#followers"), $("#followersCount"))
})
})();
(function loadFollowingUsers() {
var url = "/" + fsTweet.user.id + "/following"
$.getJSON(url, function(data){
renderUsers(data, $("#following"), $("#followingCount"))
})
})();
});
Using jQuery's getJSON function, we are fetching the JSON object and then
rendering it using Mustache template.
That's it!
241
Chapter 20 - Fetching Followers and Following Users
Summary
In this chapter, we have exposed two HTTP APIs to retrieve the list of followers and
following users.
With this, we are done with all the features that will be part of this initial version of
FsTweet. In the upcoming posts, we are going to add support for logging and learn
how to deploy it to Azure.
242
Chapter 21 - Deploying to Azure App Service
In this chapter, we are going to prepare our code for deployment, and then we'll be
deploying our FsTweet Application on Azure using Azure App Service.
[<Literal>]
let private connString =
"Server=127.0.0.1;Port=5432;Database=FsTweet;User Id=postgres;Password=test;"
In other words, we need a live database (with schemas defined) to compile the
FsTweet.
In our build script, we are running the migration script to create/modify the tables
before the compilation of the application. So, we don't need to worry about the
database schema.
// src/FsTweet.Web/FsTweet.Web.fs
// ...
let main argv =
// ...
let fsTweetConnString =
Environment.GetEnvironmentVariable "FSTWEET_DB_CONN_STRING"
// ...
let getDataCtx = dataContext fsTweetConnString
// ...
The real concern is if we are going with the current code as it is while compiling the
code on a cloud machine, that machine has to have a local Postgres database which
can be accessed using the above connection string literal.
243
Chapter 21 - Deploying to Azure App Service
We can have a separate database (accessible from anywhere) for this purpose
alone and uses that as a literal. But there are a lot of drawbacks!
Now we need to maintain two databases, one for compilation and another one
for running in production.
We also need to makes sure that the database schema should be same in both
the databases.
It's lot of work(!) for a simple task! So, this approach is not practical.
Before arriving at the solution, Let's think about what would be an ideal scenario.
The first step is manual, and our FAKE build script is already taking care of rest of
the steps.
Later in this chapter, We'll be adding a separate step in our build script to
deploy the application on cloud.
To make this ideal scenario work, we need an intermediate step between three and
four, which takes the connection string from the environment variable and replaces
the connection string literal in Db.fs with this one. After successful compilation, we
need to revert this change.
It's super easy with our build script. Let's make it work!
We are already having the local connection string in the build script which we are
using if there is no value in the FSTWEET_DB_CONN_STRING environment variable.
let connString =
environVarOrDefault
"FSTWEET_DB_CONN_STRING"
@"Server=127.0.0.1;Port=5432;Database=FsTweet;User Id=postgres;Password=test;"
244
Chapter 21 - Deploying to Azure App Service
Let's extract this out and define a binding for this value
+ let localDbConnString =
+ @"Server=127.0.0.1;Port=5432;Database=FsTweet;User Id=postgres;Password=test;"
let connString =
environVarOrDefault
"FSTWEET_DB_CONN_STRING"
- @"Server=127.0.0.1;Port=5432;Database=FsTweet;User Id=postgres;Password=test;"
+ localDbConnString
Then add a build target, to verify the presence of this connection string in the Db.fs
file.
// build.fsx
// ...
let dbFilePath = "./src/FsTweet.Web/Db.fs"
We are adding this target, to ensure that the local database connection string that we
have it here is same as that of in Db.fs file before replacing it.
Let's define a helper function swapDbFileContent , which swaps the connection string
// build.fsx
// ...
let swapDbFileContent (oldValue: string) (newValue : string) =
let dbFileContent = System.IO.File.ReadAllText dbFilePath
let newDbFileContent = dbFileContent.Replace(oldValue, newValue)
System.IO.File.WriteAllText(dbFilePath, newDbFileContent)
// ...
Then add two targets in the build target, one to change the connection string and
another one to revert the change.
245
Chapter 21 - Deploying to Azure App Service
// build.fsx
// ...
Target "ReplaceLocalDbConnStringForBuild" (fun _ ->
swapDbFileContent localDbConnString connString
)
Target "RevertLocalDbConnStringChange" (fun _ ->
swapDbFileContent connString localDbConnString
)
// ...
As the last step, alter the build order to leverage the targets that we created just now.
// Build order
"Clean"
==> "BuildMigrations"
==> "RunMigrations"
+ ==> "VerifyLocalDbConnString"
+ ==> "ReplaceLocalDbConnStringForBuild"
==> "Build"
+ ==> "RevertLocalDbConnStringChange"
==> "Views"
==> "Assets"
==> "Run"
That's it!
When we compile our application using F# 4.0 compiler, we'll get a compiler error
...\FsTweet.Web\Json.fs(17,41):
Unexpected identifier in type constraint.
Expected infix operator, quote symbol or other token.
246
Chapter 21 - Deploying to Azure App Service
If you check out the release notes of F# 4.1, you can find there are some
improvements made on Statically Resolved Type Parameter support to fix this error
(or bug).
Fortunately, rest of codebase is intact with F# 4.0, and we just need to fix this one.
As a first step, comment out the deserialize function in the JSON module and then
add the following new implementation.
// src/FsTweet.Web/Json.fs
// ...
// Json -> Choice<'a, string> -> HttpRequest -> Result<'a, string>
let deserialize tryDeserialize req =
parse req
|> bind (fun json -> tryDeserialize json |> ofChoice)
This new version of the deserialize is similar to the old one except that we are going
to get the function Json.tryDeserialize as a parameter ( tryDeserialize ) instead of
using it directly inside the function.
Then we have to update the places where this function is being used
// src/FsTweet.Web/Social.fs
...
let handleFollowUser (followUser : FollowUser) (user : User) ctx = async {
- match JSON.deserialize ctx.request with
+ match JSON.deserialize Json.tryDeserialize ctx.request with
...
// src/FsTweet.Web/Wall.fs
...
let handleNewTweet publishTweet (user : User) ctx = async {
- match JSON.deserialize ctx.request with
+ match JSON.deserialize Json.tryDeserialize ctx.request with
...
247
Chapter 21 - Deploying to Azure App Service
Http Bindings
We are currently using default HTTP bindings provided by Suave. So, when we run
our application locally, the web server will be listening on the default port 8080 .
But when we are running it on Azure or any other cloud vendor, we have to use the
port providied by them.
In addition to that, the default HTTP binding uses the loopback address 127.0.0.1
// src/FsTweet.Web/FsTweet.Web.fs
// ...
open System.Net
// ...
let main argv =
// ...
+ let port =
+ Environment.GetEnvironmentVariable "PORT"
+ let httpBinding =
+ HttpBinding.create HTTP ipZero (uint16 port)
let serverConfig =
{defaultConfig with
- serverKey = serverKey}
+ serverKey = serverKey
+ bindings=[httpBinding]}
We are getting the port number to listen to from the environment variable PORT and
modifying the defaultConfig to use the custom HTTP binding instead of the default
one.
In Azure App Service, the port to listen is already available in the environment
variable HTTP_PLATFORM_PORT . But we are using PORT here to avoid cloud
vendor specific stuff in the codebase. Later via configuration (outside the
248
Chapter 21 - Deploying to Azure App Service
Create a new file web.config in the root directory and update it as below
<handlers>
<remove name="httpplatformhandler" />
<add
name="httpplatformhandler"
path="*"
verb="*"
modules="httpPlatformHandler"
resourceType="Unspecified"
/>
</handlers>
<httpPlatform
stdoutLogEnabled="true"
startupTimeLimit="20"
processPath="%HOME%\site\wwwroot\FsTweet.Web.exe"
>
<environmentVariables>
<environmentVariable name="PORT" value="%HTTP_PLATFORM_PORT%" />
</environmentVariables>
</httpPlatform>
</system.webServer>
</configuration>
Most of the above content was copied from the documentation, and we have
modified the following.
249
Chapter 21 - Deploying to Azure App Service
The FAKE library provides a kuduSync function which copies with semantic
appropriate for deploying website files. Before calling kuduSync , we need to stage
the files (in a temporary directory) that has to be copied. This staging directory path
can be retrieved from the FAKE Library's deploymentTemp binding. Then the kuduSync
The deploymentTemp directory is the exact replica of our local build directory on the
delpoyment side. So, instead of staging the files explicitly, we can use this directory
as the build directory. Another benefit is user account which will be deploying has full
access to this directory.
To do the deployment from our build script, we first need to know what is the
environment that we are in through the environment variable FSTWEET_ENVIRONMENT .
// build.fsx
// ...
open Fake.Azure
250
Chapter 21 - Deploying to Azure App Service
// build.fsx
// ...
- // Directories
- let buildDir = "./build/"
+ let buildDir =
+ if env = "dev" then
+ "./build"
+ else
+ Kudu.deploymentTemp
// ...
// build.fsx
// ...
// ...
The last thing that we need to revisit in the build script is the build order.
We need two build orders. One to run the application locally (which we already have)
and another one to deploy. In the latter case, we don't need to run the application
explicitly as Azure Web App takes cares of executing our application using the
web.config file.
251
Chapter 21 - Deploying to Azure App Service
To make it possible, Replace the existing build order with the below one
// build.fsx
// ...
// Build order
"Clean"
==> "BuildMigrations"
==> "RunMigrations"
==> "VerifyLocalDbConnString"
==> "ReplaceLocalDbConnStringForBuild"
==> "Build"
==> "RevertLocalDbConnStringChange"
==> "Views"
==> "Assets"
"Assets"
==> "Run"
"Assets"
==> "CopyWebConfig"
==> "Deploy"
Now we have two different Target execution hierarchy. Refer this detailed
documentation to know how the order hierarchy works in FAKE.
Using this file, we can specify what command to run to deploy the application in
Azure App Service.
Let's create this file in the application's root directory and update it to invoke the build
script.
252
Chapter 21 - Deploying to Azure App Service
[config]
command = build.cmd Deploy
With this, we completed all the coding changes that are required to perform the
deployment.
Create a new free database instance in ElephantSQL and note down its credentials
to pass it as a connection string to our application.
GetStream.io Setup
Next thing that we need to set up is GetStream.io as we can't use the one that we
used during development.
253
Chapter 21 - Deploying to Azure App Service
254
Chapter 21 - Deploying to Azure App Service
After creation keep a note of the App Id, Key, and Secret
Postmark Setup
255
Chapter 21 - Deploying to Azure App Service
Regarding Postmark, we don't need to create a new server account as we are not
using it in the development environment.
However, we have to modify the signup email template to the use the URL of the
deployed application instead of the localhost URL.
- http://localhost:8080/signup/verify/{{ verification_code }}
+ http://fstweet.azurewebsites.net/signup/verify/{{ verification_code }}
To deploy the application, we are going to use Azure CLI. It offers a convenient way
to manage Azure resource easily from the command line.
Make sure you are having this CLI installed in your machine as well as an active
Azure Subscription
The first thing that we have to do is Log in to our Azure account from Azure CLI.
There are multiple ways we can log in and authenticate with the Azure CLI and here
we are going to use the Interactive log-in option.
Run the login command and then in the web browser go the given URL and enter the
provided code.
> az login
To sign in, use a web browser to open the page https://aka.ms/devicelogin and
enter the code H2ABMSZR3 to authenticate
Upon successful login, you will get a similar JSON as the output in the command
prompt.
[
{
"cloudName": "AzureCloud",
"id": "900b4d47-d0c4-888a-9e6d-000061c82010",
256
Chapter 21 - Deploying to Azure App Service
"isDefault": true,
"name": "...",
"state": "Enabled",
"tenantId": "9f67d6b5-5cb4-8fc0-a5cc-345f9cd46e7a",
"user": {
"name": "...",
"type": "user"
}
}
]
A deployment user is required for doing local git deployment to a web app.
{
"id": null,
"kind": null,
"name": "web",
"publishingPassword": null,
"publishingPasswordHash": null,
"publishingPasswordHashSalt": null,
"publishingUserName": "fstdeployer",
"type": "Microsoft.Web/publishingUsers/web",
"userName": null
}
A resource group is a logical container into which Azure resources like web apps,
databases, and storage accounts are deployed and managed.
You can get a list of all the locations available using the az appservice list-
locations command
{
"id": "/subscriptions/{id}/resourceGroups/fsTweetResourceGroup",
"location": "centralus",
257
Chapter 21 - Deploying to Azure App Service
"managedBy": null,
"name": "fsTweetResourceGroup",
"properties": {
"provisioningState": "Succeeded"
},
"tags": null
}
To host our application in Azure App Service, we first need to have an Azure App
Service Plan
Let's creates an App Service plan named fsTweetServicePlan in the Free pricing tier
{
"name": "fsTweetServicePlan",
"provisioningState": "Succeeded",
"resourceGroup": "fsTweetResourceGroup",
"sku": {
...
"tier": "Free"
},
"status": "Ready",
...
}
Then using the az webapp create command, create a new web app in the App
Service.
258
Chapter 21 - Deploying to Azure App Service
The --deployment-local-git flag, creates a remote git directory for the web app
and we will be using it to push our local git repository and deploy the changes.
Note down the URL of the git repository as we'll be using it shortly.
We’ve created an empty web app, with git deployment enabled. If you visit the
http://fstweet.azurewebsites.net/ site now, we can see a blank web app page.
We are just two commands away from deploying our application in Azure.
The FsTweet Application uses a set of environment variables to get the application's
configuration parameters (Connection string, GetStream secret, etc.,). To make
these environment variables available for the application, we can leverage the App
Settings.
Open the Azure Portal, Click App Services on the left and then click fstweet from the
list.
In the fstweet app service dashboard, click on Application Settings and enter all the
required configuration parameters and don't forget to click the Save button!
259
Chapter 21 - Deploying to Azure App Service
Add the git URL that we get after creating the web app as git remote
This command assumes that the project directory is under git version control.
If you haven't done it yet, use the following commands to setup the git
repository
git init
git add -A
git commit -m "initial commit"
The last step is pushing our local git repository to the azure (alias of the remote git
repository). It will prompt you to enter the password. Provide the password that we
used to create the deployment user.
260
Chapter 21 - Deploying to Azure App Service
Now if you browse the site, we can see the beautiful landing page :)
261
Chapter 21 - Deploying to Azure App Service
Post deployment, if we want to make any change, just do a git commit. After making
the changes and push it to the remote as we did now!
262
Chapter 21 - Deploying to Azure App Service
Summary
In this chapter, we have made changes to the codebase to enable the deployment
and deployed our application on Azure using Azure CLI.
263
Chapter 22 - Adding Logs using Logary
In this chapter, we are going to improve how we are logging the exceptions in
FsTweet.
We'll get the above error if the internet connection is down when we post a tweet.
And the code that is logging this exception looks like this
Introducing Logary
Logary is a high-performance, semantic logging, health, and metrics library for .Net.
It enables us to log what happened in the application in a meaningful way which in
turn help us a lot in analyzing them.
264
Chapter 22 - Adding Logs using Logary
At the time of this writing, there is an incompatibility issue with NodaTime version 2.0
in Logary and the NodaTime version which works well with Logary is 1.3.2 .
So, before adding the Logary package, we first need to add the NodaTime v1.3.2
package and then the Logary package.
If we just added Logary, while adding, it will pull the NodaTime 2.0 version
265
Chapter 22 - Adding Logs using Logary
Inside use case (User Signup, New Tweet, etc.,) boundary, we communicate what
went wrong with all the required information that can be helpful to troubleshoot. At
the edge of the application, we check is there any error in the request pipeline and
perform the necessary action.
To pass data between WebPart's in the request pipeline, Suave has a useful
property called userState in the HttpContext record. The userState is of type
Map<string, obj> and we can add custom data to it using the setUserData function in
the Writers module.
The setState function that we used while persisting the logged in user is to
persist data in a session (across multiple requests).
Rest of the use cases are left as exercises for you to play with!
// src/FsTweet.Web/Wall.fs
// ...
let onPublishTweetFailure (err : PublishTweetError) =
match err with
| NotifyTweetError (tweetId, ex) ->
printfn "%A" ex
onPublishTweetSuccess tweetId
| CreateTweetError ex ->
printfn "%A" ex
JSON.internalError
// ...
The first thing that we need is, for which user this error has occurred.
Let's add a new parameter user of type User in the onPublishTweetFailure function
and the pass the user when this function get called
266
Chapter 22 - Adding Logs using Logary
The Message is a core data model in Logary, which is the smallest unit you can log.
We can make use of this data model to communicate what went wrong.
// src/FsTweet.Web/Wall.fs
// ...
module Suave =
// ...
open Logary
open Suave.Writers
// ...
let msg =
Message.event Error "Tweet Notification Error"
|> Message.setField "userId" userId
267
Chapter 22 - Adding Logs using Logary
We are creating a Logary Message of type Event with the name Tweet Notification
Error and set the extra fields using the Message.setField function.
We are also adding the actual exception to the Message using the Message.addExn .
Finally, we save the Message using the setUserData function from Suave's Writers
module.
Now we have captured all the required information on the business side.
open Chiron
// ...
open Logary
Initializing Logary
A remarkable feature of Logary is its ability to support multiple targets for the log. We
can configure it to write the log on Console, RabbitMQ, LogStash and much more.
268
Chapter 22 - Adding Logs using Logary
// src/FsTweet.Web/FsTweet.Web.fs
// ...
open Logary.Configuration
open Logary
open Logary.Targets
// ...
// ...
Target specifies where to log. We are using the Console.create factory function
from the Logary.Targets module to generate the Console target. The last
argument "console" is the name of the target which can be any arbitrary string.
The Rule specifies when to log. Here we are defining to log for all the cases.
(We can configure it to log only Fatal or Error alone)
The logaryConf composes the target and the rule into single configuration using
the function composition operator.
269
Chapter 22 - Adding Logs using Logary
// src/FsTweet.Web/FsTweet.Web.fs
// ...
open Hopac
// ...
use logary =
withLogaryManager "FsTweet.Web" logaryConf |> run
let logger =
logary.getLogger (PointName [|"Suave"|])
// ...
Logary uses Hopac's Job with Actor model behind the scenes to log the data in the
Targets without the blocking the caller. You can think of this as a lightweight Thread
running parallel along with the main program. If there is anything to log, we just need
to give it this Hopac Job, and we can move on without waiting for it to complete.
Here we are initializing the logaryManager with a name and the configuration and
asking it to run parallel.
Then we get a logger instance by providing the PointName, a location where you
send the log message from.
270
Chapter 22 - Adding Logs using Logary
// src/FsTweet.Web/FsTweet.Web.fs
// ...
In the readUserState function, we are trying to find an obj with the provided key in
the userState property of HttpContext . If the obj does exist, we are downcasting it
to a generic type 'value .
function to find the error log Message and log it using the logSimple function from
Logary. The succeed is an in-built WebPart from Suave
The last step is wiring this logIfError function with the request pipeline
+ let appWithLogger =
+ app >=> context (logIfError logger)
271
Chapter 22 - Adding Logs using Logary
Now if we run the application, and post a tweet with internet connection down, we
get the following log
Summary
We just scratched the surface of the Logary library in this chapter, and we can make
the logs even more robust by leveraging other features from the Logary's kitty.
Apart from Logary, An another take away is how we separated the communication
and action aspects of logging. This separation enabled us to perform the logging
outside of the business domain (at the edge of the application boundary), and we
didn't pass the logger as a dependency from the main function to the downstream
webpart functions.
272
Chapter 23 - Wrapping Up
Thank you for joining me in the quest of developing a real-world application using
functional programming principles in F#. I believe it has added value to you.
Let's have a quick recap of what we have done so far and discuss the journey ahead
before we wrap up.
Code Organization
Here is the architectural (colorful!) diagram of our FsTweet Application.
The black ovals represent the business use cases and the red circles at the top
represent the common abstractions.
Organizing the code around business use cases give us a clarity while trying to
understand the system.
273
Chapter 23 - Wrapping Up
The Composition Root pattern along with dependency injection through the partial
application provided us the foundation for our application.
In FsTweet, the main function is the composition root for the entire application
274
Chapter 23 - Wrapping Up
Then for each business use case, the presentation layer Suave's webpart function is
the composition root.
275
Chapter 23 - Wrapping Up
There are other patterns like Free Monad and Reader Monad to solve this differently.
For our use case, the approach that we used helped us to get the job done without
any complexities.
Being said this, the context always wins. For the application that we took, it made
sense. The Free Monad and Reader Monad approaches suit well for particular kind
of problems. Hence, my recommendation would be, learn all the approaches, pick
the one that suits your project and keeps it simple.
I encourage you to try Free Monad and Reader Monad approaches with the FsTweet
codebase and do let me know once you are done. It'd be an excellent resource for
the entire fsharp community!
Do drop a note when you make any of these or something else that you found fun
and meaningful.
There are two ways of spreading light: to be the candle or the mirror that
reflects it. - Edith Wharton
276