ASP.NET Web API introduces a new API for creating REST APIs and making AJAX callbacks to the server. This new API provides a host of new great functionality that unifies many of the features of many of the various AJAX/REST APIs that Microsoft created before it - ASP.NET AJAX, WCF REST specifically - and combines them into a whole more consistent API. Web API addresses many of the concerns that developers had with these older APIs, namely that it was very difficult to build consistent REST style resource APIs easily.
While Web API provides many new features and makes many scenarios much easier, a lot of the focus has been on making it easier to build REST compliant APIs that are focused on resource based solutions and HTTP verbs. But RPC style calls that are common with AJAX callbacks in Web applications, have gotten a lot less focus and there are a few scenarios that are not that obvious, especially if you're expecting Web API to provide functionality similar to ASP.NET AJAX style AJAX callbacks.
RPC vs. 'Proper' REST
RPC style HTTP calls mimic calling a method with parameters and returning a result. Rather than mapping explicit server side resources or 'nouns' RPC calls tend simply map a server side operation, passing in parameters and receiving a typed result where parameters and result values are marshaled over HTTP. Typically RPC calls - like SOAP calls - tend to always be POST operations rather than following HTTP conventions and using the GET/POST/PUT/DELETE etc. verbs to implicitly determine what operation needs to be fired.
RPC might not be considered 'cool' anymore, but for typical private AJAX backend operations of a Web site I'd wager that a large percentage of use cases of Web API will fall towards RPC style calls rather than 'proper' REST style APIs. Web applications that have needs for things like live validation against data, filling data based on user inputs, handling small UI updates often don't lend themselves very well to limited HTTP verb usage. It might not be what the cool kids do, but I don't see RPC calls getting replaced by proper REST APIs any time soon. Proper REST has its place - for 'real' API scenarios that manage and publish/share resources, but for more transactional operations RPC seems a better choice and much easier to implement than trying to shoehorn a boatload of endpoint methods into a few HTTP verbs.
In any case Web API does a good job of providing both RPC abstraction as well as the HTTP Verb/REST abstraction. RPC works well out of the box, but there are some differences especially if you're coming from ASP.NET AJAX service or WCF Rest when it comes to multiple parameters.
Action Routing for RPC Style Calls
If you've looked at Web API demos you've probably seen a bunch of examples of how to create HTTP Verb based routing endpoints. Verb based routing essentially maps a controller and then uses HTTP verbs to map the methods that are called in response to HTTP requests. This works great for resource APIs but doesn't work so well when you have many operational methods in a single controller. HTTP Verb routing is limited to the few HTTP verbs available (plus separate method signatures) and - worse than that - you can't easily extend the controller with custom routes or action routing beyond that.
Thankfully Web API also supports Action based routing which allows you create RPC style endpoints fairly easily:
RouteTable.Routes.MapHttpRoute(
name: "AlbumRpcApiAction",
routeTemplate: "albums/**{action}**/{title}",
defaults: new {
title = RouteParameter.Optional,
controller = "AlbumApi",
**action =** **"GetAblums"** }
);
This uses traditional MVC style method routing which is different from the HTTP verb based routing you might have read a bunch about in conjunction with Web API. Action based routing like above lets you specify an end point method in a Web API controller either via the parameter in the route string or via a default value for custom routes.
Using routing you can pass multiple parameters either on the route itself or pass parameters on the query string, via ModelBinding or content value binding. For most common scenarios this actually works very well. As long as you are passing either a single complex type via a POST operation, or multiple simple types via query string or POST buffer, there's no issue. But if you need to pass multiple parameters as was easily done with WCF REST or ASP.NET AJAX things are not so obvious.
Web API has no issue allowing for single parameter like this:
RouteTable.Routes.MapHttpRoute(
name: "AlbumRpcApiAction",
routeTemplate: "albums/**{action}**/{title}",
defaults: new {
title = RouteParameter.Optional,
controller = "AlbumApi",
**action =** **"GetAblums"** }
);
There are actually two ways to call this endpoint:
albums/PostAlbum
Using the Model Binder with plain POST values
In this mechanism you're sending plain urlencoded POST values to the server which the ModelBinder then maps the parameter. Each property value is matched to each matching POST value. This works similar to the way that MVC's ModelBinder works. Here's how you can POST using the ModelBinder and jQuery:
$.ajax(
{
url: "albums/PostAlbum",
**type: "POST",**
**data: { AlbumName: "Dirty Deeds", Entered: "5/1/2012" },
** success: function (result) {
alert(result);
},
error: function (xhr, status, p3, p4) {
var err = "Error " + " " + status + " " + p3;
if (xhr.responseText && xhr.responseText[0] == "{")
err = JSON.parse(xhr.responseText).message;
alert(err);
}
});
Here's what the POST data looks like for this request:
The model binder and it's straight form based POST mechanism is great for posting data directly from HTML pages to model objects. It avoids having to do manual conversions for many operations and is a great boon for AJAX callback requests.
Using Web API JSON Formatter
The other option is to post data using a JSON string. The process for this is similar except that you create a JavaScript object and serialize it to JSON first.
album = {
AlbumName: "PowerAge",
Entered: new Date(1977,0,1)
}
$.ajax(
{
url: "albums/PostAlbum",
type: "POST",
**contentType: "application/json",
** **data: JSON.stringify(album),
** success: function (result) {
alert(result);
}
});
Here the data is sent using a JSON object rather than form data and the data is JSON encoded over the wire.
The trace reveals that the data is sent using plain JSON (Source above), which is a little more efficient since there's no UrlEncoding that occurs.
BTW, notice that WebAPI automatically deals with the date. I provided the date as a plain string, rather than a JavaScript date value and the Formatter and ModelBinder both automatically map the date propertly to the Entered DateTime property of the Album object.
Passing multiple Parameters to a Web API Controller
Single parameters work fine in either of these RPC scenarios and that's to be expected. ModelBinding always works against a single object because it maps a model. But what happens when you want to pass multiple parameters?
Consider an API Controller method that has a signature like the following:
[HttpPost]
public string PostAlbum(Album album, string userToken)
Here I'm asking to pass two objects to an RPC method. Is that possible? This used to be fairly straight forward either with WCF REST and ASP.NET AJAX ASMX services, but as far as I can tell this is not directly possible using a POST operation with WebAPI.
There a few workarounds that you can use to make this work:
Use both POST and QueryString Parameters in Conjunction
If you have both complex and simple parameters, you can pass simple parameters on the query string. The above would actually work with:
/album/PostAlbum?userToken=sekkritt
but that's not always possible. In this example it might not be a good idea to pass a user token on the query string though. It also won't work if you need to pass multiple complex objects, since query string values do not support complex type mapping. They only work with simple types.
Use a single Object that wraps the two Parameters
If you go by service based architecture guidelines every service method should always pass and return a single value only. The input should wrap potentially multiple input parameters and the output should convey status as well as provide the result value. You typically have a xxxRequest and a xxxResponse class that wraps the inputs and outputs.
Here's what this method might look like:
public PostAlbumResponse PostAlbum(PostAlbumRequest request)
{
var album = request.Album;
var userToken = request.UserToken;
return new PostAlbumResponse()
{
IsSuccess = true,
Result = String.Format("{0} {1:d} {2}", album.AlbumName, album.Entered,userToken)
};
}
with these support types:
public class PostAlbumRequest {
public Album Album { get; set; }
public User User { get; set; }
public string UserToken { get; set; }
}
public class PostAlbumResponse {
public string Result { get; set; }
public bool IsSuccess { get; set; }
public string ErrorMessage { get; set; }
}
To call this method you now have to assemble these objects on the client and send it up as JSON:
var album = {
AlbumName: "PowerAge",
Entered: "1/1/1977" }
var user = {
Name: "Rick" }
var userToken = "sekkritt";
$.ajax(
{
url: "samples/PostAlbum",
type: "POST",
contentType: "application/json",
**data: JSON.stringify({ Album: album, User: user, UserToken: userToken }),**
success: function (result) {
alert(result.Result);
}
});
I assemble the individual types first and then combine them in the data: property of the $.ajax() call into the actual object passed to the server, that mimics the structure of PostAlbumRequest server class that has Album, User and UserToken properties.
This works well enough but it gets tedious if you have to create Request and Response types for each method signature. If you have common parameters that are always passed (like you always pass an album or usertoken) you might be able to abstract this to use a single object that gets reused for all methods, but this gets confusing too: Overload a single 'parameter' too much and it becomes a nightmare to decipher what your method actual can use.
Use JObject to parse multiple Property Values out of an Object
If you recall, ASP.NET AJAX and WCF REST used a 'wrapper' object to make default AJAX calls. Rather than directly calling a service you always passed an object which contained properties for each parameter:
{ parm1: Value, parm2: Value2 }
WCF REST/ASP.NET AJAX would then parse this top level property values and map them to the parameters of the endpoint method.
This automatic type wrapping functionality is no longer available directly in Web API, but since Web API now uses JSON.NET for it's JSON serializer you can actually simulate that behavior with a little extra code. You can use the JObject class to receive a dynamic JSON result and then using the dynamic cast of JObject to walk through the child objects and even parse them into strongly typed objects.
Here's how to do this on the API Controller end:
var album = {
AlbumName: "PowerAge",
Entered: "1/1/1977" }
var user = {
Name: "Rick" }
var userToken = "sekkritt";
$.ajax(
{
url: "samples/PostAlbum",
type: "POST",
contentType: "application/json",
**data: JSON.stringify({ Album: album, User: user, UserToken: userToken }),**
success: function (result) {
alert(result.Result);
}
});
This is clearly not as nice as having the parameters passed directly, but it works to allow you to pass multiple parameters and access them using Web API.
JObject is JSON.NET's generic object container which sports a nice dynamic interface that allows you to walk through the object's properties using standard 'dot' object syntax. All you have to do is cast the object to dynamic to get access to the property interface of the JSON type.
Additionally JObject also allows you to parse JObject instances into strongly typed objects, which enables us here to retrieve the two objects passed as parameters from this jquery code:
var album = {
AlbumName: "PowerAge",
Entered: "1/1/1977" }
var user = {
Name: "Rick" }
var userToken = "sekkritt";
$.ajax(
{
url: "samples/PostAlbum",
**type: "POST",
contentType: "application/json",**
data: JSON.stringify({ Album: album, User: user, UserToken: userToken }),
success: function (result) {
alert(**result**);
}
});
No comments:
Post a Comment