Skip to content

Design Proposal: REST API Bindings

Tomáš Herceg edited this page Apr 9, 2017 · 3 revisions

After a bunch of discussions with @exyi and other guys, we have finished the design document of the the REST API bindings. Post any suggestions or comments to to the issue #282.

Step 1 - Swagger Metadata

We assume that the target API publishes a Swagger metadata. Using this metadata, we can generate both C# and JavaScript classes.

We may not use the official Swagger Codegen tools, because it requires Java to run. But the NSwag project looks promising - it can generate nice C# and JavaScript outputs (one file each) which we can use.

We'll need to build a tooling for that, so the user will just give the us the JSON file and the URL of the API. It can be a command line tool for the first version.

Step 2 - DotVVM Configuration

The DotvvmConfiguration object should be extended with a binding variable provider configuration, so you can easily register custom variable in the DotVVM bindings. The API should be configurable, at least with Base API variables.

var apiConfiguration = new RestApiBindingVariableConfiguration() 
{
    BaseUrl = "http://localhost:1234/",
    
    // the API contains several endpoints - we can expose each one to _api.Orders, _api.Companies etc.
    Endpoints = {
        new RestApiEndpoint() 
        { 
            Name = "Companies",                         // the endpoint will be exposed as _api.Companies
            ServerClientType = typeof(CompaniesClient), // type of generated C# client
            ClientClassName = "CompaniesClient",        // JS exported class name
            ClientResourceName = "MyApiClient"          // JS resource name            
        },
        ...
    }
};
config.Bindings.Variables.Register("_api", new RestApiBindingVariableProvider(apiConfiguration));

Step 3 - Binding Syntax

The _api.Endpoint variable will expose sync methods from the specified server client class, e.g.:

System.Collections.ObjectModel.ObservableCollection<Order> Get(int companyId, int? pageIndex, int? pageSize);
void Post(Order order);

The _api variable can be used in value and staticCommand bindings to call these functions. Since the function names are the same in C# and JavaScript generated files (the only difference is in Pascal vs camel casing), the translation process should not be very difficult.

For example, we can use the variable in the GridView:

<dot:GridView DataSource="{value: _api.Orders.Get(CompanyId, PageIndex, 20)}">

On the client side, the binding should create a Knockout computed observable, that:

  • returns a default value on the first call
  • invokes the REST API call and notifies subscribers when the result is obtained
  • subscribe to the event hub (described in the following step) to allow invalidation of the value when something is changed
  • adds the invalidate method which will call the REST API and refresh the value

The API can be also called as a command, for example:

<dot:Button Click="{staticCommand: _api.Post(NewOrder)"} />

Step 4 - Event Hub

After clicking the button in the previous step, the GridView needs to be refreshed. We need to introduce a page-level publish-subscribe mechanism called event hub. The event is basically a string message. Anyone can publish an event to the event hub and anyone can subscribe for a specific event.

It will be accessible from the bindings and will contain the following functions:

  • refreshOn(eventName, observable) will decorate the observable object - it will subscribe to the specified event and call invalidate on the underlying observable
  • trigger(eventName) will publish the event to the event hub

It will also be possible to publish events to the event hub from the server during the postback:

dotvvmRequestContext.EventHub.Publish("myEvent")

The events will be sent as part of the HTTP response and will be published to the event hub when the client processes the response.

Step 5 - Automatic Events

Every value binding which does a REST API call using HTTP GET, will subscribe automatically to the event hub for an event called e.g. _api.Orders. Every staticCommand binding which makes a REST API call using other than HTTP GET, will automatically trigger the corresponding event (e.g. _api.Orders) on the event hub so all bindings to GET values are updated.

It will cover most of the basic scenarios and will work well if the API implements the REST conventions correctly.

This behavior can be suppressed in the RestApiEndpoint object using the SuppressAutoRefreshEvents = false (see Step 2).

Step 6 - Manual Events

In more complex scenarios, you may need to trigger and subscribe on the events manually.

In the value bindings, you can use the the following syntax:

<dot:GridView DataSource="{value: _events.refreshOn('gridData', _api.Orders.Get(CompanyId, PageIndex, 20))}">

Because this syntax is not very convenient, we will introduce the |> operator from F# (the Forward Pipe operator):

<dot:GridView DataSource="{value: _api.Orders.Get(CompanyId, PageIndex, 20) |> _events.refreshOn('gridData')}">

This will produce an observable that will load its value from the REST API and refreshes on the gridData event on the event hub.

In the staticCommand binding, we will allow to use multiple statements:

<dot:Button Click="{staticCommand: _api.Post(NewOrder); _events.trigger('gridData')"} />

Step 7 - Stores

This step is just a proposal and there may be some changes. Basically, it can be useful to store the REST API data in the viewmodel.

In this case, you can declare the collection in the viewmodel (and don't load it on the server):

public List<Order> Items { get; set; } = new List<Order>();

In the data-binding, you can use this:

<dot:GridView DataSource="{value: _api.Orders.Get(CompanyId, PageIndex, 20) |> _store.viewmodel(Items)}">

Then you can call commands which will have the collection items available on the server. This can be useful when you apply e.g. IfInPostBackPath direction on the property

  • it will be sent to the server only when it is used from the postback data.

Also, we may use this for caching. The following syntax can be used to persist (and load) the value from the local storage. This would extend the observable to return the local storage data when the REST API call is in progress, or when it fails.

<dot:GridView DataSource="{value: _api.Orders.Get(CompanyId, PageIndex, 20) |> _store.localStorage('orders')}">

Additional Notes

  1. Javascript API extensibility: we need to support things like authentication, API keys or any other tokens. There should also be an easy way to intercept requests and responses.

  2. Date-time handling: DotVVM stores the DateTime values in a string format, we will need to transform the dates when making the REST API calls.

  3. Complex Object Transforms: The proposed way cannot deal with the situation where the API returns the following object:

{
    "items": [ ... ],
    "totalRows": 160
}

This is a situation where we may need converters or another mechanism to work with the APIs.

  1. CSRF: When the API is hosted in the same app with DotVVM, we may need to verify the CSRF tokens. When the API is hosted externally, there might be another way to work with this.

  2. CORS: We need to do a research for issues with APIs hosted on other domains.

  3. Static HTML Generation: This feature will allow to get rid of the DotVVM server runtime completely in many cases. If only the REST API bindigns to deal with page data and there are no command bindings, the HTML can be generated statically and server e.g. from a CDN. We need to do a research on the limitations for this feature as this will allow to produce nice mobile apps. Together with local storage caching, it opens a plenty of use cases that were not possible before.