checklist with red check marks and male hand pointing at the list

ASP.NET Web API: A REST perspective

Work smarter in ASP.NET with help from these Web API code examples

The ASP.NET Web API exposes all the power and ease of implementation of the .NET Framework and ASP.NET in particular. As with any framework, but especially true of a large one like ASP.NET, developers might fall into bad habits and patterns that sabotage quality standards and ultimately the product or service itself. Here, we've collected a series of best-practice suggestions to help you improve your coding with the Web API.

A Counter-Example

First, let's look at a sample API controller with several examples of bad practice, as shown in Listing 1. This class was automatically generated by the built-in scaffolding system in Visual Studio, provided as a part of the October 2013 update to the ASP.NET web stack. In particular, note the following constructions to avoid:

  • the straight use of an Entity Framework (EF) DbContext—in this case DemoContext
  • the lack of attribute routing, so that the routing information is entirely separate, making versioning and controller simplicity more difficult
  • the direct return of database objects—in this case the EF entity Customer—instead of unpolluted models. This has the added effect of not operating correctly "out of the box" because circular references common in database object models cause a runtime exception in the JavaScript Object Notation (JSON) and XML parsers.

There are, however, also elements worth noticing for their benefit:

  • convention-based verb/action matching
  • the use of ApiController-provided utility methods to create responses with correct HTTP status codes

Improving the API Controller

Now let's look at a Customer controller that improves upon several of these aspects, as shown in Listing 2. Listing 3 shows a sample OrdersController from the same sample project. Both will be referenced later in the article.

[RoutePrefix("api/v2")]
public class OrdersController : ApiController
{
    private readonly IDataService _dataService;

public OrdersController(IDataService dataService)
    {
    _dataService = dataService;
    }

[Route("customers/{customerId}/orders")]
    [ResponseType(typeof(IEnumerable))]
    public IHttpActionResult GetOrdersByCustomerId(int customerId)
    {
    if (!_dataService.CustomerExists(customerId))
    {
        return NotFound();
    }
    return Ok(_dataService.GetOrders(customerId).AsModel());
    }

[Route("customers/{customerId}/orders/{orderId}")]
    [ResponseType(typeof(OrderModel))]
    public IHttpActionResult GetOrderById(int customerId, int orderId)
    {
    if (!_dataService.CustomerExists(customerId))
    {
        return NotFound();
    }

    var order = _dataService.GetOrder(orderId);
    if (order == null)
    {
        return NotFound();
    }

    if (order.Customer.Id != customerId)
    {
        return StatusCode(HttpStatusCode.Forbidden);
    }

    return Ok(order.AsModel());
    }
}

Attribute Routing

New in Web API 2, attribute routing provides a mechanism to explicitly define routes for actions in both the ASP.NET MVC and Web API controllers. This provides several benefits:

  • Gives the developer information about how to call actions and controllers decorated on the actions and controllers themselves, removing the need to police a separate route table in the global configuration of the project.
  • Allows for idiosyncratic action naming without violating REST principles in the API, providing ways for versioning and applying the single-responsibility principle to controllers.
  • Greatly reduces the complexity of managing routes.

The CustomersV2Controller is decorated with a RoutePrefix attribute, like this:

[RoutePrefix("api/v2/customers")]
public class CustomersV2Controller : ApiController

This means that every action in this controller that does not opt out inherits this as the beginning of their route. Here we see an example of how versioning can be accomplished with attribute routing: Any calls into this controller must include the v2 declaration, separating it from the CustomersController shown previously. Note the difference in naming and how this would normally break the convention-based controller mapping in previous versions of ASP.NET. Because of attribute routing, these controllers live happily together in the same project, as shown in Figure 1.

Figure 1: Controllers in an ASP.NET Project
Figure 1: Controllers in an ASP.NET Project

Defining an action's route using attribute routing with a route prefix on the controller is straightforward, as shown in Listing 4. The code comments show examples of API calls that would result in those actions being executed.

// GET api/v2/customers
[Route("")]
public IEnumerable Get()
{
    return _dataService.AllCustomers().AsModel();
}

// GET api/v2/customers/5
[Route("{id}")]
[ResponseType(typeof(CustomerModel))]
public IHttpActionResult Get(int id)
{
    var customer = _dataService.GetCustomer(id);
    if (customer == null)
    {
    return NotFound();
    }
    return Ok(customer.AsModel());
}

Web API Standards Conflict with RESTful Principles

REST relies on the concept of relations. Simply put, data relationships should be expressed and indicated by the API calls that fetch the data. In this sample project, a customer has a set of orders associated with it. This is expressed in the routes to get the data, ideally:

  • GET api/v2/customers gets the entire list of customers.
  • GET api/v2/customers/1 gets the customer with id 1.
  • GET api/v2/customers/1/orders gets the list of orders for customer 1.
  • GET api/v2/customers/1/orders/3 gets the order with id 3 for customer 1.

The conflict appears when the relation dictates grouping the calls together, yet the single-responsibility principle dictates against making controllers overly complex.

RELATED: Web API: Getting Off the Ground

The first two calls are about customers primarily, and the last two are primarily about orders. Per the single-responsibility principle, the actions that perform these calls should be in different controllers, Customers and Orders. Routing in previous versions of ASP.NET, however, made this unnecessarily difficult, as the static route definitions had to be complex to work around the convention-based controller mapping.

Attribute Routing to the Rescue

As you've seen in the previous examples, attribute routing allows the calls to be separated across controllers (maintaining single-responsibility) and decorated with routes matching the REST style. It's a win-win approach.

The OrdersController is decorated with the version number:

[RoutePrefix("api/v2")]
public class OrdersController : ApiController

The GET actions are decorated with the specific routes used to get there:

[Route("customers/{customerId}/orders")]
[ResponseType(typeof(IEnumerable))]
public IHttpActionResult GetOrdersByCustomerId(int customerId) ...

[Route("customers/{customerId}/orders/{orderId}")]
[ResponseType(typeof(OrderModel))]
public IHttpActionResult GetOrderById(int customerId, int orderId) ...

Note the convention-based mapping happening between the Route declaration and the parameters taken by the action. Also note the idiosyncratic action naming: ASP.NET routes the API calls correctly using HTTP verbs taken from the names of the actions. In this case, both actions are inferred to respond to the GET verb and will not be called on any other verb. This allows for descriptive action naming without breaking REST principles in the routes.

RELATED: Microsoft ASP.NET: What's This New Web API?

As an aside, in defense of the decision to have controllers in the Web API, the controllers make it easy for MVC developers to learn the Web API because of their familiarity with the controller model. The naming was very Remote Procedure Call (RPC) oriented, and again the decision behind that could have been to help Windows Communication Foundation (WCF) developers more easily learn the Web API because of the resemblance of Web API's naming scheme to WCF naming conventions.

Controller Complexity

The sample controller embeds significant functionality into the controller and its actions. Database communication in particular is used extensively, tightly coupling the controller with the specific data storage technology (EF, in this case). This increases the responsibilities of the controller, making maintenance and code changes more cumbersome, as shown in Listing 5.

// DELETE api/Customer/5
[ResponseType(typeof(Customer))]
public IHttpActionResult DeleteCustomer(int id)
{
    Customer customer = db.Customers.Find(id);
    if (customer == null)
    {
    return NotFound();
    }

db.Customers.Remove(customer);
    db.SaveChanges();

return Ok(customer);
}

protected override void Dispose(bool disposing)
{
    if (disposing)
    {
    db.Dispose();
    }
    base.Dispose(disposing);
}

For example, this controller must care about pushing changes to data to the final storage mechanism itself, as well as be responsible for the lifecycle of the database connection. Any potential changes to the underlying data system must necessarily include changes to the controller, which increases the developer burden throughout the application or service.

A proper way to include data create, read, update, and delete (CRUD) operations is to use an abstracted service, preferably injected through some form of inversion of control (IoC), as seen in the controller CustomersV2Controller, shown in Listing 6. In this way, changes to the underlying data service do not percolate up to the controller level, provided the interfaces stay the same.

private readonly IDataService _dataService;

public CustomersV2Controller(IDataService dataService)
{
    _dataService = dataService;
}

// GET api/v2/customers
[Route("")]
public IEnumerable Get()
{
    return _dataService.AllCustomers().AsModel();
}

Use Models

A significant issue with the first example in Listing 1 is that the controller exposes the database objects and other information in its return data. Typically this information is unnecessary to consumers of the API and frequently also relies on domain-specific or sensitive information that should not be exposed. The leakage of this sort of information is especially important when the API could potentially reveal personally identifiable information, the exposure of which could be illegal.

RELATED: Microsoft's Web API Framework: Bridging the Divide Between Web Forms and ASP.NET MVC

A much better implementation flattens the response objects, removes unnecessary information, and returns only the specific information necessary. This functionality should be separate from the controller and action, preferably injected through some form of IoC. In this example, this is accomplished by an extension method AsModel, overloaded to take the various data types and return cleaner model representations, as shown in Listing 7.

// GET api/v2/customers/5
[Route("{id}")]
[ResponseType(typeof(CustomerModel))]
public IHttpActionResult Get(int id)
{
    var customer = _dataService.GetCustomer(id);
    if (customer == null)
    {
    return NotFound();
    }
    return Ok(customer.AsModel());
}

Following from the controller complexity concern, this mapping functionality may be a responsibility of the service that fetches the data in the first place, to further reduce the responsibilities of the controller itself.

Use Meaningful HTTP Status Codes

One of the powerful tools that REST enables is the use of HTTP standards, specifically, verbs and status codes. Verbs allow for API calls to have an intent—that is, to get some information, delete other information, post some new data, and so on. Web API already does this well, using the convention-based action naming and attributes such as HttpGet otherwise.

HTTP status codes provide a powerful way to respond to API calls without having to develop custom schemes and are already built into the communication. 200 codes signify success, 300 mean redirection, 400 specify client errors, and 500 identify server errors. Specific codes imply different situations per verb, and some make sense only as a response to a certain type of request by a client.

Because Web API is primarily concerned with creating powerful REST services, following the standards in this case is incredibly important. The ApiController base class provides several built-in utilities for using these codes in responses and should be used generally. These utilities are not strictly necessary to use in Web API, but using them will often reveal gaps in functionality and can help a developer to program defensively.

The use of status codes in the OrdersController class, as shown in Listing 8, will show the consumer of the API clearly why the call fails, should the failure be the fault of incorrect input data. This functionality is baked into Web API and should be leveraged when possible.

[Route("customers/{customerId}/orders/{orderId}")]
[ResponseType(typeof(OrderModel))]
public IHttpActionResult GetOrderById(int customerId, int orderId)
{
    if (!_dataService.CustomerExists(customerId))
    {
    return NotFound();
    }

var order = _dataService.GetOrder(orderId);
    if (order == null)
    {
    return NotFound();
    }

if (order.Customer.Id != customerId)
    {
    return StatusCode(HttpStatusCode.Forbidden);
    }

return Ok(order.AsModel());
}

Prefer Body Content over Query String

The REST principles encourage use of paging and filtering capabilities when getting data—all well and good and not specific to Web API. Developers using Web API should absolutely use these ideas, typically via the query string and automatic mapping to parameters. A fine use case!

When receiving structured data, however, using the query string to receive and map the data can be cumbersome and complicated. Web API supports reading this content from the body of the web request, and can in fact support mapping this content to parameters in actions, removing the need to muck about in the Request object and parse the data specifically.

The improved CustomersV2Controller in Listing 9 shows an example. A developer must simply decorate the parameter with the [FromBody] tag to take advantage of this functionality.

// PUT api/v2/customers/5
[Route("{id}")]
public IHttpActionResult Put(int id, [FromBody]CustomerModel customer)
{
    if (!ModelState.IsValid)
    {
    return BadRequest(ModelState);
    }

if (id != customer.Id)
    {
    return BadRequest();
    }

if (!_dataService.CustomerExists(customer.Id))
    {
    return NotFound();
    }

_dataService.Save(customer);

return StatusCode(HttpStatusCode.NoContent);
}

Continuous Improvement

As the title suggests, this article is a different perspective for Web API in relation to REST. This is in no way a criticism of the awesome work the Web API team has been doing. However, it is more of a perspective to spread the goodness of Web API and show that the recent work done by the team is making it a great framework for RESTful services. For programmers, the quest to create more efficient, elegant code never ends. The examples I've provided here show you some of the many ways you can improve your coding with the ASP.NET Web API.

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish