In this article, we will discuss Web API design, concepts, and features, and compare Web API with WCF.
Implementing a Web API Project
Let’s start our discussion with a sample Web API project. We will be using Visual Studio 2012 as our development environment. Our first step will be to create an ASP.NET MVC 4.0 project based on the Web API template, as shown in Figure 1.
Figure 1: Creating an ASP.NET MVC 4 project based on the Web API template
Next, we’ll create a sample model inside the model folder in the MVC solution. Right click on the Model folder from the solution explorer and select Add -> Class as shown in Figure 2.
Figure 2: Add new Model
For this walk-through, I am using the Product model defined in Listing 1.
public class Product
{
public int Id
{ get; set; }
public string Name
{ get; set; }
public string Category
{ get; set; }
public decimal Price
{ get; set; }}
Listing 1: Defining the Product model
After we create the Product model, we can create a new API controller inside the controllers’ folder to process the Product model. By default, an MVC project based on the Web API template adds two controllers: one inherited from the Controller class and the other from ApiController class. Right-click the controllers folder in Solution Explorer and add a new controller by selecting the Empty API controller option under template as shown in Figure 3.
Figure 3: Add Empty API Controller
The code for the sample controller should look like that shown in Listing 2.
public class ProductsController : ApiController
{
List<Product> products = new List<Product>();
public IEnumerable<Product> GetAllProducts()
{
GetProducts();
return products;
}
private void GetProducts()
{
products.Add(new Product {Id = 1, Name = "Television", Category="Electronic", Price=82000});
products.Add(new Product { Id = 2, Name = "Refrigerator", Category = "Electronic", Price = 23000 });
products.Add(new Product { Id = 3, Name = "Mobiles", Category = "Electronic", Price = 20000 });
products.Add(new Product { Id = 4, Name = "Laptops", Category = "Electronic", Price = 45000 });
products.Add(new Product { Id = 5, Name = "iPads", Category = "Electronic", Price = 67000 });
products.Add(new Product { Id = 6, Name = "Toys", Category = "Gift Items", Price = 15000 });
}
public IEnumerable<Product> GetProducts(int selectedId)
{
if (products.Count() > 0)
{
return products.Where(p => p.Id == selectedId);
}
else
{
GetProducts();
return products.Where(p => p.Id == selectedId);
}
}
Listing 2: Adding an ApiController class to the sample project
Run the project and access the API by appending the uniform resource locator (URL) with /api/Products, as in http://localhost:8747/api/Products. You can also access the GetProducts method by passing the SelectedId argument as part of the query string: http://localhost:8747/api/Products?SelectedId=2.
Passing Complex Objects to a Web API Method
Passing a simple value is a straightforward process in Web API, but in most cases you’ll need to pass a complex object as a method parameter. You can pass these objects either by using the [FromUrl] or by using [FromBody] attribute. The [FromBody] attribute reads data from the request body. However, the attribute can be used only once in the method parameter list.
Let us understand the importance of using complex parameters using the code snippet in Listing 3, which expects a complex object, Time, as its input.
public class SampleController : ApiController
{
public string GetTime(Time t)
{
return string.Format("Received Time: {0}:{1}.{2}", t.Hour, t.Minute, t.Second);
}
}
public class Time
{
public int Hour { get; set; }
public int Minute { get; set; }
public int Second { get; set; }
}
Listing 3: Passing a complex object as a method parameter
Now, let us try to pass the values to the GetTime method using the Hour, Minute and Second values. As you can see in Figure 4, this will return the null reference exception.
Figure 4: Receiving a null reference exception when calling the GetTime method
Even though we have mentioned the values related to the fields of the Time object, API method is not able to map the values properly to the parameter. Now let’s look at what happens if we modify the code to include the [FromUri] attribute so it can handle the complex object from query string. As you can see in the following snippet, we include the [FromUri] attribute before specifying the Time object:
public string GetTime([FromUri] Time t)
{ -------------------}
Now if we were to call the method, passing in the same values before, we would observe much different behavior, as shown in Figure 5.
Figure 5: Receiving successful results when calling the GetTime method
Working with the HttpClient API
HttpClient is an extensible API for accessing any services or web sites exposed over HTTP. The HttpClient API was introduced as part of the WCF Web API but is now available as part of ASP.NET Web API in.NET Framework 4.5. You can use HttpClient to access Web API methods from a code-behind file and from services such as WCF.
The code snippet shown in Listing 4 creates an HttpClient object and uses it for asynchronous access to sample API methods. Refer the code comments to understand the code snippet.
async Task GetData()
{
StringBuilder result = new StringBuilder();
{
client.BaseAddress = new Uri(HttpContext.Current.Request.Url.AbsoluteUri);
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage response = client.GetAsync("api/products").Result;
if (response.IsSuccessStatusCode)
{
var products = response.Content.ReadAsAsync<IEnumerable<Product>>().Result;
foreach (var p in products)
{
result.Append(string.Format("{0} --- [Price: {1} Category: {2}] ", p.Name, p.Price, p.Category));
}
}
else
{
result.Append(string.Format("Error Code:-{0} Error Details: {1}", (int)response.StatusCode, response.ReasonPhrase));
}
}
data.Text = result.ToString();
}
Listing 4: Creating an HttpClient object to access sample API methods
Accessing the Web API from jQuery
From an HTML5 Web application, you can use jQuery to directly access the Web API. You call the API by using the getJSON() method to extract the business object from the result. jQuery can identify internal fields associated with the business object, such as data[i].Price.
The code snippet in Listing 5 retrieves data from our Product API methods and displays the data in a tabular format. Refer the code comments to understand the code snippet.
<head>
<title></title>
<script src="Scripts/jquery-1.8.2.js"></script>
<script type="text/javascript">
$.getJSON("api/products",
function (data) {
$("#productView").empty();
var $table = $('<table border="2">');
var $tbody = $table.append('<tbody />').children('tbody');
for (var i = 0; i < data.length; i++) {
var $trow=$tbody.append('<tr />').children('tr:last');
var $tcol = $trow.append("<td/>").children('td:last')
.append(data[i].Name);
var $tcol = $trow.append("<td/>").children('td:last')
.append(data[i].Category);
var $tcol = $trow.append("<td/>").children('td:last')
.append(data[i].Price);
}
$table.appendTo('#productView');
});
</script>
</head>
<body>
<div id="productView"></div>
</body>
</html>
Listing 5: Using jQuery to retrieve data from the Web API methods
Passing a Parameter from jQuery
In addition to retrieving data from the Web API methods, you can use jQuery to pass parameters back to the Web API. jQuery supports several methods for passing the parameters. The first approach is to use separate parameter set. The following code snippet demonstrates how to pass a single parameter:
$.getJSON("api/products",
{ selectedId: '4' },
function (data) { ….});
You can also pass multiple parameters within a parameter set, as shown in the following snippet:
$.getJSON("api/products",
{ selectedId: '4', name: 'TV' },
function (data) { ….});
Another approach you can take is to pass a parameter by appending it to the URL query string, as shown in the following snippet:
$.getJSON("api/products?selectedId =4",
function (data) { ….});
First approach separates the query string or parameter passing from the URL and is appropriate for complex or multiple data passing. Second approach is more suitable for passing one or two parameters.
Scaffolding with Web API
ASP.NET Scaffolding is a code generation framework for ASP.NET Web applications. Visual Studio includes pre-installed code generators for MVC and Web API projects. Visual Studio automatically generates the API code required to perform various operations on the specified model or data source.
Basing Our Scaffold on the Entity Framework
To demonstrate how scaffolding works in a Web API project, we can use the Entity Framework to define our database and generate the Web API methods. First, we need to add an ADO.NET entity data model that defines the data. Our sample data model will be based two tables, Course and Status, as shown in Figure 6.
Figure 6: Adding an entity data model based on the Course and Status tables
After we define the data model, we can generate the Web API controller that corresponds to the model. Right-click Controller and then click Add Controller, which lunches the Add Controller dialog box. In the Controller name text box, type the controller name. Then, from the Template drop-down list, select the API controller with read/write actions, using Entity Framework template, as shown in Figure 7.
Figure 7: Adding a controller based on the data model
Next, select one of the tables from the Model class drop-down list and, in the Data context class drop-down list, specify the data context created as part of the ADO.NET entity data model. Listing 6 shows the automatic code generated for the CourseController API controller. Notice that it contains methods for to support get, put, post, and delete operations that correspond to the selected model.
public class CourseController : ApiController
{
private SampleDBEntities db = new SampleDBEntities();
public IEnumerable<Course> GetCourses()
{
return db.Courses.AsEnumerable();
}
public Course GetCourse(int id)
{
Course course = db.Courses.Single(c => c.CourseId == id);
if (course == null)
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
}
return course;
}
public HttpResponseMessage PutCourse(int id, Course course)
{
if (!ModelState.IsValid)
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
if (id != course.CourseId)
{
return Request.CreateResponse(HttpStatusCode.BadRequest);
}
db.Courses.Attach(course);
db.ObjectStateManager.ChangeObjectState(course, EntityState.Modified);
try
{
db.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
return Request.CreateErrorResponse(HttpStatusCode.NotFound, ex);
}
return Request.CreateResponse(HttpStatusCode.OK);
}
public HttpResponseMessage PostCourse(Course course)
{
if (ModelState.IsValid)
{
db.Courses.AddObject(course);
db.SaveChanges();
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created, course);
response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = course.CourseId }));
return response;
}
else
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
}
public HttpResponseMessage DeleteCourse(int id)
{
Course course = db.Courses.Single(c => c.CourseId == id);
if (course == null) {
return Request.CreateResponse(HttpStatusCode.NotFound);
}
db.Courses.DeleteObject(course);
try
{
db.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
return Request.CreateErrorResponse(HttpStatusCode.NotFound, ex);
}
return Request.CreateResponse(HttpStatusCode.OK, course);
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
Listing 6: Code generated for the CourseController API controller
The Web API scaffolding feature reduces the development effort required to generate the API methods necessary to perform CRUD (create, read, update, delete) operations in different models.
Routing in ASP.NET Web API
Web API uses routing to match uniform resource identifiers (URIs) to various actions. The WebApiConfig file, located inside the App_Start node in Solution Explorer, defines the default routing mechanism used by Web API. The mechanism is based on a combination of HTTP method, action, and attribute. However, we can define our own routing mechanism to support meaningful URIs.
For example, in our sample Web API project, we use /api/Products to retrieve product details. However, we can instead use api/Products/GetAllProducts by modifying the default routing logic to include {action} as part of the URI, as shown in the following code snippet:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
After we update the route, we can use the ActionName attribute in our Web API method to specify the action name, as the follow snippet shows:
[ActionName("GetAllProducts")]
public IEnumerable<Product> GetAllProducts()
{……………………………..}
NOTE: Use the [NonAction] attribute to indicate that the Web API method is not an API action method.
We can also use an HTTP method or Acceptverbs attribute to specify the HTTP action required to access the Web API method. For example, the following snippet uses the HttpGet and HttpPost methods:
[HttpGet]
public IEnumerable<Product> GetProducts(int selectedId)
{………….. }
[HttpPost]
public void AddProduct(Product p)
{………….. }
For multiple methods, use the Acceptsverbs attribute, as shown in the following snippet:
[AcceptVerbs("GET","PUT")]
public IEnumerable<Product> GetProducts(int selectedId)
{………….. }
Web API 2 also makes it possible to use attributes in our routing. Refer to the article "Attribute Routing in Web API v2" by Dino Esposito for more details.
Implementing Multiple Serialization Methods
Web API supports numerous serialization methods, including XML, JSON, and MessagePack. In the sections to follow, we’ll take a closer look at how to implement these three methods and then compare their behavior. Before doing that, however, let’s modify the sample Web API project to return bulk data so we can better understand and compare the performance of the serialization methods. Listing 7 shows how the GetProducts() method updated to return bulk data.
private void GetProducts()
{
for (int i = 0; i < 5000; i++)
{
products.Add(new Product { Id = i, Name = "Product - "+i,
Category = "The ASP.NET and Visual Web Developer teams have released the ASP.NET and Web Tools 2012.2 update, which extends the existing ASP.NET runtime and adds new web tooling to Visual Studio 2012. Whether you use Web Forms, MVC, Web API, or any other ASP.NET technology, there is something cool in this update for you.",
Price = 1 });
}
}
Listing 7: Updating the GetProducts() method
Using JSON Serialization
Web API uses JSON as its default serialization method. As a result, when you run the Web API project, the results are automatically returned in the JSON format. For example, to retrieve details from Products, you need only use the syntax http://<<server>>:<<port>>/api/Products to return the results in the JSON format.
Using XML Serialization
To use the XML serialization method in a Web API project, you must modify the Global.asax file by inserting the following two lines of code at the end of Application_Start() method:
GlobalConfiguration.Configuration.Formatters.RemoveAt(0);
GlobalConfiguration.Configuration.Formatters.XmlFormatter.UseXmlSerializer = true;
After implementing the XML serialization method, you can retrieve details from Products by using the syntax http://<<server>>:<<port>>/api/Products, the same syntax used for JSON.
Using MessagePack Serialization
To use the MessagePack serialization method, you must first install MessagePack (http://msgpack.org/). MessagePack is available as NuGet package. You can also download the source code from GitHub and build the application in Visual Studio.
Once you’ve installed MessagePack, use the MsgPack.dll file to add a reference to the MessagePack library. Next, create a custom media type formatter that uses MessagePack to serialize the Web API data. Listing 8 shows the code for a media type formatter based on a GitHub sample (https://gist.github.com/filipw/3693339).
public class MediaTypeFormatterCompatibleMessagePack : MediaTypeFormatter
{
private readonly string _mime = "application/x-msgpack";
Func<Type, bool> IsAllowedType = (t) =>
{
if (!t.IsAbstract && !t.IsInterface && t != null && !t.IsNotPublic)
return true;
if (typeof(IEnumerable).IsAssignableFrom(t))
return true;
return false;
};
public MediaTypeFormatterCompatibleMessagePack()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue(_mime));
}
public override bool CanWriteType(Type type)
{
if (type == null)
throw new ArgumentNullException("Type is null");
return IsAllowedType(type);
}
public override Task WriteToStreamAsync(Type type, object value, System.IO.Stream stream, HttpContent content, TransportContext transportContext)
{
if (type == null)
throw new ArgumentNullException("type is null");
if (stream == null)
throw new ArgumentNullException("Write stream is null");
var tcs = new TaskCompletionSource<object>();
if (type != typeof(string) && typeof(IEnumerable).IsAssignableFrom(type))
{
value = (value as IEnumerable<object>).ToList();
}
var serializer = MessagePackSerializer.Create<dynamic>();
serializer.Pack(stream, value);
tcs.SetResult(null);
return tcs.Task;
}
public override Task<object> ReadFromStreamAsync(Type type,Stream stream, HttpContent content, IFormatterLogger formatterLogger)
{
var tcs = new TaskCompletionSource<object>();
if (content.Headers != null && content.Headers.ContentLength == 0)
return null;
try
{
var serializer = MessagePackSerializer.Create(type);
object result;
using (var mpUnpacker = Unpacker.Create(stream))
{
mpUnpacker.Read();
result = serializer.UnpackFrom(mpUnpacker);
}
tcs.SetResult(result);
}
catch (Exception e)
{
if (formatterLogger == null) throw;
formatterLogger.LogError(String.Empty, e.Message);
tcs.SetResult(GetDefaultValueForType(type));
}
return tcs.Task;
}
public override bool CanReadType(Type type)
{
if (type == null)
throw new ArgumentNullException("type is null");
return IsAllowedType(type);
}
}
Listing 8: Building a MessagePack media type formatter
After you create your media type formatter, you must update the Global.asax file to use the formatter. Insert the following code at the end of Application_Start() method to use MessagePack serialization:
GlobalConfiguration.Configuration.Formatters.Clear();
GlobalConfiguration.Configuration.Formatters.Add(new MediaTypeFormatterCompatibleMessagePack());
Comparing Serialization Methods
You can test JSON, XML, and MessagePack serialization in Web API by using Fiddler tool. Open Fiddler and navigate to the Request Builder tab. Enter the MessagePack Web API URL in Request Builder and add application/x-msgpack as Content-Type under the Request Headers section, as shown in Figure 8.
Figure 8: Request Builder in Fiddler
Now, click the Execute button to invoke the Web API method. Repeat the process for JSON and XML based Web API methods, without specifying the Content-Type request header. Compare the results in terms of body size, bytes received, and elapsed Time as shown in Figure 9.
Figure 9: Comparing serialization methods in Fiddler
As shown in above figure, result from message pack based serialization will be compressed compared to other serializations. Size of the JSON result will be less compared to that of XML based serialization.
Help Page Generation from XML Documentation
By default, Web API provides help pages related to the API methods defined in the project. We can generate the help pages from the XML documentations provided as part of the method definition.
To create the help pages, expand the Areas node in Solution Explorer (shown in Figure 10) and then open the HelpPageConfig.cs file
Figure 10: Accessing the HelpPageConfig.cs file in the sample Web API project
In the HelpPageConfig.cs file, un-comment the following statement:
config.SetDocumentationProvider(new XmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/XmlDocument.xml")));
Next, enable XML documentation by selecting the XML documentation file option in the Build section of the project’s properties, as shown in Figure 11. Copy the filename specified in the config file to the XML documentation file option.
Figure 11: Enabling the XML documentation file in the Web API project
Now, let’s add the XML documentation to our sample API controller, as shown in Listing 9. Just before each method definition, specify "///" to get the default XML documentation for the method. Modify further with more specific comments.
public IEnumerable<Product> GetAllProducts()
{
---------------
}
public IEnumerable<Product> GetProducts(int selectedId)
{
------------------
}
Listing 9: Adding XML documentation to the sample API controller
Once you’ve updated the controller, run the application and navigate to API link at the top-right corner of the browser screen, as shown in Figure 12.
Figure 12: Viewing the API link in the application window
Click the API link to open the help page. Notice the XML documentation comments that had been added to the controller, as shown in Figure 13.
Figure 13: Viewing the comments in the Web API help file
WCF via Web API
WCF supports numerous protocols and messaging formats. With Web API, we can create only HTTP-based services. Obviously, we can create HTTP services with WCF, but they’re hidden inside different abstraction layers. In addition, implementation is more complex with WCF than with Web API. HTTP implemented on WCF uses the HTTP post model, and we may not be able to use the main features associated with HTTP protocols such as Caching and Status Code. By using Web API, we can perform all activities like get, post, and put, and we can use different HTTP protocol features.
WCF requires complex binding and address manipulation and configurations, but Web API is straightforward and simple to use. Web API is also a better choice for RESTfull service creation. You can find a detailed comparison between WCF and Web API in the MSDN article "WCF and ASP.NET Web API."
Update 1:
For passing complex data type to a Web API method using jQuery, use the JSON.Stringify to properly pass the JSON data to the method.
For example, following code snippet shows, how you can pass the custom data "user" to a Web API method
var user = {};<br /> user.name = 'Mathew';<br /> user.job = 'ITA';<br /> user.rating = 'Good';
$.getJSON("api/getEmployeeDetail",<br /> {JSON.stringify(user)},<br /> function (data) { ......}<br /> );