Skip to content

Commit 04b39a9

Browse files
authored
Add Suave-style cookie session primitives to the ASP.NET Core Suave adapter (#12)
* Add cookie and session helpers for Suave API * Implement cookie and session support APIs --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent a3816d0 commit 04b39a9

2 files changed

Lines changed: 76 additions & 1 deletion

File tree

src/FSharpPlus.AspNetCore.Suave/Library.fs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ open System.Text
99
open System
1010
open System.IO
1111
open System.Text.RegularExpressions
12+
open Microsoft.AspNetCore.Http.Features
1213

1314
// setup something that reminds us of what Suave can work with
1415
// this is an overly simplified model of Suave in order to show how OptionT can be used
@@ -87,9 +88,15 @@ module RequestErrors=
8788
let FORBIDDEN s = setStatusAndContent (int HttpStatusCode.Forbidden) s
8889
let NOT_FOUND s = setStatusAndContent (int HttpStatusCode.NotFound) s
8990
let UNAUTHORIZED s = setStatusAndContent (int HttpStatusCode.Unauthorized) s
91+
let private tryGetSession (ctx:Context) =
92+
match ctx.request.HttpContext.Features.Get<ISessionFeature>() with
93+
| null -> None
94+
| feature when isNull feature.Session -> None
95+
| feature -> Some feature.Session
9096
module Filters=
9197
let response (method : string) = OptionT << fun (x : Context) -> async.Return (if (method = x.request.Method) then Some x else None)
9298
let hasFormContentType = OptionT << fun (x : Context) -> async.Return (if x.request.HasFormContentType then Some x else None)
99+
let statefulForSession = OptionT << fun (x : Context) -> async.Return (if tryGetSession x |> Option.isSome then Some x else None)
93100

94101
let GET (x : Http.Context) = response "GET" x
95102
let POST (x : Http.Context) = response "POST" x
@@ -119,6 +126,22 @@ module Request =
119126
| _ -> None
120127
module Header=
121128
let tryGet key (r:HttpRequest)=match r.Headers.TryGetValue key with | (true,v)->Some v | _-> None
129+
module Cookie=
130+
let tryGet key (r:HttpRequest)=
131+
match r.Cookies.TryGetValue key with
132+
| true, v -> Some v
133+
| _ -> None
134+
135+
module HttpContext=
136+
let state (ctx:Context) = tryGetSession ctx
137+
138+
module Session=
139+
let tryGet key (session:ISession) =
140+
match session.GetString key with
141+
| null -> None
142+
| value -> Some value
143+
let set key value (session:ISession) =
144+
session.SetString(key, value)
122145

123146

124147

@@ -134,4 +157,3 @@ let appRun (app:WebPart<Context>) (appBuilder:IApplicationBuilder)=
134157
| None -> return! Task.CompletedTask
135158
}
136159
appRun runApp appBuilder
137-

tests/Tests/Tests.fs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ open Microsoft.AspNetCore.Builder
1616
open Microsoft.AspNetCore.Hosting
1717
open Microsoft.AspNetCore.Http
1818
open Microsoft.AspNetCore.TestHost
19+
open Microsoft.Extensions.DependencyInjection
1920

2021
open Expecto
2122

@@ -24,6 +25,10 @@ open FSharpPlus.AspNetCore.Suave
2425
open Notes
2526

2627
module ``integration test using test server`` =
28+
let tryParseInt (s:string) =
29+
match Int32.TryParse s with
30+
| true, n -> Some n
31+
| _ -> None
2732
module TestServer=
2833
let fakeDb() =
2934
let withUserId userId = (=) userId << fst
@@ -124,3 +129,51 @@ module ``integration test using test server`` =
124129
Expect.equal (parseJson noteJson) (Ok {id=NoteId 1;text="my next text"}) "Expected note json"
125130
})
126131
]
132+
133+
[<Tests>]
134+
let ``session state uses cookie`` =
135+
testCase "counter is incremented across requests with same cookie" <| fun _ -> waitFor(task {
136+
let sessionWebPart =
137+
Filters.path "/session"
138+
>=> Filters.statefulForSession
139+
>=> (fun ctx ->
140+
match FSharpPlus.AspNetCore.Suave.HttpContext.state ctx with
141+
| Some store ->
142+
let current =
143+
store
144+
|> FSharpPlus.AspNetCore.Suave.Session.tryGet "counter"
145+
|> Option.bind tryParseInt
146+
|> Option.defaultValue 0
147+
store |> FSharpPlus.AspNetCore.Suave.Session.set "counter" (string (current + 1))
148+
Successful.OK (sprintf "Hello %d time(s)" (current + 1)) ctx
149+
| None ->
150+
Successful.OK "No session available" ctx)
151+
152+
let builder =
153+
WebHostBuilder()
154+
.ConfigureServices(fun services ->
155+
services.AddDistributedMemoryCache() |> ignore
156+
services.AddSession() |> ignore)
157+
.Configure(fun app ->
158+
app.UseSession() |> ignore
159+
Suave.appRun sessionWebPart app |> ignore)
160+
161+
use testServer = new TestServer(builder)
162+
use client = testServer.CreateClient()
163+
164+
let! first = client.GetAsync("http://localhost/session")
165+
let! firstContent = first.Content.ReadAsStringAsync()
166+
let cookieHeader =
167+
first.Headers.GetValues("Set-Cookie")
168+
|> Seq.choose (fun cookie -> cookie.Split(';') |> Array.tryHead)
169+
|> String.concat "; "
170+
171+
let request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/session")
172+
request.Headers.Add("Cookie", cookieHeader)
173+
174+
let! second = client.SendAsync(request)
175+
let! secondContent = second.Content.ReadAsStringAsync()
176+
177+
Expect.equal firstContent "Hello 1 time(s)" "Expected first session response"
178+
Expect.equal secondContent "Hello 2 time(s)" "Expected second session response"
179+
})

0 commit comments

Comments
 (0)