Click here to Skip to main content
Click here to Skip to main content

RaptorDB REST

, 5 Dec 2013
Rate this:
Please Sign up or sign in to vote.
A REST web interface for RaptorDB the document database

Preface 

This article and the associated code should really be part of the main distribution of RaptorDB but since it is a proof of concept and I want feedback from users I have decided to create a separate article, if all goes well then I will integrate it into the main release, so please tell me what you think. 

Introduction 

The programming world is changing and more and more leaning towards languages like javascript and web application (to the dismay of old programmers like yours truly), and if you want to survive you must adapt, so this article and the associated code is the implementation of a REST web interface for RaptorDB.

There is a couple of reasons for wanting to use RaptorDB with web applications:

  • You already have .net applications using RaptorDB and you want to leverage your existing infrastructure.
  • Even if you don't use .net then using a high performance nosql database like RaptorDB is quite appealing.

This article uses the following references:

What is REST? 

At it's root a REST interface is simply the implementation of HTTP's POST GET PUT DELETE instructions on URL paths. This means that you can supply paths and parameters and query information for that path. Data is usually returned in XML or JSON format to the caller.

What does RaptorDB REST do?

RaptorDB REST will start a HTTP listener and process incoming request from a routing table you provide. The processing consists of saving json documents and retrieving the same or data from views which have been created from those documents.

Limitations & Requirements

The main limitation in using this REST interface is that you loose polymorphism because the json to object deserializer cannot determine nested types since that information is not contained in the json input string (fastJSON's $type extension). 

To POST json documents you must supply a string GUID value, this is needed to uniquely identify that document, if you are doing multi-user applications then you should not have a problem with this since generating a client side ID is preferred to round tripping to a server.

How to use RaptorDB REST

You can easily test the REST interface by running the testing.exe application form the output folder. This console application will startup the REST server on port 8080 and generate 100,000 Salesinvoice entities and insert them into the RaptorDB database.

The code to do this is simply :

var rest = new RestServer(8080, @"..\RaptorDBdata");

But before this you should get to know the following concepts.

Concepts

There are the following key concepts which you should be aware of when using RaptorDB REST :

  • REST specific : URLs, routing tables, entity containers

Also a determining factor is the existence of already defined entity classes, since both routes and views depend on entities or data containers.

You can use RaptorDB REST in the following two ways:

  • In your compiled code by initializing through code
  • With a route script file

The script file method is really powerful and is the preferred way to do things, since you can change the implementation at run-time. 

URL 

All REST services start off from a URL path so you should create paths that make sense with meaning full names, an endpoint to a URL can either be one of the following :
  • a POST end point for inserting/updating data
  • a GET end point to a RaptorDB View
  • a GET end point to a aggregate function on a RaptorDB View

Routing table

All the goodness is in the routing table, this is similar to the Express framework for nodejs in that you create URL paths and supply a closure function to do the work. This routing table is filled by *.route files in the "Datafolder\Routing" folder, this allows you to break-down your application into logically separate units and manage them in isolation and the system will assemble the pieces at run-time.

Useful tools  

To test a REST interface I have found the following tool indispensable :

Sample route code

The sample crm.route file consists of the following sections :

// load the following dll as a reference for salesinvoices etc.
// ref : views.dll
using System;
using System.Collections.Generic;
using System.Linq; 
...

The above snippet is from the top of the code which allows you to load existing DLL's with the ref comment and link to your script file to access already defined types.

...
namespace crm
{
   public class Customer
    { 
        public string Name; 
        public string Address;
    } 
...   
With-in the namespace above we have defined a Customer class to map the incoming json object POSTs since RaptorDB needs .net classes to operate upon, if you have existing infrastructure and applications in place with entity types you can link with those .net types instead of defining here.
...
    public class crmrouting : IRDBRouting
    {
        public void Initialize(IRouteAPI api)
        {
            api.AddRoute(new RDBRoute { URL = "crm/customer", EntityType = typeof(Customer) });
            api.AddRoute(new RDBRoute { URL = "crm/customerview", Viewname = "customer"});
...

The above code is a routing table definition which is inherited from the IRDBRouting interface. Within the Initialize() method we have defined a POST-ing handler for the "crm/customer" URL and maps the incoming json to the Customer type.

The second line maps GET request to the "crm/customerview" URL to a RaptorDB view by the name of "customer" that view is defined later in the file.

...
            api.AddRoute(new RDBRoute
            {
                URL = "testing/function",
                function = (rdb, filter) =>
                {
                    var q = rdb.Query<SalesItemRowsViewRowSchema>(filter);

                    var res = from x in q.Rows
                              group x by x.Product into g
                              select new 
                              {
                                  Product = g.Key,
                                  TotalPrice = g.Sum(p => p.Price),
                                  TotalQTY = g.Sum(p => p.QTY)
                              };
                    
                    return res.ToList<object>();
                }
            }); 
...

The above route in the Initialize() method is a very powerful closure to compute an aggregate on a RaptorDB view.

You are free to do anything with the function with the full power of .net LINQ at your disposal, the filter parameter is what is given from the REST call by the user.

Examples of all the above are shown below.

..
    [RegisterView]
    public class CustomerView : View<Customer>
    {
        public class CustomerRowSchema : RDBSchema
        {
            public string Name;
            public string Address;
        }

        public CustomerView()
        {
            this.Name = "Customer";
...
To the end of the crm.route file we have defined a RaptorDB view for the Customer entity which we defined at the start of the file.

So in one script file we have created a handler and logic for processing data all in a type-safe way which will get compiled and loaded at run-time so we have the ease of change of scripts and the full power and performance of compiled languages.

Running the sample application

Lets get started working with the sample application. First lets just query:
http://localhost:8080/crm/salesinvoice?serial<100?start=100?count=10

From the above we can see the basic structure for doing queries in that we have a query filter based on the schema for a RaptorDB view 'serial<100' and the paging extension 'start=100' and 'count=10' you can omit any of these and you will get back the related data.

In the results we can see the data rows returned and also how many rows the query had in the TotalCount field and how many were return in the Count field.

To see the count of all the data in a view we can do the following which is a short cut and the results are in the TotalCount field :

http://localhost:8080/crm/salesitemrows?count=0

 

We can call the aggregate in a similar way which is computing the group by on all the rows :

http://localhost:8080/testing/function

or with a filter: 

http://localhost:8080/testing/function?product="prod 1"

Posting is a little more involved since we can't do it through the address box in the browser, so open PostMan in Google Chrome and select POST from the drop down and type the following :

http://localhost:8080/crm/customer?id=A5AE46F0-E114-4659-A4AF-F285CD00A93D

 

We are supplying the following json document as "raw" data, by pressing "Send" we should get a reply of "posted :..." :

{"name":"aa","address":"safasdfasd","age":9090,"id":"A5AE46F0-E114-4659-A4AF-F285CD00A93D"}

 

A requirement of RaptorDB REST is that you must supply the GUID for that document when posting and presumably the json document contains the same but you can omit it.

To get back the original json we can GET from the POST URL supplying the GUID :

http://localhost:8080/crm/customer?id=A5AE46F0-E114-4659-A4AF-F285CD00A93D

 

If all went well, we can query our "customer" RaptorDB view which we have defined and see our document in there:

http://localhost:8080/crm/customerview

The inner workings

At the heart of RaptorDB's REST interface is the HTTPListener, this is a great piece of work done by the .net team which is under-rated. To get the power of this class lets see how we use it:
_server = new HttpListener();
_server.Prefixes.Add("http://*:" + _port + "/");
_server.Start();
while (_run)
{
     var context = _server.BeginGetContext(new AsyncCallback(ListenerCallback), _server);
     context.AsyncWaitHandle.WaitOne();
} 

That is all the code! and it is async and high performance (although the sync version is about the same). The HTTPListener takes a prefix which it will listen on and you handle the AsyncCallBack like the following :

        private void ListenerCallback(IAsyncResult ar)
        {
            var listener = ar.AsyncState as HttpListener;

            var ctx = listener.EndGetContext(ar);

            //do some stuff
            string path = ctx.Request.Url.GetComponents(UriComponents.Path, UriFormat.Unescaped).ToLower();
            RDBRoute r = null;
            ctx.Response.ContentEncoding = UTF8Encoding.UTF8;
            if (_routing.TryGetValue(path, out r))
            {
                string mth = ctx.Request.HttpMethod;
                if (mth == "POST" || mth == "PUT")
                    ProcessPOST(_rdb, ctx, path, r);
                else
                    ProcessGET(_rdb, ctx, path, r);
            }
            else
                WriteResponse(ctx, 404, "route path not found : " + ctx.Request.Url.GetComponents(UriComponents.Path, UriFormat.Unescaped));

            ctx.Response.OutputStream.Close();
        }

So in the above code we are checking the routing table for a matching path and handling the request.

How the RestServer works 

At a high level the RestServer() works in the following way:

As you can see the RestServer takes a json document and maps that to a .net object and sends it to RaptorDB as it expects, since there might be more in the original json document that you did not anticipate the RestServer also saves that original to a Key/Value store.

All get requests to the json document will be processed from the original json stored, not the mapped version in RaptorDB.

Queries are processed from views stored in RaptorDB

 

Appendix v1.0.1 - the weird and wonderful world of the web 

While trying to get the REST interface working with a javascript client, I have run into some of the weirdness of the http protocol and how browsers handle requests. The following fix took me a day of frustration to find out, which I was only able to do when I enabled the debugger in the Chrome browser since the browser silently does nothing and you only see the error message if you have enabled the debugger.

The problem is that the browsers require the json payload to be from the same domain address (for cross site scripting problems) as the page, to overcome this you need to add the following line on the REST server:  

Response.AppendHeader("Access-Control-Allow-Origin", "*"); 

Now for the wonderful, since json payloads can get really big you need some sort of compression on it otherwise transferring that much data can be slow and block the browser. To do compression all that is required is the following :

            byte[] b = Encoding.UTF8.GetBytes(msg);
            if (b.Length > 100*1024)
            {
                using (var ms = new MemoryStream())
                {
                    using (var zip = new GZipStream(ms, CompressionMode.Compress, true))
                        zip.Write(b, 0, b.Length);
                    b = ms.ToArray();
                }

                ctx.Response.AddHeader("Content-Encoding", "gzip");
            }
            ctx.Response.ContentLength64 = b.LongLength;

The great thing about the above is that it is totally transparent for the client javascript and it is handled by the server and the browser. As you can see if the message being sent is greater than 100Kb then it will be compressed. This saves a lot of time and bandwidth.

Also in this release the following internal routes have been added:

  • RaptorDB/GetRoutes : will give you a list of the available registered routes
  • RaptorDB/GetViews : will give you a list of RaptorDB views registered
  • RaptorDB/GetSchema : will give you the schema for a view you query 

testpage.html 

To help test the REST interface using a real javascript client instead of using POSTMan I have added the testpage.html file which you can see below:

The above page uses zepto.js [http://zeptojs.com/] as a replacement for jQuery which is smaller and does the same thing.  

By pressing the buttons on this page you will get a pretty [arguably] table representation of the json sent from the server. You can change the values in the textboxes and get different results.  

Appendix v1.1.0 - Sorting 

 In this version you can now do sorting while paging the data, to do this you append an OrderBy clause to your URL calling : 

http://localhost:8080/salesinvoice?serial<100 & count = 10 & orderby = serial desc 

 

As you can see from the above the query will return 10 items sorted in descending order on the serial column.

 

Also a nice feature in this release is the ability to serve static content, so now if you call the base URL for the server you will get the testpage.html back, as you can see from the image below I have changed the html so you can easily change the prefix and call the functions from other systems :

Previous Versions 

You can download previous versions here:

History

  • Initial Release v1.0.0 : 5th November 2013 
  • Update v1.0.1 : 16th November 2013
    • post a Guid ID with or without single or double qoutes
    • access control to everyone on http header
    • compress json if over a threshold (default 100kb)
    • start and count in query with ? or & deliminator
    • added information routes under RaptorDB/GetViews, RaptorDB/GetRoutes, RaptorDB/GetSchema
    • added testpage.html
  • Update v1.1.0 : 5th December 2013
    • added sort to queries
    • the rest server can now serve static content and the testpage.html by calling the root url
    • removed extra query overloads in favour of the new model

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Mehdi Gholam
Architect
United Kingdom United Kingdom
Mehdi first started programming when he was 8 on BBC+128k machine in 6512 processor language, after various hardware and software changes he eventually came across .net and c# which he has been using since v1.0.
He is formally educated as a system analyst Industrial engineer, but his programming passion continues.
 
* Mehdi is the 5th person to get 6 out of 7 Platinums on CodeProject (13th Jan'12)

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web04 | 2.8.140721.1 | Last Updated 5 Dec 2013
Article Copyright 2013 by Mehdi Gholam
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid