State and Sessions in Suave

While web applications are technically stateless, there are still times where we need to start from a known state. So, let's jump in and look at aspects of session cookies first, then how we can manage state among WebParts.

Suave provides storage interfaces for cookies in the Suave.State.CookieStateStore module. The statefulForSession WebPart can be composed to make a path session-aware. From there, the HttpContext.state function extracts the state information, and the get and set functions on the resulting StateStore object can be used to manipulate the contents of the state tracked by the session.

open Suave open Suave.State.CookieStateStore let setSessionValue (key : string) (value : 'T) : WebPart = context (fun ctx -> match HttpContext.state ctx with | Some state -> state.set key value | _ -> never // fail ) let getSessionValue (ctx : HttpContext) (key : string) : 'T option = match HttpContext.state ctx with | Some state -> state.get key | _ -> None /// This a convenience function that turns a None string result into an empty string let getStringSessionValue (ctx : HttpContext) (key : string) : string = defaultArg (getSessionValue ctx key) "" let cookieYes : WebPart = context (fun ctx -> OK (getStringSessionValue ctx "test")) let cookieNo : WebPart = context (fun ctx -> OK (getStringSessionValue ctx "nope")) let app = statefulForSession >=> setSessionValue "test" "123" >=> choose [ path "/yes" >=> cookieYes path "/no" >=> cookieNo RequestErrors.NOT_FOUND ]

Server Keys

The contents of the cookie are encrypted before the cookie is sent. Suave's default configuration generates a new server key each time the server is restarted. While this is not wrong, users would likely get quite annoyed if they lost their state because the server was restarted. Additionally, specifying a server key lets load-balanced servers access the same information.

The key generated by Suave is secure; we just don't need it changing. To get a key, you can use the following code, either in an .fsx file (be sure to reference Suave.dll), or by placing the following code in your application's entry point, before the startWebServer. Run it once; it will write a file called key.txt that contains a base-64 encoded string representing a random key, which we can use to configure our session key encryption. (If you put it in your application, be sure to remove it after you've run it.)

open Suave.Utils open System let writeKey key = System.IO.File.WriteAllText ("key.txt", key) Crypto.generateKey Crypto.KeyLength |> Convert.ToBase64String |> writeKey

Now, key in hand, we can continue our example from above. (Note that hard-coding the key in the source code is a poor way to manage these keys; a configuration file is better, but environment variables or container configuration per environment is best.)

let suaveCfg = { defaultConfig with serverKey = ServerKey.fromBase64 [encoded-key] } [<EntryPoint>] let main argv = startWebServer suaveCfg app 0

Suave uses the .NET Framework type BinaryFormatter to serialize the Map<string, obj> containing the session state; this is the default. However, the BinaryFormatter was removed in the .NET Core API, and the DataContractJsonSerializer does not recognize the Map<string, obj> type. One option is to utilize JSON.NET to serialize this object. To use that, ensure you've added the Newtonsoft.Json NuGet package to your project, then put the following code somewhere before the suaveCfg definition in the example above.

open Newtonsoft.Json let utf8 = System.Text.Encoding.UTF8 type JsonNetCookieSerialiser () = interface CookieSerialiser with member x.serialise m = utf8.GetBytes (JsonConvert.SerializeObject m) member x.deserialise m = JsonConvert.DeserializeObject<Map<string, obj>> (utf8.GetString m)

Then, modify the configuration to use that serializer.

let suaveCfg = { defaultConfig with serverKey = Convert.FromBase64String [encoded-key] cookieSerialiser = new JsonNetCookieSerialiser() }

State among WebParts

Within the Writers module, Suave provides the functions setUserData and unsetUserData for adding items to the context's userState property. The example below could be used to accrue a list of messages to be displayed to the user.

/// Read an item from the user state, downcast to the expected type let readUserState ctx key : 'value = ctx.userState |> Map.tryFind key |> Option.map (fun x -> x :?> 'value) |> Option.get let addUserMessage (message : string) : WebPart = context (fun ctx -> let read = readUserState ctx let existing = match ctx.userState |> Map.tryFind "messages" with | Some _ -> read "messages" | _ -> [] Writers.setUserData "messages" (message :: existing)) let app = choose [ path "/" >=> addUserMessage "It's a state!" >=> addUserMessage "Another one" >=> context (fun ctx -> Successful.OK (View.page ctx.userState)) ]

In this example, View.page is a function that generates the output, using the user state Map<string, obj> to display the messages in a nice way.

We've covered two different ways of managing state. Session state persists throughout the session, while userData has a per-request lifetime.

(NOTE: Currently, Suave is keeping userData across requests if those requests are served from the same TCP connection, which is how Suave implements HTTP keep-alive. There is an issue to fix this behavior, but if this is causing unintended issues, adding >=> Writers.unsetUserData "messages" at the end of the path "/" pipeline will ensure that it is cleared out.)

namespace Suave
module State

from Suave
module CookieStateStore

from Suave.State
val setSessionValue : key:string -> value:'T -> WebPart

Full name: temp.setSessionValue
val key : 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 value : 'T
Multiple items
module WebPart

from Suave

--------------------
type WebPart = WebPart<HttpContext>

Full name: Suave.Http.WebPart

--------------------
type WebPart<'a> = 'a -> Async<'a option>

Full name: Suave.WebPart.WebPart<_>
val context : apply:(HttpContext -> HttpContext -> 'a) -> context:HttpContext -> 'a

Full name: Suave.Http.context
val ctx : HttpContext
Multiple items
module HttpContext

from Suave.State.CookieStateStore

--------------------
module HttpContext

from Suave.Http

--------------------
type HttpContext =
  {request: HttpRequest;
   runtime: HttpRuntime;
   connection: Connection;
   userState: Map<string,obj>;
   response: HttpResult;}
  member clientIp : trustProxy:bool -> sources:string list -> IPAddress
  member clientPort : trustProxy:bool -> sources:string list -> Port
  member clientProto : trustProxy:bool -> sources:string list -> string
  member clientIpTrustProxy : IPAddress
  member clientPortTrustProxy : Port
  member clientProtoTrustProxy : string
  member isLocal : bool
  static member clientIp_ : Property<HttpContext,IPAddress>
  static member clientPort_ : Property<HttpContext,Port>
  static member clientProto_ : Property<HttpContext,string>
  static member connection_ : Property<HttpContext,Connection>
  static member isLocal_ : Property<HttpContext,bool>
  static member request_ : Property<HttpContext,HttpRequest>
  static member response_ : Property<HttpContext,HttpResult>
  static member runtime_ : Property<HttpContext,HttpRuntime>
  static member userState_ : Property<HttpContext,Map<string,obj>>

Full name: Suave.Http.HttpContext
val state : ctx:HttpContext -> State.StateStore option

Full name: Suave.State.CookieStateStore.HttpContext.state
union case Option.Some: Value: 'T -> Option<'T>
val state : State.StateStore
abstract member State.StateStore.set : string -> 'T -> WebPart
val never : WebPart<'a>

Full name: Suave.WebPart.never
val getSessionValue : ctx:HttpContext -> key:string -> 'T option

Full name: temp.getSessionValue
type 'T option = Option<'T>

Full name: Microsoft.FSharp.Core.option<_>
abstract member State.StateStore.get : string -> 'T option
union case Option.None: Option<'T>
val getStringSessionValue : ctx:HttpContext -> key:string -> string

Full name: temp.getStringSessionValue


 This a convenience function that turns a None string result into an empty string
val defaultArg : arg:'T option -> defaultValue:'T -> 'T

Full name: Microsoft.FSharp.Core.Operators.defaultArg
val cookieYes : WebPart

Full name: temp.cookieYes
val OK : body:string -> WebPart

Full name: Suave.Successful.OK
val cookieNo : WebPart

Full name: temp.cookieNo
val app : (HttpContext -> Async<HttpContext option>)

Full name: temp.app
val statefulForSession : WebPart

Full name: Suave.State.CookieStateStore.statefulForSession
val choose : options:WebPart<'a> list -> WebPart<'a>

Full name: Suave.WebPart.choose
val path : pathAfterDomain:string -> WebPart

Full name: Suave.Filters.path
module RequestErrors

from Suave
val NOT_FOUND : body:string -> WebPart

Full name: Suave.RequestErrors.NOT_FOUND
namespace Suave.Utils
namespace System
val writeKey : key:string -> unit

Full name: temp.writeKey
namespace System.IO
type File =
  static member AppendAllLines : path:string * contents:IEnumerable<string> -> unit + 1 overload
  static member AppendAllText : path:string * contents:string -> unit + 1 overload
  static member AppendText : path:string -> StreamWriter
  static member Copy : sourceFileName:string * destFileName:string -> unit + 1 overload
  static member Create : path:string -> FileStream + 3 overloads
  static member CreateText : path:string -> StreamWriter
  static member Decrypt : path:string -> unit
  static member Delete : path:string -> unit
  static member Encrypt : path:string -> unit
  static member Exists : path:string -> bool
  ...

Full name: System.IO.File
IO.File.WriteAllText(path: string, contents: string) : unit
IO.File.WriteAllText(path: string, contents: string, encoding: Text.Encoding) : unit
module Crypto

from Suave.Utils
val generateKey : keyLength:uint16 -> byte []

Full name: Suave.Utils.Crypto.generateKey
val KeyLength : uint16

Full name: Suave.Utils.Crypto.KeyLength
type Convert =
  static val DBNull : obj
  static member ChangeType : value:obj * typeCode:TypeCode -> obj + 3 overloads
  static member FromBase64CharArray : inArray:char[] * offset:int * length:int -> byte[]
  static member FromBase64String : s:string -> byte[]
  static member GetTypeCode : value:obj -> TypeCode
  static member IsDBNull : value:obj -> bool
  static member ToBase64CharArray : inArray:byte[] * offsetIn:int * length:int * outArray:char[] * offsetOut:int -> int + 1 overload
  static member ToBase64String : inArray:byte[] -> string + 3 overloads
  static member ToBoolean : value:obj -> bool + 17 overloads
  static member ToByte : value:obj -> byte + 18 overloads
  ...

Full name: System.Convert
Convert.ToBase64String(inArray: byte []) : string
Convert.ToBase64String(inArray: byte [], options: Base64FormattingOptions) : string
Convert.ToBase64String(inArray: byte [], offset: int, length: int) : string
Convert.ToBase64String(inArray: byte [], offset: int, length: int, options: Base64FormattingOptions) : string
val suaveCfg : SuaveConfig

Full name: temp.suaveCfg
val defaultConfig : SuaveConfig

Full name: Suave.Web.defaultConfig
type ServerKey = byte []

Full name: Suave.Http.ServerKey
Multiple items
type EntryPointAttribute =
  inherit Attribute
  new : unit -> EntryPointAttribute

Full name: Microsoft.FSharp.Core.EntryPointAttribute

--------------------
new : unit -> EntryPointAttribute
val main : argv:string [] -> int

Full name: temp.main
val argv : string []
val startWebServer : config:SuaveConfig -> webpart:WebPart -> unit

Full name: Suave.Web.startWebServer
module Json

from Suave
val utf8 : Text.Encoding

Full name: temp.utf8
namespace System.Text
type Encoding =
  member BodyName : string
  member Clone : unit -> obj
  member CodePage : int
  member DecoderFallback : DecoderFallback with get, set
  member EncoderFallback : EncoderFallback with get, set
  member EncodingName : string
  member Equals : value:obj -> bool
  member GetByteCount : chars:char[] -> int + 3 overloads
  member GetBytes : chars:char[] -> byte[] + 5 overloads
  member GetCharCount : bytes:byte[] -> int + 2 overloads
  ...

Full name: System.Text.Encoding
property Text.Encoding.UTF8: Text.Encoding
Multiple items
type JsonNetCookieSerialiser =
  interface CookieSerialiser
  new : unit -> JsonNetCookieSerialiser

Full name: temp.JsonNetCookieSerialiser

--------------------
new : unit -> JsonNetCookieSerialiser
type CookieSerialiser =
  interface
    abstract member deserialise : byte [] -> Map<string,obj>
    abstract member serialise : Map<string,obj> -> byte []
  end

Full name: Suave.CookieSerialiser
val x : JsonNetCookieSerialiser
override JsonNetCookieSerialiser.serialise : m:Map<string,obj> -> byte []

Full name: temp.JsonNetCookieSerialiser.serialise
val m : Map<string,obj>
Text.Encoding.GetBytes(s: string) : byte []
Text.Encoding.GetBytes(chars: char []) : byte []
Text.Encoding.GetBytes(chars: char [], index: int, count: int) : byte []
Text.Encoding.GetBytes(chars: nativeptr<char>, charCount: int, bytes: nativeptr<byte>, byteCount: int) : int
Text.Encoding.GetBytes(s: string, charIndex: int, charCount: int, bytes: byte [], byteIndex: int) : int
Text.Encoding.GetBytes(chars: char [], charIndex: int, charCount: int, bytes: byte [], byteIndex: int) : int
override JsonNetCookieSerialiser.deserialise : m:byte [] -> Map<string,obj>

Full name: temp.JsonNetCookieSerialiser.deserialise
val m : byte []
Multiple items
module Map

from YoLo

--------------------
module Map

from Microsoft.FSharp.Collections

--------------------
type Map<'Key,'Value (requires comparison)> =
  interface IEnumerable
  interface IComparable
  interface IEnumerable<KeyValuePair<'Key,'Value>>
  interface ICollection<KeyValuePair<'Key,'Value>>
  interface IDictionary<'Key,'Value>
  new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
  member Add : key:'Key * value:'Value -> Map<'Key,'Value>
  member ContainsKey : key:'Key -> bool
  override Equals : obj -> bool
  member Remove : key:'Key -> Map<'Key,'Value>
  ...

Full name: Microsoft.FSharp.Collections.Map<_,_>

--------------------
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
Multiple items
val string : value:'T -> string

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

--------------------
type string = String

Full name: Microsoft.FSharp.Core.string
type obj = Object

Full name: Microsoft.FSharp.Core.obj
Text.Encoding.GetString(bytes: byte []) : string
Text.Encoding.GetString(bytes: nativeptr<byte>, byteCount: int) : string
Text.Encoding.GetString(bytes: byte [], index: int, count: int) : string
Convert.FromBase64String(s: string) : byte []
val readUserState : ctx:HttpContext -> key:string -> 'value

Full name: temp.readUserState


 Read an item from the user state, downcast to the expected type
HttpContext.userState: Map<string,obj>
val tryFind : key:'Key -> table:Map<'Key,'T> -> 'T option (requires comparison)

Full name: Microsoft.FSharp.Collections.Map.tryFind
Multiple items
module Option

from YoLo

--------------------
module Option

from Microsoft.FSharp.Core
val map : mapping:('T -> 'U) -> option:'T option -> 'U option

Full name: Microsoft.FSharp.Core.Option.map
val x : obj
val get : option:'T option -> 'T

Full name: Microsoft.FSharp.Core.Option.get
val addUserMessage : message:string -> WebPart

Full name: temp.addUserMessage
val message : string
val read : (string -> string list)
val existing : string list
module Writers

from Suave
val setUserData : key:string -> value:'T -> WebPart

Full name: Suave.Writers.setUserData
val app : WebPart<HttpContext>

Full name: temp.app
module Successful

from Suave

results matching ""

    No results matching ""