6
comments
on 12/7/2016 5:23 AM

This article is part of F# Advent 2016

Introduction

Often some functionality of a web application is split between the server and client layers. For example you want a separate component on the page which needs server interaction and package it to reuse in multiple projects. Sometimes you need to talk to the server to be which is hosting the current application, sometimes another one that hosts a service for multiple web applications.

We will look at the tools that WebSharper provides to achieve all this and some more using only F#.

There is just a fresh release of WebSharper 4 beta out which contains fixes for these features, be sure to grab Zafir beta-5 packages from NuGet or get the Visual Studio installer for project templates here. For a short introduction, read The road to WebSharper 4.

WebSharper remoting

We will go into a couple new features of WebSharper 4 beta later, but start with a long-time basis of WebSharper: RPCs. Just having a Remote annotated method exposes it to be callable from client-side code which is translated to JavaScript:

1
2
3
4
5
6
7
8
9
10
[<Remote>] // runs on the server
let GetData key = async { return DataStore.[key] } 

[<JavaScript>] // runs in the browser
let DataView key =
	async { 
		let! data = GetData key // calls server asynchronously
		return div [ text data ] // creates the view when response has arrived
	} |> Doc.Async // converts to an `Async<Doc>` to `Doc` value that can be embedded
				   // on a page and displays content as soon as it is available

This is transparent and lightweigth way of communicating with the server, no larger state is sent back to the server, only what you explicitly pass and a user session cookie.

Let's take a look at what is happening here behind our backs:

  • The client-side gets constructs an instance of a "RemotingProvider" object. By default it is the AjaxRemotingProvider defined in WebSharper.
  • The Async method of the RemotingProvider is called with with the RPC method handle (auto-generated) and arguments. There are separate functions for calling Rpcs that return an async a Task and unit, but all use a method AsyncBase for common logic. The method handle is something like MyApp:MyApp.Server.GetData:-1287498065, containing the assembly name, full path of method and a hash of the method's signature.
  • The default implementation of AsyncBase sends a XMLHttpRequest to the client with the JSON-serialized form of the arguments.
  • Server handles the request: looks up the method based on the handle, deserializes the arguments to .NET values and executes the method.
  • Server serializes result based on metadata information which tells how they are represented in the JavaScript translation, and sends this back in the response.
  • Client deserializes JSON response into objects and applies prototypes.
  • Async continuation is called, or in case of a server error, the Error value will be propagated as in the async workflow.

So calling a remote is not a safe operation as it can throw an exception, but we can catch it:

1
2
3
4
5
6
7
8
	async {  
		try 
			let! data = GetData key 
			return div [ text data ]  
		with e ->
			Console.Log("GetData error:", e)
			return div [ text "Error" ]
	} |> Doc.Async

But it is a not a nice functional approach to rely on exceptions. There is a way to catch the error around every RPC call automatically.

Customizing the client request

We can inherit from the default AjaxRemotingProvider and override the AsyncBase member which is has the common logic to handle calling RPC methods :

1
2
3
4
5
6
7
8
9
[<JavaScript>]
type SafeRemotingProvider() =
    inherit Remoting.AjaxRemotingProvider()

	override this.AsyncBase(handle, data) =
        async.TryWith(base.AsyncBase(handle, data), 
            fun e -> 
                Console.Log("Remoting exception", handle, e)
                async.Return(box None)

This does not knows about the RPC method is actually returning, so it is just assuming that it is an option so None is a correct value. So we still need to be a bit careful to apply it only to Remote methods which are indeed returning an option.

1
2
3
4
5
6
7
[<Remote; RemotingProvider(typeof<SafeRemotingProvider>)>]
let GetData key =
    async { 
		match DataStore.TryGetValue(key) with 
		| true, c -> return Some c
		| _ -> return NOne
	} 

A good practice would be to have all RPC methods return an option (or Result or similar union) value and use the RemotingProvider attribute on the module defining the server-side functions, or even on assembly level.

Setting the target server

The default AsyncBase looks up the Endpoint property on the same object, which by default reads a module static value WebSharper.Remoting.Endpoint. This gives us a couple possibilities:

  • If we want all remoting to target a server with a known URL, we can just set WebSharper.Remoting.Endpoint in the client side startup.
  • If we want to host some server-side functionality as a separate service used by multiple web applications, we can put the it in a library project and apply a custom RemotingProvider class for the assembly that overrides the Endpoint property. You can use the WebSharper.Web.Remoting.AddAllowedOrigin method on server startup to allow access to the RPCs from other origins (sites which will be using your service).

Decouple server-side implementation

WebSharper allows a fully type-checked communication between your web appication server and client code but if you have it all in one project then there is more coupling between the two layers than desired. It comes in handy to define your RPC methods on abstract classes so that you can implement it in another project. Then you can have your communication protocol defined in a common project referenced by a server-side and client-side project not depending directly on each other.

1
2
3
4
5
namespace Model
[<AbstractClass>]
type ServerDataStore() =
	[<Remote>]
	abstract GetData : string -> Async<option<string>>

Note that now we are putting the Remote attribute on an instance method. There is no restriction on how many Remote methods you can have on a class, and you can mix abstract and non-abstract members of course. In another project we can implement this with a subclass, its methods do not need the Remote attribute as they are not directly exposed but will be called through its virtual slot.

1
2
3
4
5
6
7
8
9
type ServerDataStoreImpl(store: Dictionary<string, string>) = 
	inherit Model.ServerDataStore()
	
	override this.GetData(key) =
		async { 
			match store.TryGetValue(key) with 
			| true, c -> return Some c
			| _ -> return NOne
		}

Then we need to provide the server runtime at startup with an instance of the ServerDataStore class, which will execute its implementations of the abstract remote methods:

1
2
3
    let store = Dictionary()
    store.Add("greeting", "Hello world!")
	AddRpcHandler typeof<Model.ServerDataStore> (ServerDataStoreImpl(store))

There should be only one handler object per type. As now the RPC is an instance method, we need a small helper to call it from the client code:

1
2
3
4
	async {
		let! greet = Remote<Model.ServerDataStore>.GetData("greeting")
		greet |> Option.iter Console.Log
	}

Putting it together

These features are all combinable. So you can write a library that houses both server-side and client-side functionality for a feature, customize what happens on the client when the application calls one of the RPCs, and make the implementation of some or all RPCs overridable. We are using these possibilities actively in our current projects, we hope that WebSharper can bring joy to even more developers in the future.

.

Very nice! The decoupling of the server-side RPCs is very interesting information. Is that only available in Zafir or is it currently available in 3.6?

By on 12/20/2016 9:02 AM ()

Instance Remote methods are available in 3.6, see some tests. API have changed in Zafir, in 3.6 you can specify a single factory object that creates the instances on which the Remote methods are ran, see it also in the tests.

RemotingProvider usage has changed too, it was per assembly in WS3 and affecting all client side calls to any remote method, which had less use cases than having specific client-side code unning around one or more Remote methods.

By on 1/23/2017 3:47 PM ()

Hello! Could you give an example how SafeRemotingProvider should be implemented in C#? It has to be some manipulation with FSharpAsync object...

By on 6/4/2018 9:52 PM ()

Hi!

I was also thinking on this before, how to expose it for C# best. Now came up with this, I will test it a bit and probably add it to WebSharper itself, but you can try this code in your own project too:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Threading.Tasks;
using Microsoft.FSharp.Control;

namespace WebSharper
{
    [JavaScript]
    public class CSharpRemotingProvider : Remoting.AjaxRemotingProvider
    {
        public virtual Task<object> TaskBase(string m, object[] data)
        {
            return FSharpAsync.StartAsTask(base.AsyncBase(m, data), null, null);
        }

        public override FSharpAsync<object> AsyncBase(string m, object[] data)
        {
            return FSharpConvert.Async(TaskBase(m, data));
        }
    }
}

The idea is that AsyncBase (which) calls into TaskBase which exposes it as a C#-friendly abtract method, ready to be overridden with an async method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    [JavaScript]
    public class SafeRemotingProvider : CSharpRemotingProvider
    {
        public override async Task<object> TaskBase(string m, object[] data)
        {
            try
            {
                return await base.TaskBase(m, data);
            }
            catch (Exception e)
            {
                JavaScript.Console.Log("Remoting exception", m, e);
                return null;
            }
        }
    }
By on 6/6/2018 7:04 AM ()

This only code added to client-server template project results in infinite looping in js caused by exception:

1
2
3
4
5
6
"TypeError: Cannot read property 'get_EndPoint' of undefined
    at http://localhost:51826/Scripts/WebSharper/WebSharper.Main.js?h=-1136005996:330:17
    at http://localhost:51826/Scripts/WebSharper/WebSharper.Main.js?h=-1136005996:1068:4
    at http://localhost:51826/Scripts/WebSharper/WebSharper.Main.js?h=-1136005996:1229:15
    at Object.tick (http://localhost:51826/Scripts/WebSharper/WebSharper.Main.js?h=-1136005996:723:27)
    at http://localhost:51826/Scripts/WebSharper/WebSharper.Main.js?h=-1136005996:737:11"

Line 330 is:

1
      a$1=$this.get_EndPoint();
By on 6/6/2018 11:01 PM ()

Sorry, actually WebSharper is creating incorrect translation for a try/with inside an async method, that caused the issue. :( I will look into that. Tested with a workaround using tasks straight and it worked. :)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    [JavaScript]
    public class SafeRemotingProvider : CSharpRemotingProvider
    {
        public override Task<object> TaskBase(string m, object[] data)
        {
            return
                base.TaskBase(m, data)
                    .ContinueWith(t => {
                         if (t.IsFaulted)
                         {
                             WebSharper.JavaScript.Console.Log("Remoting exception", m, t.Exception);
                             return null;
                         }
                         else return t.Result;
                    });
        }
    }

(no change in CSharpRemotingProvider)

By on 6/7/2018 6:01 AM ()
IntelliFactory Offices Copyright (c) 2011-2012 IntelliFactory. All rights reserved.
Home | Products | Consulting | Trainings | Blogs | Jobs | Contact Us | Terms of Use | Privacy Policy | Cookie Policy
Built with WebSharper