Taking Inspiration from SignalR
One of my favourite features of SignalRis the the automatic generation of JavaScript proxies for hub methods. By adding a hub class in C#…
//server
public class ExampleHub : Hub
{
public void SendMessage(string message)
{
//do something with message
}
}
…you can get the JavaScript wrapper for that method just by adding a reference to /signalr/hubs
:
//client
$(function() {
var hub = $.connection.exampleHub;
$.connection.hub.start(function() {
hub.sendMessage('a message from the client');
});
});
This is such a useful feature that it has even been suggested as an alternative to Web API controllers.
But why should we use hubs as a *replacement *for MVC or Web API controllers? Why can’t we instead write similar functionality to work with controllers?
Introducting ProxyApi
ProxyApi is a small NuGet package that automatically generates JavaScript proxy objects for your MVC and Web API controller methods.
Install-Package ProxyApi
(as an aside, this was my first attempt at creating a NuGet package and it was embarrassingly simple).
Once you’ve installed the NuGet package you just need to add a link to ~/api/proxies
and the JavaScript API classes will be automatically created.
So what do these actually give you?
API Controllers
Let’s say you have started a new MVC4 project and you add a new “API controller with empty read/write actions”:
public class DataController : ApiController
{
// GET api/data
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/data/5
public string Get(int id)
{
return "value";
}
// POST api/data
public void Post([FromBody]string value)
{
}
// PUT api/data/5
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/data/5
public void Delete(int id)
{
}
}
Ordinarily if you wanted to call these methods from JavaScript you would need to write something like the example below using jQuery:
$.post('/Data/123', function(data) {
//do something with the result
});
This is actually pretty concise, but I have 2 problems with this approach:
- The code calling this method knows that it is making a POST call. What do I do when I want to switch my data source to local storage, or some other data accessor?
- The code knows about the URL.
Instead, what I would prefer is a JavaScript proxy object on which I can call a method - passing in appropriate parameters - without ever knowing where that method gets it’s data or how it does so. And this is what ProxyApi provides.
Add a new script tag to _Layout.cshtml
to ~/api/proxies
(after the jQuery reference) and you can start directly calling API methods without writing another line of code yourself:
$.proxies.data.get().done(function(allItems) {
//allItems will contain ['value1', 'value2']
});
$.proxies.data.get(123).done(function(item) {
//item will be 'value'
});
//will send 'value' to Post method on controller
$.proxies.data.post('value');
//will send id=1, value='value' to Put method on controller
$.proxies.data.put(123, 'value');
//will send 123 to Delete method on controller
$.proxies.data.put(123, 'value');
These proxy objects can now be passed to any code that needs to perform data access without ever exposing how that data is sourced. You can easily mock them for unit testing, replace them if needed, and call them without needing to know where the website is hosted.
Complex Types
This is all well and good for simple data types like the strings in the example above, but what about when you want to manipulate complex types?
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class DataController : ApiController
{
[HttpPost]
public void UpdatePerson(Person value)
{
}
}
In this case, just pass a JSON object to the generated method:
$.proxies.data.updatePerson({
Id: 123,
FirstName: 'Steve',
LastName: 'Greatrex'
});
This will send the JSON object as POST data to the Post method on the controller.
And when you have both URL and body data, such as in the auto-generated Put
method? Just decorate the parameters with [FromBody]
or [FromUri]
and the rest will be taken care of:
public void Put([FromUri]int id, [FromBody]Person value)
{
}
Note: it generally isn’t necessary to use [FromUri]
as ProxyApi will assume that anything is a URL parameter unless told otherwise. The only exception to this is for POST methods that take a single parameter, which will be assumed to be POSTed.
Non-Conventional Method Names
All the examples so far have been using conventionally-named methods, but there is no requirement for this: any method name will work:
public class DataController : ApiController
{
public void DoSomething(int id)
{
// --> $.proxies.data.dosomething(123) (GET)
}
[HttpPost]
public void DoSomethingElse(Person person)
{
// --> $.proxies.data.dosomethingelse({ ... }) (POST)
}
}
Appropriate HTTP verbs will be used for any method based on the following rules (in priority order):
[Http*]
attribute (e.g.[HttpPost]
,[HttpGet]
etc.)[AcceptVerbs(...)]
attribute- Method naming conventions, e.g.
DeletePerson()
==DELETE
GET
for everything else
You can also specify custom names for both the proxy objects and methods using the [ProxyName]
attribute.
[ProxyName("custom")]
public class DataController : ApiController
{
[ProxyName("method")]
public void DoSomething(int id)
{
// --> $.proxies.custom.method()
}
}
Excluding and Including Elements
By default, ProxyApi will automatically include every method in every type in the current AppDomain that inherits from either System.Web.Mvc.Controller
or System.Web.Http.ApiController
. You can change this behaviour to exclude everything by default by changing the ProxyGeneratorConfiguration.Default.InclusionRule
property:
ProxyGeneratorConfiguration.Default.InclusionRule = InclusionRule.ExcludeAll;
You can also explicitly include or exclude any element by decorating it with one of the [ProxyInclude]
or [ProxyExclude]
attributes:
[ProxyExclude] //excludes entire controller
public class ExcludedController : ApiController
{}
[ProxyInclude] //includes entire controller and all methods...
public class IncludedController : ApiController
{
[ProxyExclude] //...except for explicitly excluded methods
public void ExcludedMethod() {}
}
public class DefaultController : ApiController
{
[ProxyExclude] //will always be excluded
public void ExcludedMethod() {}
[ProxyInclude] //will always be included
public void IncludedMethod() {}
//will be included or excluded based on the globally configured default
public void DefaultMethod() {}
}
Returning Data & Handling Errors
The generated proxy methods all return an instance of the jQuery $.Deferred
object, so you can use the done
, fail
and complete
methods to handle the results from the controller acctions:
$.proxies.person
.getAllPeople()
.done(function(people) {
//people will contain return value of PersonController.GetAllPeople(), if it succeeds
})
.fail(function(err) {
//this will be called if the controller throws an exception
//err contains exception details
})
.complete(function() {
//this will be called after success or failure
});
You can get more information on how to use the jQuery Deferred object from the documentation
MVC Controllers
The examples above are all based around Web API, but everything will work with MVC controllers as well:
$.proxies.home.index().done(function(content) {
//content will contain HTML from /Home/Index
});
Source Code
I’ve put the source code (including unit tests) on GitHub so feel free to take a look around and make any changes you think are useful.
This is an early version and will probably change quite quickly, so keep an eye out for new developments. If you have any feature suggestions then leave them in the comments (or fork and write them yourself)!