Mounting Suave in an Owin application - f#

I have an existing owin application written in C# and would like to mount a suave application as a middleware but since I am relatively new to F# I am finding it quite difficult to navigate how this should be done. I think I'm looking for something like:
// in F# land
module MySuaveApp.ApiModule
let app =
choose
[ GET >=> choose
[ path "/hello" >=> OK "Hello GET"
path "/goodbye" >=> OK "Good bye GET" ]
POST >=> choose
[ path "/hello" >=> OK "Hello POST"
path "/goodbye" >=> OK "Good bye POST" ] ]
let getSuaveAsMiddleware() =
... magic goes here ...
// in Startup.cs
app.Use(MySuaveApp.ApiModule.getSuaveAsMiddleware())
As for what that magic should be I think it's a combination of OwinApp.ofAppFunc or OwinApp.ofMidFunc, but I can't for the life of me figure out what it should be.

There is no easy magic.1 ofAppFunc and ofMidFunc are here for creating WebParts out of OWIN components, i.e. OWIN -> Suave, whereas you want Suave -> OWIN.
The following works for your 'application' and serves as an example what would be needed to get it working:
open System.Runtime.CompilerServices
[<Extension>]
module Api =
open Suave
open Successful
open Filters
open Operators
open Microsoft.Owin
open System.Threading.Tasks
let app =
choose [ GET >=> choose [ path "/hello" >=> OK "Hello GET"
path "/goodbye" >=> OK "Good bye GET" ]
POST >=> choose [ path "/hello" >=> OK "Hello POST"
path "/goodbye" >=> OK "Good bye POST" ] ]
let withCtx (ctx : IOwinContext) webpart =
async {
let request =
{ HttpRequest.empty with
headers = ctx.Request.Headers |> List.ofSeq |> List.map (fun kvp -> kvp.Key, kvp.Value |> String.concat ",")
host = ctx.Request.Host.Value
``method`` = HttpMethod.parse ctx.Request.Method
url = ctx.Request.Uri }
let! res = webpart { HttpContext.empty with request = request }
res |> Option.iter (fun r ->
ctx.Response.StatusCode <- r.response.status.code
match r.response.content with
| Bytes bs -> ctx.Response.Write bs
| _ -> failwith "Not supported")
return res
}
type SuaveMiddleware(n) =
inherit OwinMiddleware(n)
override __.Invoke(context : IOwinContext) =
let res = withCtx context app |> Async.RunSynchronously
match res with
| Some _ -> Task.CompletedTask
| None -> base.Next.Invoke context
[<Extension>]
let UseSuave(app : Owin.IAppBuilder) =
app.Use(typeof<SuaveMiddleware>)
The main works is delegated to withCtx that tries to fulfill a request given a IOwinContext and a WebPart. It does so mainly by converting back and forth between Suave and OWIN context and related entities.
Note that this code is a PoC (Proof-of-Concept) and not fit for production.
The SuaveMiddleware forwards request to the next middleware if Suave cannot fulfill the request.
Using from C# is easy then:
using MySuave;
using Owin;
namespace Main
{
using System.Web.Http;
public class Startup
{
public static void Configuration(IAppBuilder appBuilder)
{
appBuilder.UseSuave();
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
appBuilder.UseWebApi(config);
}
}
}
given
namespace Main.Example
{
using System.Web.Http;
[RoutePrefix("api")]
public class ExampleController : ApiController
{
[HttpGet, Route("")]
public string Index()
{
return "Hello World";
}
}
}
And both URLs work:
http://localhost:9000/hello
Hello GET
http://localhost:9000/api
<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">Hello World</string>
1 At least none I know of. I'm happy to be proven wrong.

Related

WebPart for forwarding requests to another server using Suave?

I would like to create a WebPart that forwards all requests to another web server that I specify.
Usage might look like this:
let app =
choose
[
path "/" >=> OK "Hello, world. "
path "/graphql" >=> createProxy "localhost:5000"
RequestErrors.NOT_FOUND "Not found"
]
startWebServer defaultConfig app
How should I implement this in Suave?
I found this snippet, but it seems to be for an old version of Suave:
let build_proxy_resolver (fwd_to_host : String) fwd_to_port =
let heserver = System.Net.Dns.GetHostEntry(fwd_to_host)
let ipaddr = heserver.AddressList.[0]
fun (request : HttpRequest) ->
Some (ipaddr, fwd_to_port)
let build_headers ctx =
//add and remove headers from the ctx, return the header list
ctx.request.headers
let proxy_app (ctx:HttpContext) =
let new_headers = build_headers ctx
let fwd_ctx = {ctx with request={ctx.request with headers=new_headers}}
let pxy = proxy (build_proxy_resolver "PROXY_TO.com" 80us) fwd_ctx
{ctx with response = { ctx.response with status=Suave.Types.Codes.HTTP_200; content=SocketTask pxy }} |> Some
This has now been added to Suave:
open System
open Suave
open Suave.Proxy
let app =
choose
[
path "/" >=> OK "Hello, world. "
path "/graphql" >=> proxy (Uri "http://localhost:5000")
RequestErrors.NOT_FOUND "Not found"
]
startWebServer defaultConfig app

How to customize logging in F# Saturn framework?

I created a default SAFE app as described here.
Removing redundant stuff, the server is this:
open Giraffe
open Saturn
let webApp = scope {
get "/api/init" (fun next ctx ->
task {
let number = 42
let! counter = task { return number }
return! Successful.OK counter next ctx
})
}
let app = application {
url ("http://0.0.0.0:8085/")
router webApp
memory_cache
use_static "../Client/public"
use_gzip
}
run app
Now, when running app, I see some logging in the console, basically incoming requests:
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET http://localhost:8085/api/init
How do I customize the logging? The docs are as scarce as possible, no examples. I need something simple, like logging "going to return 42...".
Or at least some links with cases.
You can pull the fully blown ILogger object from the context, ctx.
Open Microsoft.Extensions.Logging module and then you can do things like this:
let webApp = scope {
get "/api/init" (fun next ctx ->
task {
let logger = ctx.GetLogger();
let number = 42
logger.Log(LogLevel.Information, "Going to return " + number.ToString())
let! counter = task { return number }
return! Successful.OK counter next ctx
})
}
This will bring to your console:
info: object[0]
Going to return 42
I do not have any proper references. I found a similar thing at the Github of Giraffe server for which Saturn is basically a set of abstractions.
Logging configuration is built into v0.9 at least. I used the case below for myself to suppress most of the logging.
open Microsoft.Extensions.Logging
let app = application {
url ("http://0.0.0.0:8085/")
use_router webApp
logging (fun logger -> logger.SetMinimumLevel LogLevel.Critical |> ignore)
}

websharper F# endpoints - catch all page handler and page redirect

sorry this is such a long post
I am using F# and WebSharper (I am new to both technologies)
I have some endpoints defined (I have working code until I add the NotFound endpoint)
type EndPoint =
| [<EndPoint "/">] Home
| [<EndPoint "/login">] Login
| [<EndPoint "/about">] About
| [<EndPoint "/logout">] Logout
// trying to make a catch-all page handler
| [<EndPoint "/"; Wildcard>] NotFound of string
...
let HomePage ctx =
Templating.Main ctx EndPoint.Home "Home" [
// page stuff
]
let LoginPage ctx =
Templating.Main ctx EndPoint.Login "Login" [
h1 [] [text "Login Here"]
div [] [client <# Client.LoginWidget() #>]
]
// other page constructs
let MissingPage ctx path =
Templating.Main ctx EndPoint.About "Page Not Found" [
h1 [] [text "404"]
p [] [text "The requested page could not be found"]
p [] [text path]
]
...
[<Website>]
let Main =
Application.MultiPage (fun ctx endpoint ->
match endpoint with
| EndPoint.Home -> HomePage ctx
| EndPoint.About -> AboutPage ctx
| EndPoint.Login -> LoginPage ctx
| EndPoint.Logout ->
async {
// call server-side code to log the user out
// what would i do here to redirect the user to the /login
// page
}
| EndPoint.NotFound path -> MissingPage ctx path
)
Add the NotFound endpoint messes up my other pages, for example, my home page starts getting handled by the MissingPage handler. which I can understand since the home page is set to match "/" and the not pattern is matching "/" wildcard, although I would have expected the single / to have matched the Home endpoint and anything else other than /Login /About and /Logout to have match the NotFound branch. But clearly, I do not understand something correctly.
So How can I get a "catch-all" type endpoint so that I can properly handle any path that is not explicitly catered for
the other thing that is messing up when I had the NotFound handling code is the Login handler no longer processes
div [] [client <# Client.LoginWidget() #>]
And lastly, in the Logout handler, I want to call some server-side code (no problem) but what should I do to then redirect to a new web page, for example, to send the user back to the /login page?
Sorry again for such a long post
Derek
Below is based on what Loïc at websharper.com passed on to me, adding here in case it is helpful to someone else.
First Web.config needed
<httpErrors errorMode="Custom">
<remove statusCode="404"/>
<error statusCode="404" responseMode="ExecuteURL" path="/notfound"/>
</httpErrors>
type EndPoint =
... other end points
| [<EndPoint "/notfound"; Wildcard>] NotFound of string
[<Website>]
let Main =
Application.MultiPage (fun ctx endpoint ->
match endpoint with
// handle other endpoints
// Handle none existant paths
| EndPoint.NotFound _ ->
// Parse the original URI from ASP.NET's rewrite, in case you need it
let requestedUri =
let q = ctx.RequestUri.Query
let q = q.[q.IndexOf(';') + 1 ..]
match System.Uri.TryCreate(q, System.UriKind.Absolute) with
// The request was to /notfound/... directly
| false, _ -> ctx.RequestUri
// The request was to a non-existent path, and rewritten by ASP.NET
| true, uri -> uri
Content.Text (sprintf "Unknown URI: %A" requestedUri)
|> Content.SetStatus Http.Status.NotFound
...

Using node modules from fable

I'm trying to import a node module in my fable code. Being new to fable I did expect so problems and understanding the import flow seems to be one of those. I have the below code which compiles fine but fails run time with Cannot read property 'request' of undefined on the line of the printfn statement
module Session =
let inline f (f: 'a->'b->'c->'d) = Func<_,_,_,_> f
[<Import("default","request")>]
type Http =
abstract request : string -> System.Func<obj,obj,obj,unit> -> unit
let http : Http = failwith "js only"
let start () =
http.request "http://dr.dk" (ff (fun error response body ->
printfn "%A" body
))
do
start()
I was able to get your example working in the Fable REPL:
open System
open Fable
open Fable.Core
open Fable.Core.JS
open Fable.Core.JsInterop
type RequestCallback = Func<obj, obj, obj, unit>
type Request = Func<string, RequestCallback, unit>
[<ImportDefault("request")>]
let request : Request = jsNative
let start () =
request.Invoke("http://dr.dk", (fun error response body ->
console.log("error", error)
console.log("response", response)
console.log("body", body)
))
start ()
And here is the JavaScript that it generated:
import request from "request";
import { some } from "fable-library/Option.js";
export function start() {
request("http://dr.dk", (error, response, body) => {
console.log(some("error"), error);
console.log(some("response"), response);
console.log(some("body"), body);
});
}
start();
Note that there are bindings for many modules already. For this particular task, I would suggest using Fable.Fetch. If you want a library that works in the browser and .NET, try Fable.SimpleHttp.

Suave in watch mode (during development)

I am working on Suave 1.0 + Angular 2.0 sample app and very interesting to start Suave server in watch mode, so the server watch file changes (js,css,html) in root folder and sub-folders and automatically send refresh command to all open browser tabs with my application when any file is changed.
lite-server from Angular 2 5min Quckstark can do this and it is very handy.
I think that most of watch pieces can be found in latest Steffen Forkmann's post but it is not very clean how to send refresh to the open browser tabs.
Please provide complete code of similar implementation with Suave.
The code of Suave server should looks similar to this
#r "packages/Suave/lib/net40/suave.dll"
#r "packages/FAKE/tools/FakeLib.dll"
open Fake
open Suave
open Suave.Operators
open Suave.Sockets.Control
open Suave.WebSocket
open Suave.Utils
open Suave.Files
open Suave.RequestErrors
open Suave.Filters
open System
open System.Net
let port =
let rec findPort port =
let portIsTaken =
System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners()
|> Seq.exists (fun x -> x.Port = int(port))
if portIsTaken then findPort (port + 1us) else port
findPort 8083us
let logger = Logging.Loggers.ConsoleWindowLogger Logging.LogLevel.Verbose
let refreshEvent = new Event<_>()
let handleWatcherEvents (events:FileChange seq) =
for e in events do
let fi = fileInfo e.FullPath
traceImportant <| sprintf "%s was changed." fi.Name
refreshEvent.Trigger()
let socketHandler (webSocket : WebSocket) =
fun cx -> socket {
while true do
let! refreshed =
Control.Async.AwaitEvent(refreshEvent.Publish)
|> Suave.Sockets.SocketOp.ofAsync
do! webSocket.send Text (ASCII.bytes "refreshed") true
}
let cfg =
{ defaultConfig with
homeFolder = Some (__SOURCE_DIRECTORY__)
bindings =
[ HttpBinding.mk HTTP IPAddress.Loopback port ]
listenTimeout = TimeSpan.FromMilliseconds 3000. }
let app : WebPart =
choose [
Filters.log logger logFormat >=> never
Filters.path "/websocket" >=> handShake socketHandler
Filters.GET >=> Filters.path "/" >=> file "index.html"
Writers.setHeader "Cache-Control" "no-cache, no-store, must-revalidate"
>=> Writers.setHeader "Pragma" "no-cache"
>=> Writers.setHeader "Expires" "0"
>=> browseHome
NOT_FOUND "Found no handlers."
]
let watcher =
!! ("app/*.js")
++ ("*.html")
|> WatchChanges handleWatcherEvents
try
System.Diagnostics.Process.Start(sprintf "http://localhost:%d/index.html" port) |> ignore
startWebServer cfg app
finally
watcher.Dispose()
So we setup watcher that handle changes in js(generated by TypeScript) and html files and send refresh command to the client, but in the same time we need to add following code to the head section of index.html to handle refresh on the client side
<!-- 3. Listen on refresh events from the server -->
<script language="javascript" type="text/javascript">
function init()
{
websocket = new WebSocket("ws://"+window.location.host+"/websocket");
websocket.onmessage = function(evt) { location.reload(); };
}
window.addEventListener("load", init, false);
</script>
The full demo app you can find here

Resources