val simpleApp : obj

Full name: saturn.simpleApp
val betterApp : obj

Full name: saturn.betterApp
val get : ctx:'a * name:string -> 'b

Full name: saturn.get
val ctx : 'a
val name : string
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
val resource : obj

Full name: saturn.resource
val api : obj

Full name: saturn.api
val apiRouter : obj

Full name: saturn.apiRouter
val indexHandler : name:string -> 'a

Full name: saturn.indexHandler
val sprintf : format:Printf.StringFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.sprintf
val anotherScope : obj

Full name: saturn.anotherScope
val post : ctx:'a -> 'b

Full name: saturn.post
val put : ctx:'a -> 'b

Full name: saturn.put
val pc : obj

Full name: saturn.pc
val app : obj

Full name: saturn.app

Saturn: F# |> MVC

FsReveal

Presented by Jeremy Abbott

Hi

Too Gay to λ

About Me

  • Senior Software Engineer @ Incomm Digital Solutions
  • F# and Functional Programming Fan

The Slides

Saturn

Not this Saturn

This is Saturn

  • An opinionated functional-first micro-web-framework
  • Even thought it's not a planet, it does have "rings":
    • Kestrel and ASP.NET Core
    • Giraffe
  • Created by Krzysztof Cieslak
    • Also created:
      1. Ionide 👏🏼
      2. Forge 👏🏼
      3. VSCode Elm Extension 👏🏼

Also Moons

  • Dapper for performant SQL data access
  • Simple.Migrater for data migration support

Why?

  • Reduces the barrier to entry for folks new to F#
  • Abstracts away raw HTTP manipulation until you need it.
  • The MVC pattern is familiar to those coming from other web frameworks
  • High team productivity thanks to the Saturn CLI

Before Saturn...

  • You're an F#er that wants to write your next web app at work with F#...

Can I Haz Functional?

You show this to your teammates who haven't used a lot of F#:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
let simpleApp = (Successful.OK "Hello F#ers")

let betterApp =
    choose [
        GET >=> path "/hello" >=> simpleApp
        POST >=> path "/goodbye" >=> (Successful.OK "Goodbye")
    ]
startWebServer defaultConfig betterApp

What's That >=>

Suave/Giraffe Are AMAZING

But Saturn can ease people into the abstractions used by these frameworks.

With Saturn

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
let get (ctx: HttpContext, name : string) =
    task {
        return! Controller.json ctx ("Hello " + name)
    }

let resource = controller {
    show get // routes to GET /<resource>/name
}

Recognition

  • "It's just an async function that returns JSON"
  • "It looks a little like JavaScript, but with real types"

Pipelines

  • Saturn makes brilliant use of computation expressions
  • The pipeline CE is used to combine Giraffe HTTP Handlers in a declarative manner

Pipelines (cont.)

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
let api = pipeline {
    plug acceptJson
    set_header "x-pipeline-type" "Api"
}
// api is an HttpHandler
// acceptJson is also an HttpHandler
// set_header returns an HttpHandler
// plug takes any HttpHandler

Scopes

CE use to combine pipelines, controllers, and other HttpHandlers

1: 
2: 
3: 
4: 
5: 
let apiRouter = scope {
    not_found_handler notFound
    pipe_through api // add a pipeline
    forward "/albums" Albums.Controller.resource
}

Scopes (cont.)

1: 
2: 
3: 
4: 
5: 
6: 
7: 
let indexHandler (name : string) =
    sprintf "Hello %s" name
    |> json

let anotherScope = scope {
    getf "/hello/%s" indexHandler
}

Controllers

CE used to create convention based routing/handlers

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
// w/o controller
route "/person" >=>
    choose [
        POST >=>
            fun (next : HttpFunc) (ctx : HttpContext) ->
            task {
                let! person = ctx.BindModelAsync<Person>()
                return! json person next ctx
            }
        PUT >=>
            fun (next : HttpFunc) (ctx : HttpContext) ->
            task {
                let! person = ctx.BindModelAsync<Person>()
                return! json person next ctx
            }
    ]

Controllers (cont.)

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
let post (ctx : HttpContext) =
    task {
        let! person = ctx.BindModelAsync<Person>()
        return! Controller.json ctx "Created!"
    }
let put (ctx : HttpContext) =
    task {
        let! person = ctx.BindModelAsync<Person>()
        return! Controller.json ctx "Updated!"
    }

let pc = controller {
    create post
    update put
}

The Application CE

  • Builder that returns an IWebHost
  • Declarative syntax that abstracts
    • IWebHostBuilder
    • IServiceCollection
    • IApplicationBuilder

The Album Application

1: 
2: 
3: 
4: 
5: 
6: 
7: 
let app = application {
    pipe_through endpointPipe

    router Router.router
    url "http://0.0.0.0:8085/"
    memory_cache
}

Saturn CLI

This goes in paket.references:

1: 
dotnet-saturn

And then

1: 
2: 
# dotnet saturn gen.json <singular> <plural> <fields>
dotnet saturn gen.json Album Albums Id:int Key:guid Name:string Price:decimal

Saturn CLI gen Output

  • Model with properties based on passed in fields
  • Repository with CRUD functions (via dapper)
  • Controller with CRUD actions
  • Server-side views (if gen/gen.html called)
  • Migration for new Albums database table

Saturn CLI Migrations

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
[<Migration(201803041313L, "Create Albums")>]
type CreateAlbums() =
  inherit Migration()
  override __.Up() =
    base.Execute(@"CREATE TABLE Albums(
      Id INT NOT NULL,
      Key TEXT NOT NULL,
      Name TEXT NOT NULL,
      Price DECIMAL NOT NULL
    )") // sqlite data types
  override __.Down() =
    base.Execute(@"DROP TABLE Albums")

Run the Migrations

1: 
2: 
# update the database to the latest migration
dotnet saturn migration

What's Next?

Install the template:

1: 
2: 
dotnet new -i Saturn.Template
dotnet new saturn -lang F#

Any Questions?

Resources