Code Monkey home page Code Monkey logo

fable.remoting's Introduction

Fable.Remoting

Build Status Build status

Fable.Remoting is a RPC communication layer for Fable and .NET apps, it abstracts away Http and Json and lets you think of your client-server interactions only in terms of pure stateless functions that are statically checked at compile-time:

Define a shared interface

This interface is a record type where each field is either Async<'T> or a function that returns Async<'T>

type IGreetingApi = {
  greet : string -> Async<string>
}

Implement the interface on the server

let greetingApi = {
  greet = fun name ->
    async {
      let greeting = sprintf "Hello, %s" name
      return greeting
    }
}

// Expose the implementation as a HTTP service
let webApp =
  Remoting.createApi()
  |> Remoting.fromValue greetingApi

Call the functions from the client

// get a typed-proxy for the service
let greetingApi =
  Remoting.createApi()
  |> Remoting.buildProxy<IGreetingApi>

// Start using the service
async {
  let! message = greetingApi.greet "World"
  printfn "%s" message // Hello, World
}

That's it, no HTTP, no JSON and it is all type-safe.

Applications using Remoting

  • SAFE-TodoList A simple full-stack Todo list application (beginner)
  • tabula-rasa a real-world-ish blogging platform (intermediate)
  • Yobo Yoga Class Booking System implemented with Event Sourcing (advanced)

The library runs everywhere on the backend: As Suave WebPart, as Giraffe/Saturn HttpHandler or any other framework as Asp.NET Core middleware. Clients can be Fable or .NET application.

"Fable.Remoting solves the age-old problem of keeping your front-end code in sync with your backend code at compile time, and in a language as enjoyable to use as F#" - David Falkner

Quick Start

Use the SAFE Simplified template where Fable.Remoting is already set up and ready to go

Available Packages:

Library Version
Fable.Remoting.MsgPack Nuget
Fable.Remoting.Client Nuget
Fable.Remoting.Json Nuget
Fable.Remoting.Server Nuget
Fable.Remoting.Suave Nuget
Fable.Remoting.Giraffe Nuget
Fable.Remoting.AspNetCore Nuget
Fable.Remoting.DotnetClient Nuget
Fable.Remoting.AzureFunctions.Worker Nuget

Scaffold from scratch - Suave

Create a new F# console app:

dotnet new console -lang F#

Define the types you want to share between client and server:

// SharedTypes.fs
module SharedTypes

type Student = {
    Name : string
    Age : int
}

// Shared specs between Server and Client
type IStudentApi = {
    studentByName : string -> Async<Student option>
    allStudents : unit -> Async<list<Student>>
}

The type IStudentApi is very important, this is the specification of the protocol between your server and client. Fable.Remoting expects such type to only have functions returning Async on the final result:

Async<A>
A -> Async<B>
A -> B -> Async<C>
// etc...

Try to put such types in seperate files to reference these files later from the Client

Then provide an implementation for IStudentApi on the server:

open SharedTypes

let getStudents() =
  async {
    return [
        { Name = "Mike";  Age = 23; }
        { Name = "John";  Age = 22; }
        { Name = "Diana"; Age = 22; }
    ]
  }

let findStudentByName name =
  async {
    let! students = getStudents()
    let student = List.tryFind (fun student -> student.Name = name) students
    return student
  }

let studentApi : IStudentApi = {
    studentByName = findStudentByName
    allStudents = getStudents
}

Now that we have the implementation studentApi, you can expose it as a web service from different web frameworks. We start with Suave

Install the library from Nuget using Paket:

paket add Fable.Remoting.Suave --project /path/to/Project.fsproj

Create a WebPart from the value studentApi

open Suave
open Fable.Remoting.Server
open Fable.Remoting.Suave

let webApp : WebPart =
    Remoting.createApi()
    |> Remoting.fromValue studentApi
    |> Remoting.buildWebPart

// start the web server
startWebServer defaultConfig webApp

Yes, it is that simple. You can think of the webApp value as if it was the following in pseudo-code:

let webApp =
 choose [
  POST
   >=> path "/IStudentApi/studentByName"
   >=> (* deserialize request body (from json) *)
   >=> (* invoke studentApi.getStudentByName with the deserialized input *)
   >=> (* give client the output back serialized (to json) *)

 // other routes
 ]

You can enable diagnostic logging from Fable.Remoting.Server (recommended) to see how the library is doing it's magic behind the scenes :)

let webApp =
    Remoting.createApi()
    |> Remoting.fromValue studentApi
    |> Remoting.withDiagnosticsLogger (printfn "%s")
    |> Remoting.buildWebPart

AspNetCore Middleware

Install the package from Nuget using paket

paket add Fable.Remoting.AspNetCore --project /path/to/Project.fsproj

Now you can configure your remote handler as AspNetCore middleware

let webApp =
    Remoting.createApi()
    |> Remoting.fromValue studentApi

let configureApp (app : IApplicationBuilder) =
    // Add Remoting handler to the ASP.NET Core pipeline
    app.UseRemoting webApp

[<EntryPoint>]
let main _ =
    WebHostBuilder()
        .UseKestrel()
        .Configure(Action<IApplicationBuilder> configureApp)
        .Build()
        .Run()
    0

Giraffe

You can follow the Suave part up to the library installation, where it will become:

paket add Fable.Remoting.Giraffe --project /path/to/Project.fsproj

Now instead of a WebPart, by opening the Fable.Remoting.Giraffe namespace, you will get a HttpHandler from the value server:

open Giraffe
open Fable.Remoting.Server
open Fable.Remoting.Giraffe

let webApp : HttpHandler =
    Remoting.createApi()
    |> Remoting.fromValue studentApi
    |> Remoting.buildHttpHandler

let configureApp (app : IApplicationBuilder) =
    // Add Giraffe to the ASP.NET Core pipeline
    app.UseGiraffe webApp

let configureServices (services : IServiceCollection) =
    // Add Giraffe dependencies
    services.AddGiraffe() |> ignore

[<EntryPoint>]
let main _ =
    WebHostBuilder()
        .UseKestrel()
        .Configure(Action<IApplicationBuilder> configureApp)
        .ConfigureServices(configureServices)
        .Build()
        .Run()
    0

Saturn

You can use the same webApp generated by the Giraffe library.

open Saturn
open Fable.Remoting.Server
open Fable.Remoting.Giraffe

let webApp : HttpHandler =
    Remoting.createApi()
    |> Remoting.fromValue studentApi
    |> Remoting.buildHttpHandler

let app = application {
    url "http://127.0.0.1:8083/"
    use_router webApp
}

run app

Azure Functions (isolated)

To use Azure Functions in isolated mode with custom HttpTrigger as serverless remoting server, just install:

dotnet add package Fable.Remoting.AzureFunctions.Worker

or using paket

paket add Fable.Remoting.AzureFunctions.Worker --project /path/to/Project.fsproj

Since Azure Functions don't know anything about HttpHandler we need to use built-in HttpRequestData and HttpResponseData objects. Luckily we have Remoting.buildRequestHandler and HttpResponseData.fromRequestHandler functions to the rescue:

open Fable.Remoting.Server
open Fable.Remoting.AzureFunctions.Worker
open Microsoft.Azure.Functions.Worker
open Microsoft.Azure.Functions.Worker.Http
open Microsoft.Extensions.Logging

type Functions(log:ILogger<Functions>) =
    
    [<Function("Index")>]
    member _.Index ([<HttpTrigger(AuthorizationLevel.Anonymous, Route = "{*any}")>] req: HttpRequestData, ctx: FunctionContext) =
        Remoting.createApi()
        |> Remoting.withRouteBuilder FunctionsRouteBuilder.apiPrefix
        |> Remoting.fromValue myImplementation
        |> Remoting.buildRequestHandler
        |> HttpResponseData.fromRequestHandler req

Of course, having one implementation per Function App is not ideal, so HttpResponseData.fromRequestHandlers is here to the rescue:

type Functions(log:ILogger<Functions>) =
    
    [<Function("Index")>]
    member _.Index ([<HttpTrigger(AuthorizationLevel.Anonymous, Route = "{*any}")>] req: HttpRequestData, ctx: FunctionContext) =
        let handlerOne =
            Remoting.createApi()
            |> Remoting.withRouteBuilder FunctionsRouteBuilder.apiPrefix
            |> Remoting.fromValue myImplementationOne
            |> Remoting.buildRequestHandler
        
        let handlerTwo =
            Remoting.createApi()
            |> Remoting.withRouteBuilder FunctionsRouteBuilder.apiPrefix
            |> Remoting.fromValue myImplementationTwo
            |> Remoting.buildRequestHandler
        
        [ handlerOne; handlerTwo ] |> HttpResponseData.fromRequestHandlers req

Fable Client

Install Fable.Remoting.Client from nuget using Paket:

paket add Fable.Remoting.Client --project /path/to/Project.fsproj

Reference the shared types to your client project

<Compile Include="path/to/SharedTypes.fs" />

Start using the library:

open Fable.Remoting.Client
open SharedTypes

// studentApi : IStudentApi
let studentApi =
    Remoting.createApi()
    |> Remoting.buildProxy<IStudentApi>

async {
  // students : Student[]
  let! students = studentApi.allStudents()
  for student in students do
    // student : Student
    printfn "Student %s is %d years old" student.Name student.Age
}
|> Async.StartImmediate

Finally, when you are using webpack-dev-server, you have to change the config from this:

devServer: {
  contentBase: resolve('./public'),
  port: 8080
}

to this:

devServer: {
  contentBase: resolve('./public'),
  port: 8080,
  proxy: {
    '/*': { // tell webpack-dev-server to re-route all requests from client to the server
      target: "http://localhost:5000",// assuming the backend server is hosted on port 5000 during development
      changeOrigin: true
    }
}

That's it!

Dotnet Client

You can also use client functionality in non-fable projects, such as a console, desktop or mobile application.

Install Fable.Remoting.DotnetClient from nuget using Paket:

paket add Fable.Remoting.DotnetClient --project /path/to/Project.fsproj

Reference the shared types in your client project

<Compile Include="path/to/SharedTypes.fs" />

Start using the library:

open Fable.Remoting.DotnetClient
open SharedTypes

// studentApi : IStudentApi
let studentApi =
  Remoting.createApi "http://localhost:8085"
  |> Remoting.buildProxy<IStudentApi>

async {
  // students : Student[]
  let! students = studentApi.allStudents()
  for student in students do
    // student : Student
    printfn "Student %s is %d years old" student.Name student.Age
}
|> Async.StartImmediate

Notice here, that unlike the Fable client, you will need to provide the base Url of the backend because the dotnet client will be deployed separately from the backend.

Adding a new route

  • Add another record field function to IStudentApi
  • Implement that function
  • Restart server and client

Done! You can now use that function from the client too.

See the following article if you are interested in how this library is implemented (a bit outdated but gives you an overview of the mechanism) Statically Typed Client-Server Communication with F#: Proof of Concept

fable.remoting's People

Contributors

0x53a avatar bklop avatar blair55 avatar dawedawe avatar dependabot[bot] avatar do-wa avatar drk-mtr avatar dzoukr avatar elonon avatar forki avatar irium avatar jmaharman avatar juselius avatar kerams avatar marcpiechura avatar martinbryant avatar matthewcrews avatar mvsmal avatar nhowka avatar nojaf avatar onurgumus avatar pavelm avatar pierreyvesr avatar shmew avatar sistracia avatar smoothdeveloper avatar strigoenturbano avatar tforkmann avatar theimowski avatar zaid-ajaj avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fable.remoting's Issues

Using Fable.Remoting in multiple server scenarios

The documentation seems to show only examples where there is only one server, and client and server are in the same machine.
Is there an example where a client can call multiple different servers in different networked computers?
For example, how can I create multiple proxies in a client like in

let proxy1 = createApi("10.129.2.13")
let proxy2 = createApi("www.contoso.com")
let len1 = proxy1.getLength "String1"
let len2 = proxy2.getLength "String2"

Compilation warnings for Client

After upgrading Fable.Remoting.Client to 2.1 I get a few warnings on every compilation.
It complains about some obsolete calls used in the internals of the library:

WARNING in ~/.nuget/packages/fable.remoting.client/2.1.0/fable/Proxy.fs
~/.nuget/packages/fable.remoting.client/2.1.0/fable/Proxy.fs(344,12): (344,32) warning FSHARP: This construct is deprecated. For backward compatibility only. @ ./Client.fs 9:0-107
 @ ./Client.fsproj
 @ multi (webpack)-dev-server/client?http://localhost:8080 webpack/hot/dev-server ./Client.fsproj

WARNING in ~/.nuget/packages/fable.remoting.client/2.1.0/fable/Proxy.fs
~/.nuget/packages/fable.remoting.client/2.1.0/fable/Proxy.fs(345,12): (345,37) warning FSHARP: This construct is deprecated. For backward compatibility only. @ ./Client.fs 9:0-107
 @ ./Client.fsproj
 @ multi (webpack)-dev-server/client?http://localhost:8080 webpack/hot/dev-server ./Client.fsproj

WARNING in ~/.nuget/packages/fable.remoting.client/2.1.0/fable/Proxy.fs
~/.nuget/packages/fable.remoting.client/2.1.0/fable/Proxy.fs(346,12): (346,47) warning FSHARP: This construct is deprecated. For backward compatibility only. @ ./Client.fs 9:0-107
 @ ./Client.fsproj
 @ multi (webpack)-dev-server/client?http://localhost:8080 webpack/hot/dev-server ./Client.fsproj

WARNING in ~/.nuget/packages/fable.remoting.client/2.1.0/fable/Proxy.fs
~/.nuget/packages/fable.remoting.client/2.1.0/fable/Proxy.fs(347,12): (347,61) warning FSHARP: This construct is deprecated. For backward compatibility only. @ ./Client.fs 9:0-107
 @ ./Client.fsproj
 @ multi (webpack)-dev-server/client?http://localhost:8080 webpack/hot/dev-server ./Client.fsproj

WARNING in ~/.nuget/packages/fable.remoting.client/2.1.0/fable/Proxy.fs
~/.nuget/packages/fable.remoting.client/2.1.0/fable/Proxy.fs(348,12): (348,51) warning FSHARP: This construct is deprecated. For backward compatibility only. @ ./Client.fs 9:0-107
 @ ./Client.fsproj
 @ multi (webpack)-dev-server/client?http://localhost:8080 webpack/hot/dev-server ./Client.fsproj

If you define an interface with `T seq` return type, Fable.Remoting doesn't seem to deserialize DUs properly.

Full repro here: https://github.com/AkosLukacs/fable-remoting-seq-type Just clone & run.

If you define an interface with T seq return type, Fable.Remoting doesn't seem to deserialize DUs properly. Deserialization works just fine with T list

// interface in Shared.fs
type ICounterApi =
    {
      test: unit -> Async<Dummyx list>
      testSeq: unit -> Async<Dummyx seq>
    }

// implementation in Server.fs.
let counterApi = {
    test = fun () -> async {
        return [
            {Id = 1; First = SimpleDU.FirstCase; Second = SimpleDU.SecondCase; Third = SimpleDU.ThirdCase; Complex = ComplexDU.B "test"}
        ]
    }
    testSeq = fun () -> async {
        return List.toSeq [
            {Id = 1; First = SimpleDU.FirstCase; Second = SimpleDU.SecondCase; Third = SimpleDU.ThirdCase; Complex = ComplexDU.B "testSeq"}
        ]
    }
}

Server response as seen on Network tab of the browser is same in both cases:

[{"Id":1,"First":"FirstCase","Second":"SecondCase","Third":"ThirdCase","Complex":{"B":"test"}}]
[{"Id":1,"First":"FirstCase","Second":"SecondCase","Third":"ThirdCase","Complex":{"B":"testSeq"}}]

If you console log the result, shows different result for different interface definitions:

// GotTest data:
{โ€‹โ€‹
    Complex: Object { tag: 1, data: "test" }โ€‹โ€‹
    First: Object { tag: 0 }โ€‹โ€‹
    Id: 1โ€‹โ€‹
    Second: Object { tag: 1 }โ€‹โ€‹
    Third: Object { tag: 2 }
}

// GotTestSeq data:
{
    Complex: Object { B: "testSeq" }
    First: "FirstCase"    โ€‹โ€‹
    Id: 1    โ€‹โ€‹
    Second: "SecondCase"    โ€‹โ€‹
    Third: "ThirdCase"
}

Roadmap and next steps

After the recent discussion and a review on the overall architecture of the library, I have decided to make a small rewrite and here are my thoughts on what next steps will be taken and the reason behind them

  • Removing the CE based Api
    It was a nice addition to the previous api, which was just a function that takes in many parameters. It makes the internal structure more complicated than it should be and it is just syntactic addition. It will be replaced with good old method chaining:
type IMusicStore = { bestAlbums : Async<Album list> }
 
let musicStore = { bestAlbums = Db.getBestAlbums() } 

let musicStoreApi : WebPart = 
   Remoting.createApi()
   |> Remoting.withRouteBuilder (sprintf "/api/%s/%s")
   |> Remoting.fromValue musicStore 
   |> Remoting.buildWebPart
  • Removing the Response Override type
    Along with custom handlers, they don't really fit in the core idea of the library of abstracting http away, if it is needed, then just use raw http instead
  • Mapping record fields of type Async<'t> or unit -> Async<'t> to GET instead of POST request because they don't require input <-> no request body needed so it becomes natural this way
  • Rename Fable.Remoting.Middleware to Fable.Remoting.AspNetCore and publish to make the library run eveywhere (Giraffe, Saturn, Freya, Nancy etc.)
  • Integrate with the dependency injection system of Aspnet core with simple way to resolve dependencies with a simple reader monad
let musicStore = reader {
    let! musicDb = resolve<IMusicDb>()
    let! securityOps = resolve<ISecurityOps>() 
    let! context = resolve<HttpContext>() 
    return { bestAlbums = async { return musicDb.getAwesomeAlbums() } }
}

let musicStoreApi = 
    Remoting.createApi()
    |> Remoting.withRouteBuilder (sprintf "/api/%s/%s")
    |> Remoting.fromReader musicStore 
    |> Remoting.build

// later at IApplicationBuidler
app.UseRemoting(musicStoreApi)

where fromReader is just a syntactic sugar around HttpContext -> 't

  • Add optional dependency injection mechanism to Suave
    Suave doesn't have it's own DI mechanism but we could add it to Suave as an optional feature with packages Fable.Remoting.AutoFac, Fable.Remoting.Ninject to allow use of these ioc containers for Suave like this:
  • Remove the contextual handler in favor of the reader api
  • Migrate the Client to Fable 2 when Fable 2 becomes stable, the Json converter is already implemented in Fable.SimpleJson and hopefully doesn't require any changes in Fable.Remoting.Json because it understands JSON representation of Fable 1 as well

These were my thoughts, I will be able to start working on the rewrite later this week

Fable.Remoting.Json 2.0.0 is not released

I tried to add Fable.Remoting.Giraffe 2.0 to SAFE-template, but the dependency on Fable.Remoting.Json 2.0 is unsatisfied:

Paket failed with
-> There was a version conflict during package resolution.
     Resolved packages:
      - Fable.Remoting.Giraffe 2.0.0
      - FSharp.Core 4.3.4
      - Microsoft.DotNet.Watcher.Tools 1.0.0
      - Saturn 0.4.3
     Conflict detected:
      - Fable.Remoting.Giraffe 2.0.0 requested package Fable.Remoting.Json: >= 2.0
      - Available versions:
        - (1.3, [https://api.nuget.org/v3/index.json])
        - (1.3.0, [https://api.nuget.org/v3/index.json])
        - (1.2.0, [https://api.nuget.org/v3/index.json])
        - (1.1.0, [https://api.nuget.org/v3/index.json])
        - (1.0.0, [https://api.nuget.org/v3/index.json])

Hard to use APIs due to lack of parameter names

Do I understand rightly that the following API method is idiomatic?

image

If so, I find it very inconvenient to use because it lacks parameter names. The only way to improve it I could think about is adding a lot of type aliases, so it could look like this:

type Host = string
type App = string
type Op = string
PerformOp: Host * App * Op -> Async<unit>

Despite it looks nicely, adding the type aliases feels more like a hack around the language limitation.

Make HttpContext available from ErrorHandler

Instead of

Exception -> RouteInfo -> ErrorResult

I think it would be much nicer to have

Exception -> HttpContext -> ErrorResult

There are many reasons why more context information is needed, for example, the ErrorHandler is where most people put their logging logic when things go wrong. When that happens, being able to trace back the request using the context is extremely convenient.

@Nhowka What do you think?

Related:

Make GET requests from Fable client when possible

Although the server adapters can handler POST-only requests, use GET for record fields of type Async<'t> or 'u -> Async<'t> when isUnit typeof<'u> from the client side too for a coherent RPC story

TypeError: $x$$3.CompareTo is not a function

I have TypeError: $x$$3.CompareTo is not a function receiving a list of records.

The record contains string and lists of other records, all of them contains fields of primitive types only, no Sets or Maps.

I turn on the diagnostics on server and it wrote it sent completely nice JSON to the client.

How to diagnose this error further?

Update docs

Just so I don't forget

  • Update template parameters in README and guide
  • Add docs about the documentation API

Simplify logger output

Logger logs too many arguments with null values, doesn't affect execution but should be cleaned up.

Simplify type name output, for example:

Fable.Remoting: Invoking method listIntegers
Fable.Remoting:
About to deserialize JSON:
[[1,2,3,4,5,6,7,8,9,10]]
Into .NET Types: [Microsoft.FSharp.Collections.FSharpList`1[[System.Int32, System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]]

Should be

Fable.Remoting: Invoking method listIntegers
Fable.Remoting:
About to deserialize JSON:
[[1,2,3,4,5,6,7,8,9,10]]
Into .NET Types: [FSharpList<System.Int32>]

Async.Bind fails for remoting client if the protocol returns an array

Given following sample:

Shared

namespace Shared

module Route =
    let builder typeName methodName =
        sprintf "/api/%s/%s" typeName methodName

type HighScore =
    { Name : string
      Score : int }

type IGameApi =
    { getHighScores : unit -> Async<HighScore array> }

Server (Saturn):

let getHighScores() : Task<_> =
    task {
        return
            [|
                { Name = "alfonsogarciacaro"; Score =  100 }
                { Name = "theimowski"; Score =  28 }
            |]
    }

let gameApi = {
    getHighScores = getHighScores >> Async.AwaitTask
}

let webApp =
    Remoting.createApi()
    |> Remoting.withRouteBuilder Route.builder
    |> Remoting.fromValue gameApi
    |> Remoting.buildHttpHandler

let app = application {
    url ("http://0.0.0.0:" + port.ToString() + "/")
    use_router webApp
    memory_cache
    use_static publicPath
    use_gzip
}

run app

and Client:

module Server =

    open Shared
    open Fable.Remoting.Client

    /// A proxy you can use to talk to server directly
    let api : IGameApi =
      Remoting.createApi()
      |> Remoting.withRouteBuilder Route.builder
      |> Remoting.buildProxy<IGameApi>

async {
    Browser.window.alert "before"
    let! scores = Server.api.getHighScores ()
    Browser.window.alert "after"
} |> Async.StartImmediate

The after msg does not appear in console.
If I change the protocol to return HighScore instead of HighScore array, the code works properly.

Edit: Just found that if I replace HighScore array with HighScore list the code works fine as well. So it seems it's just an issue with array.

ASP.NET Core sample not working out

Hi, I'm trying to setup remoting with ASP.NET Core.

let remotingApp = 
    Remoting.createApi()
    |> Remoting.fromValue ronnyService
Value restriction. The value 'remotingApp' has been inferred to have generic type
    val remotingApp : RemotingOptions<'_a,Shared.Domain.IRonnyService>    
Either define 'remotingApp' as a simple data term, make it a function with explicit arguments or, if you do not intend for it to be generic, add a type annotation.
val remotingApp : RemotingOptions<obj,Shared.Domain.IRonnyService>
Full name: Ronnies.Server.Api.remotingApp
Assembly: Ronnies.Server
    let configureApp (app : IApplicationBuilder) =
        app.UseStaticFiles() 
        |>  fun a -> a.UseFreya (Api.webApp)
        |>  fun a -> a.UseRemoting(Api.remotingApp)
Type mismatch. Expecting a 'RemotingOptions<Http.HttpContext,'a>' but given a 'RemotingOptions<obj,Shared.Domain.IRonnyService>' The type 'Http.HttpContext' does not match the type 'obj'

What am I missing here?

How to access HttpContext in the Giraffe wrapper?

I have some handlers that need to access HttpContext, like this:

route "/user" (fun next ctx ->
    if ctx.User.Identity.IsAuthenticated then
        json (Some ctx.User.Identity.Name) next ctx
    else json None next ctx
)

Is it possible to port it?

Paket versions mismatch

I'm having issues with installing Fable.Remoting.Suave alongside Suave:

Paket failed with
-> There was a version conflict during package resolution.
     Resolved packages:
      - Fable.Remoting.Suave 2.6.0
     Conflict detected:
      - Suave 2.3.0-beta3 requested package FSharp.Core: >= 4.0.0.1 < 4.0.1*
      - Fable.Remoting.Suave 2.6.0 requested package FSharp.Core: >= 4.3.4

I'm actually building for netstandard 2.0, but on ARM architecture (Rasblerry Pi). Not everything is polished for ARM yet.

Is there a reason for the upper limit on version of FSharp.Core?

byte array does not work

My models look like this:

type Mesh = {
    Hash: byte array // 32 bytes

    Vertices: Vector3 array
    Indices: uint32 array

    Color: string
}

/// reference a mesh
type PlacedMeshRef = {
    Hash: byte array
    Placement : ObjectPlacement
}

when I send them from server to client I get an error:

grafik

Error: "Cannot convert ["JString","MYiahJg85WmXkHFtQHhmaEoy0JvgZGZmqnE9X0/jvMs="] to ["Array",null]"

grafik

It seems the issue is that the server encodes the byte array as base64, but the client expects an actual array.

Encoding it as base64 is probably a good idea to reduce data waste, so I guess the client should be updated to special case byte arrays.

Error when passing Map with DU as Keys in Fable 2 client.

I've added two test cases:

IServer.echoConfigMap - "canonical" form. Error thrown is somewhat different than my real case, which I've implemented as 2nd test case:
IServer.returnConfigMap. Error at the client side is because it compares key name in quotes (""K1"" in the test) with key name without them ("K1") and didn't find corresponding key.

See #74

.NET Core Support

It would be great to see this running on .NET Core. What do you think would be the main challenges in getting this working?

I'm learning F# myself at the moment and looking for a project to contribute to. If it is within my capabilities I'd like to help.

Can't find WebHostBuilder in Giraffe example

Well... It's a minor thing, but I decided to ask anyway.

I tried following the manual step-by-step guide, but when I hit the section for Giraffe, the compiler complains that WebHostBuilder() is unknown. Where can I find it ?

Port Client to Fable 2.x

Json serialization/deserialization should be almost ready from Fable.SimpleJson, although there might be problems with recursive (self-referencing) types, I will need to check.

Async<'T> does not send a request in production mode

Remoting functions of type Async<'T> don't seem to send the request to server when serving the generated javascript bundle (in production mode) from the same server normally rather than from webpack-dev-server.

I always ran integration tests in webpack-dev-server but production builds seem to affect the behaviour. @Nhowka can you reproduce this? If so, this could be a Fable problem as well

Support netstandard2.0

Would it be possible to support netstandard2.0 ?

Seems like it' support netcore2.0, but I don't what this version is for.

Typo in readme

It seems that in the readme, the following example code has an extra ()

open SharedTypes

let getStudents() = async {
    return [
        { Name = "Mike";  Age = 23; }
        { Name = "John";  Age = 22; }
        { Name = "Diana"; Age = 22; }
    ]
}

let findStudentByName name = async {
    let! students = getStudents() 
    let student = List.tryFind (fun student -> student.Name = name) students
    return student 
}

let studentApi : IStudentApi = {
    studentByName = findStudentByName
    allStudents = getStudents()  // shouldn't have () here
}

Q: Ending up with two libraries to serialize things.

Hi, I'm using Fable.Remoting.Client and I noticed it has a dependency on Fable.PowerPack and Fable.SimpleJson. If you look closer Fable.PowerPack has a dependency on Thoth.Json.

So I guess in my bundle I end up with two libraries that serialize json. I'm having some mixed feelings about this. I'd like to hear your thoughts on this.

//cc @MangelMaxime

After adding Fable.Remoting am I loosing ability to render html

Hi, I am using Giraffe with Fable.Remoting. Things are working fine if I am using it with Fable.Elmish single page application.

but what if I wanted to use Giraffe as web server as well to render html pages? Is adding Fable.Remoting will remove that ability or it would be still be possible?

Control serialization of payload

Fable.Remoting works really well for normal F# types (Records, Unions etc).

Is there a way to control serialization for more complex types?


I want to send geometric models from server to client.

My model looks like this:

type P = {
    Position: System.Numerics.Vector3
    Rotation: System.Numerics.Quaternion
}

On the server-side I use https://www.nuget.org/packages/System.Numerics.Vectors/.

On the client-side I added my own replacements which model the nuget package 1 : 1:

namespace rec System.Numerics

[<Struct>]
type Vector3(x : single, y : single, z : single) =

    new (value : single) = Vector3(value, value, value)

    member __.X = x
    member __.Y = y
    member __.Z = z

    // ...


[<Struct>]
type Quaternion(x : single, y : single, z : single, w : single) =
    
    new(vectorPart : Vector3, scalarPart : single) =
        Quaternion(vectorPart.X, vectorPart.Y, vectorPart.Z, scalarPart)

    member __.X = x
    member __.Y = y
    member __.Z = z
    member __.W = w

    // ...

Trying to send such an object over the wire fails:

"Cannot convert ["JObject",{"comparer":{},"tree":["MapNode","Y",["JNumber",0],["MapOne","X",["JNumber",0]],["MapOne","Z",["JNumber",0]],2]}] to ["Any",null]"

grafik

I've worked around this issue by just representing the vectors and quaternions as simple float arrays and then converting back and forth.

Suave nuget 3.x is not working

The package Fable.Remoting.Suave doesn't seem to work. The error is related to Fable.Remoting.Json dependency not being resolved correctly

Project:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Fable.Remoting.Suave" Version="3.1.0" />
  </ItemGroup>

</Project>

Program.fs

open System
open Suave 
open Fable.Remoting.Server
open Fable.Remoting.Suave 

type Album = { 
    Id: int 
    Name: string 
}

let result = DynamicRecord.serialize ({ Id = 1; Name = "Album" })

printfn "%A" result 

When running using dotnet restore --no-cache && dotnet run I get

Unhandled Exception: System.TypeInitializationException: The type initializer for '<StartupCode$Fable-Remoting-Server>.$DynamicRecord' threw an exception. ---> System.IO.FileLoadException: Could not load file or assembly 'Fable.Remoting.Json, Version=2.1.0.0, Culture=neutral, PublicKeyToken=null'. The located assembly's
manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)
   at <StartupCode$Fable-Remoting-Server>.$DynamicRecord..cctor()
   --- End of inner exception stack trace ---
   at Fable.Remoting.Server.DynamicRecord.serialize[a](a result)
   at <StartupCode$ConsoleApp5>.$Program.main@() in C:\Users\zaidn\source\repos\ConsoleApp5\ConsoleApp5\Program.fs:line 11

dotnet version => 2.1.300
issue occurs when using paket too

Todo - more tests / code clean up

  • Saturn integration tests
  • Test projects using the Nuget package
  • Unit tests
    • Partially applied proxy functions
    • Partially applied proxy functions using complex types
    • Edge-cases for primitive types (NaN, infinity, etc)

[Discussion] Removing the contextual remoting handler

TL;DR

Remove the ability to create contextual remoting protocols to prevent people from writing untestable/hard to unit test code.

Description

We have added the ability to access and read http context data to make Fable.Remoting more flexible, the way we do it is by defining generic protocols, an implementation of the protocol gives a concrete HttpContext as the type parameter, this is explained in Accessing Request Context. You end up writing protocols of this shape:

type IMusicStore<'Context> = {
    /// return favorite albums of current logged in user
    favoriteAlbums : 'Context -> Async<Album list>
}

However, for a while now I am thinking that this should be removed and keeping the good old non-generic interfaces because it hurts the testability of the concrete implementations: there is no simple way of mocking HttpContext (in Suave, I think you must build it from scratch for testing) and I think people using remoting shouldn't be bothered with HttpContext at all if they are testing their implementations and should only be testing their functions

Alternative

The alternative already "implemented" and explained in
Logging - Section: Without extending protocol definition

That is by contructing the remoting WebPart from the context functions that give access to the context from outside the implementation.

The following example is provided in the docs (which itself is quite rare I think) is only testable at the HTTP level, so you have to actually send and requests and get responses to assert results (which in my opinion, shouldn't be the case)

type IMusicStore<'Context> = {
    /// return favorite albums of current logged in user
    favoriteAlbums : 'Context -> Async<Album list>
}

let musicStore : IMusicStore<HttpContext> = {
    // return favorite albums of current logged-in user
    favoriteAlbums = fun ctx -> async {
        // get the authorization header, if any
        let authHeader = 
            ctx.request.headers
            |> List.tryFind (fun (key, _) -> key = "authorization")
            |> Option.map snd 

        match Security.extractUser authHeader with
        | None -> return [] // no albums 
        | Some user -> 
            let! favoriteAlbums = Database.favoriteAlbums user
            return favoriteAlbums
    }
}

// WebPart
let webApp = remoting musicStore {()}

Becomes

type SecurityToken = SecurityToken of string 

type IMusicStore = {
    /// return favorite albums of current logged in user
    favoriteAlbums : Async<Album list>
}

Then the implementation becomes the following

// give me a database and a security token and get a music store 
// this is easily testable: only mock database operations and provide a security token
let createMusicStore (db: IDatabase) 
                     (security: ISecurityOps)
                     (securityToken: Option<SecurityToken>) : IMusicStore =  
    let musicStore : IMusicStore = {
        // return favorite albums of current logged-in user
        favoriteAlbums = async {
            match security.extractUser securityToken with
            | None -> return [] // no albums 
            | Some user -> return! db.favoriteAlbums user
        }
    }

    musicStore


// a web app needs a database 
// IDatabase -> ISecurityOps -> WebPart
let createWebApp (db: IDatabase) (security: ISecurityOps) = context <| fun ctx ->
    // extract token from the context
    let securityToken = 
       ctx.request.headers
        |> List.tryFind (fun (key, _) -> key = "authorization")
        |> Option.map (snd >>  SecurityToken)
    // create the store
    let musicStore = createMusicStore db security securityToken 
    // expose as web part
    remoting musicStore {()}

// actual webApp, the composition root of the app
let webApp : WebPart = 
   let db = Database.createFromProductionConfig()
   let security = Security.defaultImplementation() 
   createWebApp db security  

Your opinions please!

What do you guys think? did I miss anything or do you think there are examples where the contextual remoting handler is better than using the context function from suave or giraffe? @Nhowka @irium

Should a request sent from client to server include browser cookies?

I'm observing that a Fable.Remoting RPC POST request to the (Suave) server does not include the Cookie header, so domain cookie values are not sent. This makes session management difficult.

I am wondering is this by design? Or perhaps an XMLHttpRequest restriction? I had thought you might not have to cater for this concept in the client lib at all, as the cookie header would be included automatically by the browser...?

What do you suggest?

Stack overflow when deserialising int64

This is a remarkably simple repro, but it took a long time to track down. I'd set up some endpoints and one kept killing the server process with StackOverflowException with no hint of what or where it was failing.

It turns out (possibly a separate issue) that the client will serialise a long / int64 like this:

{low: 5, high: 0, unsigned: false}

And deserialising this using Fable.Remoting.Json.FableJsonConverter will result in a StackOverflowException.

I've put together a small repro contrasting the behaviour of the converter here and the one in Fable.JsonConverter. The last 3 all cause the same issue.

printfn "Fable.JsonConverter"
parse<int> fable "\"5\""
parse<int> fable "\"+5\""
parse<int> fable "5"
parse<int> fable "\"text\""
parse<int64> fable "\"5\""
parse<int64> fable "\"+5\""
parse<int64> fable "5"
parse<int64> fable "\"text\""
parse<int64> fable "{low: 41, high: 0, unsigned: false}"

printfn "--"

printfn "Fable.Remoting"
parse<int> remoting "\"5\""
parse<int> remoting "\"+5\""
parse<int> remoting "5"
parse<int> remoting "\"text\""
parse<int64> remoting "\"5\""
parse<int64> remoting "\"+5\""
parse<int64> remoting "5"
parse<int64> remoting "\text\""
parse<int64> remoting "{low: 41, high: 0, unsigned: false}"

The output is

Fable.JsonConverter
Success: "5" -> 5
Success: "+5" -> 5
Success: 5 -> 5
Failed: "text", Could not convert string to integer: text. Path '', line 1, position 6.
Success: "5" -> 5L
Success: "+5" -> 5L
Success: 5 -> 5L
Failed: "text", Input string was not in a correct format.
Failed: {low: 41, high: 0, unsigned: false}, Expecting int64 but got StartObject
--
Fable.Remoting
Success: "5" -> 5
Success: "+5" -> 5
Success: 5 -> 5
Failed: "text", Could not convert string to integer: text. Path '', line 1, position 6.
Success: "5" -> 5L
Success: "+5" -> 5L
Process is terminating due to StackOverflowException.

I've tried (briefly) to work out where this is broken, but I've run out of time today.

Maps with record keys deserialization

I've updated to a recent version of Fable.Remoting and am getting errors on deserialization. I've reduced the issue to maps with records as keys:

type ExampleRecord = {
    prop : int
}

type IServer = {
    recordKeyAPI : unit -> Async<Map<ExampleRecord, int>>
}

let server = {
    recordKeyAPI = fun() -> async { return Map.ofList [({prop = 2}, 3)] }
}

client:

try
    let! exampleCall = server.recordKeyAPI()
    printfn "%O" exampleCall
with ex ->
    console.error ex.Message

Then I get:

Cannot convert ["JString","{\"prop\":2}"] to ["Record",null]

Should that still work if I've got everything configured correctly?

Fable.Remoting.Client (4.5.1)
Fable.SimpleJson (2.6)

Cannot read nested union types on client side

In shared project, I have DU for errors:

type TextValue =
    | Email

type DomainError =
    | ItemAlreadyExists of TextValue

type ServerError =
    | DomainError of DomainError

type ServerResponse<'a> = Async<Result<'a, ServerError>>

and proxy defined as:

type API = {
    Register : Account -> ServerResponse<Guid>
}

On error (in Result), the payload is serialized to:

{"Error":{"DomainError":{"ItemAlreadyExists":"Email"}}}

But when handling on client side, the automatic deserialization fails with:

Cannot convert ["JObject",{"comparer":{},"tree":["MapOne","Error",["JObject",{"comparer":{},"tree":["MapOne","DomainError",["JObject",{"comparer":{},"tree":["MapOne","ItemAlreadyExists",["JString","Email"]]}]]}]]}] to ["Any",null]

Is it because of nested union types or there is problem with transfering Result types as such?

Thanks a lot for help

Allow a single JSON object as input for functions expecting a single parameter

For example, if a function is expecting a record of type Person = { name: string; age; int } then it more natural from the consumer's perspective to send just:

{ "name": "John", "age": 21 } 

instead of

[{ "name": "John", "age": 21 }]

Allowing both forms as valid input for the same function (this is not limited to records)

float32 does not work

I wanted to transmit a float32 array over Fable.Remoting, but always got

Error: "Cannot convert ["JNumber",0] to ["Any",null]"

grafik

Workaround

After changing all of them to float (64 bit), it works

Root cause

I would guess this is maybe because Fable.SimpleJson doesn't handle System.Single?

grafik

Remove application logic from Fable proxy

The following functionality will be removed to tidy up the library and slim down it's responsibilities:

  • Retry logic
  • Authentication-require callbacks
  • Custom handlers
  • the CE based api

Should be added from the consuming app as needed

Is it possible to receive request headers and/or send response headers?

I set status codes and headers in accordance with the RFCs all the time. For example, if I send back a None, I also often send back a 404 Not Found. After a POST I typically send back a 201 Created with a Location header if the result was a new resource; otherwise often a 204 No Content. Many of our responses include cache control headers, including ETag or Last-Modified, and the subsequent Conditional GET requests need to send If-None-Match or If-Modified-Since/If-Unmodified-Since.

Would it be possible to expose a Context object of some kind in order to read/set status codes and headers?

Localize Fable client errors

Errors generated from the Fable client are sent off to the global handlers that are defined when the proxy is created. This is not very helpful, because reacting to these errors locally (where it originated) using a global handler is hard

for example in Elmish app, you would have to create a subscription for different kinds of errors and propagate a message to the child program where the error originated telling it what happened

Solution: localize the errors to be able to react to them on call-site just like how the dotnet client does it, here is a snippet from the tests

(*
    calling the function:
    throwError = fun () -> async { 
        return! failwith "Generating custom server error" 
    }
    with error handler: (fun ex routeInfo -> Propagate ex.Message)
*)
testCaseAsync "IServer.throwError using callSafely" <| async {
    let! result = proxy.callSafely <@ fun server -> server.throwError() @> 
    match result with 
    | Ok value -> failwithf "Got value %A where an error was expected" value
    | Result.Error ex -> 
        match ex with 
        | :? Http.ProxyRequestException as reqEx ->
            let statusCode = reqEx.StatusCode 
            Expect.equal HttpStatusCode.InternalServerError statusCode "The status code is 500"
            let! responseText = reqEx.ReadResponseContent() 
            let json = JToken.Parse(responseText) 
            Expect.isFalse (json.["ignored"].ToObject<bool>()) "Error was not ignored"
            Expect.isTrue (json.["handled"].ToObject<bool>()) "Error was handled" 
            Expect.equal (json.["error"].ToString()) "Generating custom server error" "The error was propagated"
        | other -> Expect.isTrue false "Should not happen"
}

RemotingFailure Error: Cannot convert "JNull" to "Unit"

Servers sends:

Fable.Remoting: Returning serialized result back to client
{"Ok":null}

And return type is

type SecureResponse<'t> = Result<'t, AuthenticationError>
Async<SecureResponse<unit>>

RemotingFailure Error: Cannot convert "JNull" to "Unit".
Using latest fable2 bits.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.