-
Notifications
You must be signed in to change notification settings - Fork 97
Design Proposal: REST API Bindings
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.
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.
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));
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)"} />
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 callinvalidate
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.
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).
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')"} />
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 command
s 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')}">
-
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.
-
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.
-
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.
-
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.
-
CORS: We need to do a research for issues with APIs hosted on other domains.
-
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.