Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[<Addtional Package Request (and Solution)>] Remoting with Old School Azure Function Style (HttpRequest and IActionResult) #281

Open
DieselMeister opened this issue Nov 18, 2021 · 3 comments

Comments

@DieselMeister
Copy link

DieselMeister commented Nov 18, 2021

Hi.

At first the important part: I love your work and this project is awesome.

I was playing around with the Azure Function implementation. Sadly the isolated function is not supported by the Visual Studio (Debugging and Stuff), so I thought, hey modify the your "FableAzureFunctionsAdapter.fs" in the Azure Function Package and let it work with the "old school" Azure Function HttpRequest and IActionResult ...

Btw. Maybe there is an easier solution already in place and using another package. I don't know.

So here ist the modified version of the Adapter. (.NET 6.0)

namespace Fable.Remoting.AzureFunctions.OldSchool

open System
open System.Net
open System.Threading.Tasks
open System.IO
open Fable.Remoting.Server
open Newtonsoft.Json
open Fable.Remoting.Server.Proxy
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Mvc



module private FuncsUtil =
    
    let private htmlString (html:string) (req:HttpRequest) : Task<IActionResult option> =
        task {
            return Some (ContentResult(Content=html, StatusCode=200, ContentType = "text/html; charset=utf-8") :> IActionResult)
            
        } 
        
    
    let text (str:string) (req:HttpRequest) : Task<IActionResult option> =
        task {
            return Some (ContentResult(Content=str, StatusCode=200, ContentType = "text/plain; charset=utf-8") :> IActionResult)
        }
    
    let private path (r:HttpRequest) = r.Path.Value.Split("?").[0]

    let setJsonBody (response: obj) (statusCode:int) (logger: Option<string -> unit>) (req:HttpRequest) : Task<IActionResult option> =
        task {
            use ms = new MemoryStream ()
            jsonSerialize response ms
            let responseBody = System.Text.Encoding.UTF8.GetString (ms.ToArray ())
            Diagnostics.outputPhase logger responseBody
            return Some <| (ContentResult(Content=responseBody, StatusCode=statusCode, ContentType = "application/json; charset=utf-8") :> IActionResult)
        }
        

    /// Handles thrown exceptions
    let fail (ex: exn) (routeInfo: RouteInfo<HttpRequest>) (options: RemotingOptions<HttpRequest, 't>) (req:HttpRequest) : Task<IActionResult option> =
        let logger = options.DiagnosticsLogger
        match options.ErrorHandler with
        | None -> setJsonBody (Errors.unhandled routeInfo.methodName) 500 logger req
        | Some errorHandler ->
            match errorHandler ex routeInfo with
            | Ignore -> setJsonBody (Errors.ignored routeInfo.methodName) 500 logger req
            | Propagate error -> setJsonBody (Errors.propagated error) 500 logger req
    
    let halt: IActionResult option = None
    
    let buildFromImplementation<'impl> (implBuilder: HttpRequest -> 'impl) (options: RemotingOptions<HttpRequest, 'impl>) =
        let proxy = makeApiProxy options
        fun (req:HttpRequest) ->
            async  {
                let isProxyHeaderPresent = req.Headers.ContainsKey "x-remoting-proxy"
                let isBinaryEncoded =
                    req.Headers.ContainsKey "Content-Type" &&
                    req.Headers["Content-Type"] |> Seq.contains "application/octet-stream"
                
                let props = { ImplementationBuilder = (fun () -> implBuilder req); EndpointName = path req; Input = req.Body; IsProxyHeaderPresent = isProxyHeaderPresent;
                    HttpVerb = req.Method.ToUpper (); IsContentBinaryEncoded = isBinaryEncoded }
                
                match! proxy props with
                | Success (isBinaryOutput, output) ->
                    use output = output
                    let resp = 
                        if isBinaryOutput && isProxyHeaderPresent then
                            FileContentResult(output.ToArray(),"application/octet-stream") :> IActionResult
                        elif options.ResponseSerialization = SerializationType.Json then
                            let responseBody = System.Text.Encoding.UTF8.GetString (output.ToArray ())
                            ContentResult(Content=responseBody, StatusCode=200, ContentType = "application/json; charset=utf-8") :> IActionResult
                        else
                            let responseBody = System.Text.Encoding.UTF8.GetString (output.ToArray ())
                            ContentResult(Content=responseBody, StatusCode=200, ContentType = "application/msgpack") :> IActionResult
                    
                    return Some resp
                | Exception (e, functionName, requestBodyText) ->
                    let routeInfo = { methodName = functionName; path = path req; httpContext = req; requestBodyText = requestBodyText }
                    return! fail e routeInfo options req |> Async.AwaitTask
                | InvalidHttpVerb -> return halt
                | EndpointNotFound ->
                    match req.Method.ToUpper(), options.Docs with
                    | "GET", (Some docsUrl, Some docs) when docsUrl = (path req) ->
                        let (Documentation(docsName, docsRoutes)) = docs
                        let schema = Docs.makeDocsSchema typeof<'impl> docs options.RouteBuilder
                        let docsApp = DocsApp.embedded docsName docsUrl schema
                        return! htmlString docsApp req |> Async.AwaitTask
                    | "OPTIONS", (Some docsUrl, Some docs)
                        when sprintf "/%s/$schema" docsUrl = (path req)
                          || sprintf "%s/$schema" docsUrl = (path req) ->
                        let schema = Docs.makeDocsSchema typeof<'impl> docs options.RouteBuilder
                        let serializedSchema = schema.ToString(Formatting.None)
                        return! text serializedSchema req |> Async.AwaitTask
                    | _ -> return halt
            }
            |> Async.StartAsTask
            

module Remoting =

  /// Builds a HttpRequest -> IActionResult option function from the given implementation and options
  /// Please see IActionResult.fromRequestHandler for using output of this function
  let buildRequestHandler (options: RemotingOptions<HttpRequest, 't>) =
    match options.Implementation with
    | StaticValue impl -> FuncsUtil.buildFromImplementation (fun _ -> impl) options
    | FromContext createImplementationFrom -> FuncsUtil.buildFromImplementation createImplementationFrom options
    | Empty -> fun _ -> async { return None } |> Async.StartAsTask

module FunctionsRouteBuilder =
    /// Default RouteBuilder for Azure Functions running HttpTrigger on /api prefix
    let apiPrefix = sprintf "/api/%s/%s"
    /// RouteBuilder for Azure Functions running HttpTrigger without any prefix
    let noPrefix = sprintf "/%s/%s"

module IActionResult =
    
    let rec private chooseHttpResponse (fns:(HttpRequest -> Task<IActionResult option>) list) =
        fun (req:HttpRequest) ->
            async {
                match fns with
                | [] -> return NotFoundResult() :> IActionResult
                | func :: tail ->
                    let! result = func req |> Async.AwaitTask
                    match result with
                    | Some r -> return r
                    | None -> return! chooseHttpResponse tail req
            }
    
    /// Build IActionResult from builder functions and HttpRequest
    /// This functionality is very similar to choose function from Giraffe
    let fromRequestHandlers (req:HttpRequest) (fns:(HttpRequest -> Task<IActionResult option>) list)  : Task<IActionResult> =
        chooseHttpResponse fns req |> Async.StartAsTask
    
    /// Build IActionResult from single builder function and HttpRequest
    let fromRequestHandler (req:HttpRequest) (fn:HttpRequest -> Task<IActionResult option>)  : Task<IActionResult> =
        fromRequestHandlers req [fn]

the only thing I had to do is to instatiate my service inside the function itself. (or if you use an object for dependency injection, I think it's suffient to set the "service" inside the object itself)

    open Fable.Remoting.AzureFunctions.OldSchool
    
    
    [<FunctionName("RemotingEndpoint")>]
    let RemotingEndpoint ([<HttpTrigger(AuthorizationLevel.Anonymous, Route = "{*any}")>]req: HttpRequest, log: ILogger) =
        log.LogDebug "RemotingEndpoint called"

        let myAwesomeApi: MyAwesomeApi = {
            getRandomWordList = (fun () -> async { return [ "Word!"; "Word2" ] })
        }

        Remoting.createApi()
            |> Remoting.withRouteBuilder Shared.Remoting.routeBuilder
            |> Remoting.fromValue myAwesomeApi
            |> Remoting.buildRequestHandler
            |> IActionResult.fromRequestHandler req

I am not sure how to integrate this as a pull request. Folder and nuget "building".

So I leave this solution here. :)

@Zaid-Ajaj
Copy link
Owner

Hi @DieselMeister thanks for the issue and the implementation! I'll try to make a nuget out of this when time permits. Any target framework requirements that I need to take into account when making a package or can I just target netstandard2.0?

@DieselMeister
Copy link
Author

Oh damn, I missed your message. So Sorry :( ...

That's actually a good question.

It's using the package: "Microsoft.AspNetCore.Http" this is netstandard2.0 (HttpRequest)
and "Microsoft.AspNetCore.Mvc" which is also netstandard 2.0 (IActionResult)

It should work with netstandard2.0.

@AlbertoDePena
Copy link

This looks very useful. Any way this can be provided as a nuget?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants