Automatic Javascript-to-MVC Client Proxy

RPC frameworks (Java RMI, WCF, DCOM, CORBA, SOAP, etc) allow us to call functions over a network as if the function existed locally. Generally, the framework creates a local "proxy" (or "stub") version of the function that hides the networking details for us. Sadly, this isn't usually the case if the client is JavaScript, so here I present a simple method to generate a JavaScript client proxy for C# MVC / Web API functions. What's the point of all this? Well, the idea is that you can add, delete, or change Web API methods and begin using them immediately, in almost the same form, from your browser.

An Example

Let's say you have the following C# Web API function:

   public int Some_Fn(int a, int b)  
   {  
    return a+b;  
   }  

If, like me, you prefer RPC-style calls because you're less than impressed with REST then you'd like your JavaScript calls to look something like this:

   Some_Fn(a, b, function(res) { ... });  

The JavaScript proxy for "Some_Fn" might then be defined like this:

   function Some_Fn(a, b, success_fn)  
   {  
    var url, req;  
   
    url = "Api/Some_Fn?a=" + a + "&b=" + b;  
   
    req = new XMLHttpRequest();  
    req.onreadystatechange = Req_State_Change;  
    req.open('GET', url, true);  
    req.send();  
   
    function Req_State_Change()  
    {  
     if (this.readyState == XMLHttpRequest.DONE && success_fn != null)  
      success_fn(JSON.parse(this.responseText));  
    }  
   }  

The Problem

Nothing special in the preceding code, but every time you make a change that affects a function's signature you'll need to make the same change within the proxy. This quickly gets tedious with large numbers of functions, even when you've factored out the common boilerplate code. It would be nice to have something generate the proxy code automatically and there's a couple of products that do exactly this (Jayrock, DWR). They go as far as removing the need for MVC Controllers so that you can expose remoting functions directly from their POJO/POCO classes. However, in a corporate environment, it may be impossible to get rid of MVC altogether. In these scenarios we could roll our own functionality.

The Proxy

One way to do this is to add a method to our controllers that returns a JavaScript file with the required proxy code. We can then include this in our HTML via the usual script tag. The controller method itself can simply iterate through its constituent methods and, using reflection, generate the equivalent Javascript proxy functions. How to identify the relevant methods to export is largely up to you. One can use the attributes assigned to the method, its return type, its name, or perhaps something in "web.config". The sample provided simply exposes all "public" methods. Included are three sample functions, imaginatively named "Fn_1", "Fn_2", and "Fn_3". If you haven't noticed already, the code is intended for .Net Core. Also, each proxy function has been placed within a single JavaScript object whose name is based on the originating controller (ApiController), just to keep naming conflicts down.

1:  namespace Blog.Controllers  
2:  {  
3:   [Microsoft.AspNetCore.Mvc.Route("[controller]/[action]")]  
4:   public class ApiController : Microsoft.AspNetCore.Mvc.Controller  
5:   {  
6:    [Microsoft.AspNetCore.Mvc.HttpGet]  
7:    public string[] Fn_1()  
8:    {  
9:     return new string[] { "value1", "value2" };  
10:    }  
11:    
12:    [Microsoft.AspNetCore.Mvc.HttpGet]  
13:    public int Fn_2(int a, int b)  
14:    {  
15:     return a+b;  
16:    }  
17:    
18:    [Microsoft.AspNetCore.Mvc.HttpGet]  
19:    public object Fn_3(string name, int age)  
20:    {  
21:     return new { name = name, age = age };  
22:    }  
23:    
24:    [Microsoft.AspNetCore.Mvc.HttpGet]  
25:    public Microsoft.AspNetCore.Mvc.ContentResult Proxy()  
26:    {  
27:     System.Type type;  
28:     System.Reflection.MethodInfo[] methods;  
29:     System.Reflection.ParameterInfo[] parameters;  
30:     string  
31:      fn_js = null, params_js = null, fns_js = null, class_js,  
32:      ctrl_name, misc_fns;  
33:    
34:     type = this.GetType();  
35:     ctrl_name = type.Name.Substring(0, type.Name.Length - 10);  
36:    
37:     methods = type.GetMethods  
38:      (System.Reflection.BindingFlags.Instance |  
39:      System.Reflection.BindingFlags.Public |  
40:      System.Reflection.BindingFlags.DeclaredOnly);  
41:     foreach (System.Reflection.MethodInfo method in methods)  
42:     {  
43:      fn_js = null;  
44:      params_js = null;  
45:    
46:      // build fn params  
47:      parameters = method.GetParameters();  
48:      foreach (System.Reflection.ParameterInfo param in parameters)  
49:      {  
50:       fn_js = Append_Str(fn_js, param.Name, ", ");  
51:       params_js += "url = this.Append_Str(url, \""+ param.Name + "=\" + encodeURIComponent(" + param.Name + "), \"&\"); ";  
52:      }  
53:      // add callback to fn params  
54:      fn_js = Append_Str(fn_js, "on_success_fn", ", ");  
55:      // build full fn signature  
56:      fn_js = method.Name + ": function(" + fn_js + ") ";  
57:      // add fn body  
58:      fn_js +=  
59:       "{ " +  
60:        "var url; "+  
61:        params_js+  
62:        "url = this.Append_Str(\"" + ctrl_name + "/" + method.Name + "\", url, \"?\"); " +  
63:        "this.Req_Json(url, on_success_fn); " +  
64:       "}, ";  
65:    
66:      fns_js += fn_js;  
67:     }  
68:    
69:     misc_fns = @"  
70:      Req_Json: function(url, success_fn)  
71:      {  
72:       var req;  
73:    
74:       req = new XMLHttpRequest();  
75:       req.onreadystatechange = Req_State_Change;  
76:       req.open('GET', url, true);  
77:       req.send();  
78:    
79:       function Req_State_Change()  
80:       {  
81:        if (this.readyState == XMLHttpRequest.DONE && success_fn!=null)  
82:        {  
83:         if (this.responseText == '')  
84:          success_fn(null);  
85:         else  
86:          success_fn(JSON.parse(this.responseText));  
87:        }  
88:       }  
89:      },  
90:      Append_Str: function(a, b, sep)  
91:      {  
92:       var res=null;  
93:    
94:       if (this.Not_Empty(a) && this.Not_Empty(b))  
95:        res=a+sep+b;  
96:       else if (!this.Not_Empty(a) && this.Not_Empty(b))  
97:        res=b;  
98:       else if (this.Not_Empty(a) && !this.Not_Empty(b))  
99:        res=a;  
100:    
101:       return res;  
102:      },  
103:      Not_Empty: function(str)  
104:      {  
105:       var res=false;  
106:    
107:       if (str!=null && str!='')  
108:        res=true;  
109:    
110:       return res;  
111:      },";  
112:    
113:     class_js = "var " + ctrl_name + " = " + "{" + fns_js + misc_fns + "};";  
114:    
115:     return Content(class_js, "application/javascript");  
116:    }  
117:    
118:    public static string Append_Str(string a, string b, string sep)  
119:    {  
120:     string res = null;  
121:    
122:     if (!string.IsNullOrEmpty(a) && !string.IsNullOrEmpty(b))  
123:      res = a + sep + b;  
124:     else if (string.IsNullOrEmpty(a) && !string.IsNullOrEmpty(b))  
125:      res = b;  
126:     else if (!string.IsNullOrEmpty(a) && string.IsNullOrEmpty(b))  
127:      res = a;  
128:    
129:     return res;  
130:    }  
131:   }  
132:  }  

Usage

Usage is fairly straightforward. Import the relevant script file, which is really a call to the controller's proxy generating method, and start making calls. The following JavaScript shows this in action.

1:  <!DOCTYPE html>  
2:  <html>  
3:    
4:  <head>  
5:   <script src="Api/Proxy"></script>  
6:   <script>  
7:    function Main()  
8:    {  
9:     Api.Fn_1(On_Fn_1_OK);  
10:     function On_Fn_1_OK(res)  
11:     {  
12:      document.getElementById("fn_1_res").innerText = res;  
13:     }  
14:    
15:     Api.Fn_2(3, 4, On_Fn_2_OK);  
16:     function On_Fn_2_OK(res)  
17:     {  
18:      document.getElementById("fn_2_res").innerText = res;  
19:     }  
20:    
21:     Api.Fn_3("fred", 34, On_Fn_3_OK);  
22:     function On_Fn_3_OK(res)  
23:     {  
24:      document.getElementById("fn_3_res").innerText = "{ name: " + res.name + ", age: " + res.age + " }";  
25:     }  
26:    }  
27:    
28:    window.onload = Main;  
29:    </script>  
30:   </head>  
31:    
32:  <body>  
33:   <div>Api.Fn_1() = <span id="fn_1_res"></span></div>  
34:   <div>Api.Fn_2(3, 4) = <span id="fn_2_res"></span></div>  
35:   <div>Api.Fn_3("fred", 34) = <span id="fn_3_res"></span></div>  
36:   </body>  
37:    
38:  </html>  

Improvements

As this is only a simple example, there are many ways in which we could improve it. Firstly, we've only handled the HTTP method "Get". We could add "Post" and the rest of the REST methods "Put", "Delete", "Patch", etc. We could also start handling the marshalling of parameters ourselves, as a way to remove the dependence on MVC altogether. Also, the more Object Oriented RPC mechanisms also handle the definition, instantiation, and propagation of entire objects. I'll leave it to you, now, to contemplate the possibilities.

Popular Posts