Search code examples
f#owinsuave

Mounting Suave in an Owin application


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.


Solution

  • 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.