CHAPTER 11

image

Action Method Results

In this chapter, I start digging into the details of how Web API web services work, starting right at the heart of web services, namely, the different ways that action methods return results and how these are used to generate HTTP responses. As you will learn, Web API has convenient features that use the standard characteristics of C# methods to express results, which makes generating the most common types of results easy. Behind this convenience is a flexible and extensible system of action results, which are similar to the ones used by the MVC framework and which allow for complete control over the HTTP response sent to the client. I explain how this system works and demonstrate how you can customize it. Table 11-1 summarizes this chapter.

Table 11-1. Chapter Summary

Problem

Solution

Listing

Define an action method that doesn’t return any data.

Return void from the method.

1–4

Define an action method that returns a result.

Return an implementation of the IHttpActionResult interface from the action method.

5–10

Select the data format that will be used for serialized data.

Create a content negotiation class.

11, 12

Register a content negotiation class.

Replace the service implementation of the IContentNegotiator interface with the custom class.

13, 14

Specify a result code to be used in a response that contains serialized data.

Create a negotiable action result.

15

Preparing the Example Project

In this chapter, I am going to continue working with the ExampleApp project I created in Chapter 10. There is one change required to prepare for this project, which is to change the dependency injection object life cycle for the Repository class. In Chapter 10, I was focused on showing you how to create objects that are scoped to individual requests because that is what is usually required for real repository objects that are backed by a database, such as the one I used in Chapter 5. My example Repository class, however, keeps its model data in memory, which means I need to create one instance of the Repository class and use it throughout the life of the application; otherwise, each request will be working solely with the default data. Listing 11-1 shows the change I made to the NinjectResolver class to change the scope of the Repository class.

Image Tip  Remember that you don’t have to create the example project yourself. You can download the source code for every chapter for free from Apress.com.

Listing 11-1. Changing an Object Scope in the NinjectResolver.cs File

using System;
using System.Collections.Generic;
using System.Web.Http.Dependencies;
using ExampleApp.Models;
using Ninject;
using Ninject.Extensions.ChildKernel;
using Ninject.Web.Common;

namespace ExampleApp.Infrastructure {

    public class NinjectResolver : System.Web.Http.Dependencies.IDependencyResolver,
            System.Web.Mvc.IDependencyResolver {
        private IKernel kernel;

        public NinjectResolver() : this(new StandardKernel()) { }

        public NinjectResolver(IKernel ninjectKernel) {
            kernel = ninjectKernel;
            AddBindings(kernel);
        }

        public IDependencyScope BeginScope() {
            return this;
        }

        public object GetService(Type serviceType) {
            return kernel.TryGet(serviceType);
        }

        public IEnumerable<object> GetServices(Type serviceType) {
            return kernel.GetAll(serviceType);
        }

        public void Dispose() {
            // do nothing
        }

        private void AddBindings(IKernel kernel) {
            kernel.Bind<IRepository>().To<Repository>().InSingletonScope();
        }
    }
}

As I explained in Chapter 10, using the InSingletonScope method means that one instance will be created and used to resolve all of the dependencies for the IRequest interface in the application. (As a reminder, this is the version of the resolver that supports applications containing MVC and Web API components.)

Understanding Action Method Results

As I explain in detail in Chapter 22, the goal of a controller is to use an action method to process an HttpRequestMessage object in order to create an HttpResponseMessage object. The HttpRequestMessage describes the request to be handled, and the HttpResponseMessage describes the response to be returned to the client. The hosting environment (which will typically be IIS, but see Chapter 26 for another option) is responsible for creating the HttpResponseMessage object to represent the request and turning the HttpRequestMessage into an HTTP response and sending it to the client. Figure 11-1 shows the basic flow.

9781484200865_Fig11-01.jpg

Figure 11-1. The basic request and result flow

The controller provides the action method with the data contained in the request using model binding, which I describe in Chapters 14–17, and about the request itself through the HttpRequestMessage object. The HttpRequestMessage object is part of the System.Net.Http namespace and presents a general view of an HTTP request that Web API can operate on using the properties shown in Table 11-2.

Table 11-2. The Properties Defined by the HttpRequestMessage Class

Name

Description

Content

Returns an HttpContent object that contains the content of the HTTP request. Request content is accessed through the model binding feature, which I describe in Chapters 1417.

Headers

Returns an HttpRequestHeaders object that contains the headers sent by the client.

Method

Returns an HttpMethod object that describes the HTTP method/verb for the request.

Properties

Returns a dictionary that contains objects provided by the hosting environment.

RequestUri

Returns the URL requested by the client, expressed as an Uri object.

Version

Returns the version of HTTP that was used to make the request, expressed as a System.Version object.

Action methods can return C# objects that represent model data or by creating an HttpResponseMessage object directly. Actions can also elect to return a result but still respond to the client to acknowledge that an operation has been successfully completed. I describe the different kinds of results in the sections that follow, and Table 11-3 puts action methods results into context.

Table 11-3. Putting Action Method Results in Context

Question

Answer

What is it?

The result from an action method describes the HTTP response that will be sent to the client.

When should you use it?

You need to explicitly specify results when you want control over the HTTP response sent to the client, but all action methods produce results, even when the void keyword is used in the method signature.

What do you need to know?

Web API has some nice features that hide away the details of creating HTTP responses for common outcomes, but you will need to understand the different kinds of results that are available to get full control over the operation of a web service.

Returning No Result

The simplest way to respond to an HTTP request is to return no result data at all. This isn’t as odd as it might seem because web services often need to provide actions that perform work but that don’t generate a data response. As an example, a request to delete an object from the repository may not require any data to be returned to the client because the HTTP status code will indicate whether the operation was successful. A status code in the 200 range will indicate success, and a code in the 400 or 500 range will indicate a failure. Action methods that don’t produce data return void, as shown in Listing 11-2.

Listing 11-2. Adding an Action Method That Returns void in the ProductsController.cs File

using System.Collections.Generic;
using System.Web.Http;
using ExampleApp.Models;

namespace ExampleApp.Controllers {
    public class ProductsController : ApiController {
        IRepository repo;

        public ProductsController(IRepository repoImpl) {
            repo = repoImpl;
        }

        public IEnumerable<Product> GetAll() {
            return repo.Products;
        }

        public void Delete(int id) {
            repo.DeleteProduct(id);
        }
    }
}

I have added a Delete action method that calls the corresponding method defined by the repository. The method returns void, which means that no data will be returned to the client.

The simplest way to test the action method is with Postman, which will clearly display the HTTP result code returned by the server. Sending an HTTP DELETE request to /api/products/1 will result in status code 204, as shown in Figure 11-2.

9781484200865_Fig11-02.jpg

Figure 11-2. Targeting an action method that returns no data

Status code 204 is the No Data code, which is defined as follows:

The server has fulfilled the request but does not need to return an entity-body.

You can see the full W3C definition of status codes at www.w3.org/Protocols/rfc2616/rfc2616-sec10.html, but this is the result code that is used most commonly for delete operations in web services.

Consuming a No Result Action Method

jQuery treats any HTTP status code in the 200 range as a success, so dealing with action methods that don’t return data is a matter of defining a success callback function that updates the client-side data model to reflect the operation that has been performed. Listing 11-3 shows the changes that I made to the Index.cshtml file to add Delete buttons for each product data object.

Listing 11-3. Adding Product Delete Buttons to the Index.cshtml File

@model IEnumerable<ExampleApp.Models.Product>
@{ ViewBag.Title = "Index";}

@section Scripts {
    <script>
        var products = ko.observableArray(
            @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model)));
    </script>
    <script src="~/Scripts/exampleApp.js"></script>
}

<div class="panel panel-primary">
    <div class="panel-heading">RSVPs</div>
    <table id="rsvpTable" class="table table-striped">
        <thead>
            <tr><th>ID</th><th>Name</th><th>Price</th></tr>
        </thead>
        <tbody data-bind="foreach: products">
            <tr>
                <td data-bind="text: ProductID"></td>
                <td data-bind="text: Name"></td>
                <td data-bind="text: Price"></td>
                <td>
                    <button class="deleteBtn btn btn-danger btn-xs"
                        data-bind="click: deleteProduct">
                        Delete
                    </button>
                </td>
            </tr>
        </tbody>
    </table>
</div>
<button data-bind="click: getProducts" class="btn btn-primary">Refresh</button>

I have added a column to the table, and each td element contains a Deslete button to which I have applied the Knockout click binding to invoke a function called deleteProduct when the button elements are clicked. Listing 11-4 shows the implementation of the deleteProduct function in the exampleApp.js file.

Listing 11-4. Handling Button Events in the exampleApp.js File

$(document).ready(function () {

    deleteProduct = function (data) {
        $.ajax("/api/products/" + data.ProductID, {
            type: "DELETE",
            success: function () {
                products.remove(data);
            }
        })
    };

    getProducts = function () {
        $.ajax("/api/products", {
            success: function (data) {
                products.removeAll();
                for (var i = 0; i < data.length; i++) {
                    products.push(data[i]);
                }
            }
        })
    };
    ko.applyBindings();
});

As I explained in Chapter 3, Knockout passes the data item associated with the element that triggered the click binding to the callback function, which means I can read the value of the ProductID property to create the URL that I need to target, like this:

...
$.ajax("/api/products/" + data.ProductID, {
...

I use the type property to tell jQuery to make a DELETE request, and Web API uses the HTTP verb and the URL to target the Delete action method on the Products controller (I explain how this happens in Chapter 22). The action method performs the delete operation on the repository but doesn’t return a result because the method was defined with the void keyword.

The 204 status code will cause jQuery to invoke my success function, which I defined without arguments because I am not expecting to receive data back from the web service. I remove the data object that Knockout passed to the deleteProduct function from the model array, which causes the contents of the table element to be updated, as shown in Figure 11-3.

9781484200865_Fig11-03.jpg

Figure 11-3. Deleting items from the repository

AVOIDING THE URL VS. BODY PITFALL

Notice that I constructed the URL for the DELETE request so that it included the ProductID property value of the object I wanted to remove from the repository, like this:

...
deleteProduct = function (data) {
    $.ajax("/api/products/" + data.ProductID, {
        type: "DELETE",
        success: function () {
            products.remove(data);
        }
    })
};
...

If you have experience using jQuery to make Ajax requests, then you might expect to be able to include the value of the ProductID property in the request body, like this:

...
deleteProduct = function (data) {
    $.ajax("/api/products", {
        type: "DELETE",
        data: {id: data.ProductID},
        success: function () {
            products.remove(data);
        }
    })
};
...

This will result in an error because the Delete method in the Products controller is targeted based on the URL without taking into account the data contained in the request body. The effect of the previous code is to send a DELETE request to an action method that will accept only GET requests. I explain how Web API routing works in Chapters 20 and 21 and how parameter values are extracted from requests in Chapters 1417, but the key point for this chapter is that you must ensure that the URLs you request uniquely identify the object or objects you want to operate on.

Returning an Action Result

The next step up from returning no result is to return an implementation of the IHttpActionResult interface, which is roughly equivalent to the ActionResult class in the MVC framework.

Web API goes to a lot of effort to make returning results from action methods as simple as possible, taking responsibility for creating HttpResponseMessage objects for you whenever possible. You saw this in the previous section for void action methods, and you’ll see it again in the “Returning Model Data” section when I demonstrate how model objects are automatically serialized.

The IHttpActionResult interface allows an action method to specify how HttpResponseMessage objects should be generated as instructions, which are then executed to produce the HttpResponseMessage that is used to respond to the client. In this section of this chapter, I explain how the IHttpActionResult interface fits into Web API and demonstrate the different ways it can be used. Table 11-4 puts action methods that return implementations of the IHttpActionResult interface into context.

Table 11-4. Putting Action Methods That Return IHttpActionResult into Context

Question

Answer

What is it?

Action results are implementations of the IHttpActionResult interface that produce an HttpResponseMessage that describes the response that should be sent to the client.

When should you use it?

Action results allow you to take control over the HTTP response that will be returned to the client and, in particular, specify the status code that will be used. Returning void from an action method generates a 204 code, and returning model data (which I describe later in this chapter) generates a 200 code. For all other status codes (or for action methods that need to decide which status code to return dynamically), action results are required.

What do you need to know?

The ApiController class defines a set of convenience methods that create IHttpActionResult implementation objects for most common HTTP status codes. Call these methods to get an object that will generate the response you require and return it as the result from an action method.

Understanding the IHttpActionResult Interface

The IHttpActionResult interface is used to separate an action method from the HttpResponseMessage object that represents its results. This is an example of the command pattern, which you can learn about at http://en.wikipedia.org/wiki/Command_pattern and which makes it easier to isolate and test an action method and the action result separately. Listing 11-5 shows the definition of the IHttpActionResult interface, which is defined in the System.Web.Http namespace.

Listing 11-5. The Definition of the IHttpActionResult Interface

using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace System.Web.Http {
    public interface IHttpActionResult {
        Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken);
    }
}

CANCELLING ASYNCHRONOUS TASKS

You will see that many of the interfaces that describe Web API components are asynchronous and return Task objects that produce other Web API or System.Net.Http types. You usually don’t have to worry about creating Task objects when you are using the default implementations of these interfaces, but they become important when you start to create custom implementations to change the default behaviors.

Most of the important methods receive a CancellationToken argument, which is used by the caller to signal that the operation has been cancelled, allowing your implementation classes to avoid doing work that will just be discarded when it is complete. You can check to see whether your operation has been cancelled by reading the CancellationToken.IsCancellationRequested property, and it is good practice to do just that in your code. I describe Task cancellation in detail in my Pro .NET Parallel Programming in C# book, published by Apress.

The interface defines the ExecuteAsync method, which accepts a CancellationToken object as its argument and returns a Task that produces an HttpResponseMessage object. Table 11-5 shows the properties of the HttpResponseMessage, which gives a sense of the information that is required to generate an HTTP response.

Table 11-5. The Properties Defined by the HttpResponseMessage Class

Name

Description

Content

Gets or sets the content of the response, expressed as an HttpContent object

Headers

Gets the HttpResponseHeaders objects that are used to collect the headers for the response

IsSuccessStatusCode

Returns true if the result of the StatusCode property is between 200 and 299, inclusive

ReasonPhrase

Gets or sets the explanatory phrase associated with the status code, expressed as a string

RequestMessage

Gets or sets the HttpRequestMessage that the HttpResponseMessage is associated with

StatusCode

Gets or sets the status code using the values defined in the HttpStatusCode class

Version

Gets or sets the HTTP version, expressed as a System.Version

Using the ApiController Action Result Methods

The ApiController class, which is the default base for Web API controllers, defines a set of convenience methods that make it easy to create a range of IHttpActionResult implementation objects, which are suitable for most of the common responses that HTTP web services need. Table 11-6 describes the methods available. These methods instantiate classes defined in the System.Web.Http.Results namespace.

Table 11-6. The ApiController Methods That Return Objects That Implement the IHttpActionResult Interface

Name

Description

BadRequest()

Creates a BadRequest object that uses status code 400.

BadRequest(message)

Creates a BadRequestErrorMessageResult, which uses a status code of 400 and contains the specified message in the response body.

BadRequest(modelstate)

Creates an InvalidModelStateResult that uses status code 400 and includes validation information in the response body. See Chapter 18 for details of Web API data validation.

Conflict()

Creates a ConflictResult, which uses status code 409. This status code is used when the request contravenes the internal rules defined by the web service. The standard example is trying to upload an older version of a file than is already stored by the web service, but this is a rarely used result.

Content(status, data)

See the “Bypassing Content Negotiation” section of this chapter for details.

Created(url, data)

See the “Creating Negotiable Action Results” section of this chapter for details.

CreatedAtRoute(name, vals, data)

See the “Creating Negotiable Action Results” section of this chapter for details.

InternalServerError()

Creates an InternalServerError, which uses status code 500.

InternelServerError(exception)

Creates an ExceptionResult, which uses status code 500 and which details of the specified exception in the response body.

NotFound()

Creates a NotFoundResult, which uses status code 404.

Ok()

Creates an OkResult, which uses status code 200.

Ok(data)

See the “Creating Negotiable Action Results” section of this chapter for details.

Redirect(target)

Creates a RedirectResult, which uses status code 302 to redirect the client to the URL, which can be specified as a string or a Uri.

RedirectToRoute(name, props)

Creates a RedirectToRouteResult, which generates a URL from the routing configuration and uses it to send a 302 response to the client. See Chapters 20 and 21 for details of Web API routing.

ResponseMessage(message)

Creates a ResponseMessageResult, which is a wrapper around an existing HttpResponseMessage object. See the “Creating an HttpResponseMessage Object” section.

StatusCode(code)

Creates a StatusCodeResult, which uses the specified status code, expressed as a value from the HttpStatusCode class. See the “Creating an HttpResponseMessage Object” section.

Unauthorized(headers)

Creates a UnauthorizedResult, which uses the 401 status code. See Chapters 23 and 24 for the details of authentication.

The methods that return objects that include information in the response body, such as BadRequest(message) and InternalServerError(exception), rely on the media formatting and content negotiation features to format the response content so that it can be processed by the client. I explain these features in the “Understanding Content Negotiation” section.

The methods shown in Table 11-6 create the IHttpActionResult objects, which you then return as the result from an action method, just as with ActionResult objects in the MVC framework. Listing 11-6 shows the addition of an action method to the Products controller that just returns a result code to the client without doing any work.

Listing 11-6. Adding an Action Method in the ProductsController.cs File

using System.Collections.Generic;
using System.Web.Http;
using ExampleApp.Models;

namespace ExampleApp.Controllers {
    public class ProductsController : ApiController {
        IRepository repo;

        public ProductsController(IRepository repoImpl) {
            repo = repoImpl;
        }

        public IEnumerable<Product> GetAll() {
            return repo.Products;
        }

        public void Delete(int id) {
            repo.DeleteProduct(id);
        }

        [HttpGet]
        [Route("api/products/noop")]
        public IHttpActionResult NoOp() {
            return Ok();
        }
    }
}

The NoOp action method calls the Ok method to create an OkResult object and then returns it as the result of the action. You can test the action method by starting the application and using Postman to send a GET request to /api/products/noop.

Image Tip  I had to apply the Route attributes to prevent the default Web API routes for RESTful web services from directing the request to the GetAll method, which I explain in Chapter 22. The HttpGet attribute enables the action method to receive HTTP GET requests, as described in Chapter 14.

For quick reference, Table 11-7 lists the action result methods ordered by the status codes they produce, which is usually what you need to know in the middle of a project.

Table 11-7. ApiController Action Result Methods by HTTP Status Code

Status Code

Meaning

Method

200

Operation successful

Ok()

Ok(data)

302

Temporary redirection

Redirect(target)

RedirectToRoute(name, props)

400

Bad request

BadRequest()

BadRequest(message)

BadRequest(model)

404

Not found

NotFound()

409

Conflict

Conflict()

500

Internal server error

InternalServerError()

InternalServerError(exception)

Returning Other Status Codes

There are predefined IHttpActionResult implementations for the most widely used HTTP result codes, but Web API makes it easy to return other codes using the IHttpActionResult mechanism, which I’ll demonstrate by returning a result from the Delete method, which currently relies on the controller to detect the void keyword and send the 204 response.

Creating a StatusCodeResult Object

The simplest approach is to use the StatusCode method, which returns a StatusCodeResult object whose ExecuteAsync method yields an HttpResponseMessage with an arbitrary HTTP status code, as shown in Listing 11-7.

Listing 11-7. Using a StatusCodeResult in the ProductsController.cs File

using System.Collections.Generic;
using System.Web.Http;
using ExampleApp.Models;
using System.Net;

namespace ExampleApp.Controllers {
    public class ProductsController : ApiController {
        IRepository repo;

        public ProductsController(IRepository repoImpl) {
            repo = repoImpl;
        }

        public IEnumerable<Product> GetAll() {
            return repo.Products;
        }

        public IHttpActionResult Delete(int id) {
            repo.DeleteProduct(id);
            return StatusCode(HttpStatusCode.NoContent);
        }

        [HttpGet]
        [Route("api/products/noop")]
        public IHttpActionResult NoOp() {
            return Ok();
        }
    }
}

A lot of different HTTP status codes are available, and this technique is useful if you find yourself needing one of them that isn’t covered by the other controller convenience methods. The set of status codes that you can use is defined by the System.Net.HttpStatusCode class, which has properties for each code.

To be clear, there is little need to explicitly return code 204 (No Data) in a real application because using the void keyword is more elegant and natural, although it can be a useful technique when performing data validation, which I describe in Chapter 18.

Creating an HttpResponseMessage Object

You can use the ResponseMessage method as an IHttpActionResult wrapper around an HttpResponseMessage that you have already created or obtained. This isn’t something you will need to do for most web services, but it can be useful when modifying the Web API request dispatch process, which I describe in Part 3. Listing 11-8 shows the changes I made in the Delete method in the Products controller to create an HttpResponseMessage object and pass it to the ResponseMessage method.

Listing 11-8. Using the ResponseMessage Method in the ProductsController.cs File

using System.Collections.Generic;
using System.Web.Http;
using ExampleApp.Models;
using System.Net;
using System.Net.Http;

namespace ExampleApp.Controllers {
    public class ProductsController : ApiController {
        IRepository repo;

        public ProductsController(IRepository repoImpl) {
            repo = repoImpl;
        }

        public IEnumerable<Product> GetAll() {
            return repo.Products;
        }

        public IHttpActionResult Delete(int id) {
            repo.DeleteProduct(id);
            return ResponseMessage(new HttpResponseMessage(HttpStatusCode.NoContent));
        }

        [HttpGet]
        [Route("api/products/noop")]
        public IHttpActionResult NoOp() {
            return Ok();
        }
    }
}

The HttpResponseMessage class has a constructor that takes a value from the HttpStatusCode class to specify the status code. I didn’t need to set the other properties of the HttpResponseMethod because I was not trying to send any content back to the client. This technique produces the same effect as using the StatusCode method or defining the action method with the void keyword.

Creating a Custom Action Result

If you frequently need to return a result for which there is no controller convenience method, then you can define a custom implementation of the IHttpActionResult interface that yields the response you need. I created a NoContentResult.cs class file in the Infrastructure folder and used it to define the action result shown in Listing 11-9.

Listing 11-9. The Contents of the NoContentResult.cs File

using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;

namespace ExampleApp.Infrastructure {
    public class NoContentResult : IHttpActionResult {

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken
                cancellationToken) {
            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NoContent));
        }
    }
}

The convenience methods defined by the ApiController class are protected, which means they can’t be built on in custom action results. Instead, my NoContentResult class creates a new HttpResponseMessage object, using the constructor argument to specify the 204 status code.

Image Tip  Notice that I used the static Task.FromResult method to create a Task that yields the HttpResponseMessage object as the result from the ExecuteAsync method. Almost all Web API operations are asynchronous, but the overhead of creating a new Task and performing work asynchronously isn’t always worthwhile when the work you have to do is simple. In these cases, the Task.FromResult method allows you to create a Task wrapper that yields the object you provide as the argument.

I can now use my custom implementation of the IHttpActionResult interface as the result in an action method, as shown in Listing 11-10.

Listing 11-10. Using a Custom Action Method in the ProductsController.cs File

using System.Collections.Generic;
using System.Web.Http;
using ExampleApp.Models;
using System.Net;
using System.Net.Http;
using ExampleApp.Infrastructure;

namespace ExampleApp.Controllers {
    public class ProductsController : ApiController {
        IRepository repo;

        public ProductsController(IRepository repoImpl) {
            repo = repoImpl;
        }

        public IEnumerable<Product> GetAll() {
            return repo.Products;
        }

        public IHttpActionResult Delete(int id) {
            repo.DeleteProduct(id);
            return new NoContentResult();
        }

        [HttpGet]
        [Route("api/products/noop")]
        public IHttpActionResult NoOp() {
            return Ok();
        }
    }
}

Returning Model Data

One of the headline features of Web API is the ability to return model data objects and have them serialized and sent the client automatically. In this section, I demonstrate this feature, explain one of the two components responsible for the process, and show you how to customize it. (The other component, the media formatter, is described in Chapters 12 and 13.) Table 11-8 puts returning model data into context.

Table 11-8. Putting Returning Model Data in Context

Question

Answer

What is it?

To make creating web services simple, Web API allows you to return one or more model objects from action methods, which are then serialized into a format that can be processed by the client.

When should you use it?

You should use this feature whenever you need to return data to a client with a 200 status code. See the “Returning Negotiable Action Results” section if you need to send data with another status code.

What do you need to know?

The data format used to serialize the data is selected based on a process called content negotiation, which relies on the client sending an HTTP Accept header. This means different clients can receive the same data in different formats, so make sure you test thoroughly or limit the formats that your application supports (which I describe in the  “Implementing a Custom Negotiator” section in this chapter and in Chapter 13).

Understanding the Default Behavior

Understanding the default behavior means making a couple of requests to the Web API web service and studying the results. First start the application and use Postman to send a GET request to the /api/products URL. You will see that the following data is returned:

[{"ProductID":1,"Name":"Kayak","Price":275.0},
 {"ProductID":2,"Name":"Lifejacket","Price":48.95},
 {"ProductID":3,"Name":"Soccer Ball","Price":19.50},
 {"ProductID":4,"Name":"Thinking Cap","Price":16.0}]

This request targets the GetAll action method defined by the Products controller, which is defined like this:

...
public IEnumerable<Product> GetAll() {
    return repo.Products;
}
...

The action method returns an enumeration of Product objects, which Web API has serialized as a JSON array. That’s useful, but there is something else that is happening behind the scenes that requires a second request to understand. If you request the /api/products URL using Google Chrome, you will see the following data displayed in the browser tab:

<ArrayOfProduct xmlns:i="http://www.w3.org/2001/XMLSchema-
       instance"xmlns="http://schemas.datacontract.org/2004/07/ExampleApp.Models">
    <Product>
        <Name>Kayak</Name><Price>275</Price><ProductID>1</ProductID>
    </Product>
    <Product>
        <Name>Lifejacket</Name><Price>48.95</Price><ProductID>2</ProductID>
    </Product>
    <Product>
        <Name>Soccer Ball</Name><Price>19.50</Price><ProductID>3</ProductID>
    </Product>
    <Product>
        <Name>Thinking Cap</Name><Price>16</Price><ProductID>4</ProductID>
    </Product>
</ArrayOfProduct>

This time, the enumeration of Product objects has produced XML data, which happens because Google Chrome sent headers as part of the HTTP request that expressed a preference for XML.

There are two important Web API features at work here. The first is content negotiation, where Web API inspects the request and uses the information it contains to figure out what data formats the client can process. The second feature is media formatting, where Web API serializes the data into the format that has been identified—JSON and XML in these examples—so that it can be sent to the client. I describe basic content negotiation in this chapter and media formatters and advanced negotiation in Chapters 12 and 13.

Understanding the Content Negotiation Process

Content negotiation is the process by which an appropriate format is selected for serializing the data format. The word negotiation is misleading because it conjures up a back-and-forth exchange between the client and the web service, rather like haggling in a back room. The reality is much simpler: the client includes an Accept header in the HTTP request that describes the data formats that it can handle, expressed as MIME types with information about the order of preference. The web service works its way down the preference list until it finds a format that it can produce and then uses that format to serialize the data. (There are other headers that clients use to express preferences—Accept-Charset, Accept-Encoding and Accept-Language—but I focus on the Accept header in this chapter. See Chapter 12 for details of how Web API supports the Accept-Charset header and how you can use any header for negotiation.)

Here is the Accept header that Google Chrome sent in the previous section, which I obtained using the Network panel of the F12 tools (I added some spaces to make it easier to read):

Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8

Each content type has a q value, which is a measure of preference, and greater q values indicate more preferable formats. A value of 1.0—the maximum value—is implied when a q value isn’t expressed. This header is interpreted as follows:

  • Chrome prefers the text/html (HTML), application/xhtml+xml (XHTML), and image/webp formats above all others.
  • If HTML, XHTML, and image/webp are not available, then XML is the next most preferred format.
  • If none of the preferred formats is available, then Chrome will accept any format (expressed as */*).

Web API has built-in support for JSON, BSON, and XML. (JSON and XML are widely used and understood. BSON is Binary JSON, which isn’t supported by browser-based clients.)

The content negotiation process compares the Chrome preferences with the Web API formats and determines that Chrome would prefer to receive the model data formatted as XML. If there is no Accept header in the request, then the web service is allowed to assume that the client will accept any data format. Postman sets the Accept header to */* by default, so it receives the default Web API data format, which is JSON. The Accept header for jQuery Ajax requests is controlled through the accept setting (as described in Chapter 3) and is also set to */* by default. This is why clicking the Refresh button rendered by the Index.cshtml view obtains JSON data, even though requesting the same URL directly through Chrome produces XML data.

Image Tip  The image/webp MIME type refers to an image format called WebP that Google has developed. By giving the format a preference of 1.0, Chrome is expressing a preference to receive images in this format over all others. WebP doesn’t have any bearing on HTTP web services, but you can learn more about it here: http://en.wikipedia.org/wiki/WebP.

Implementing a Custom Content Negotiator

The content negotiator is the class responsible for examining requests and identifying the format that best suits the client. The content negotiator is not responsible for formatting the data; that’s the job of the media formatter, which I describe in Chapters 12 and 13. Content negotiators implement the IContentNegotiator interface, which is defined in the System.Net.Http.Formatting namespace. Listing 11-11 shows the definition of the interface.

Listing 11-11. The IContentNegotiator Interface

using System.Collections.Generic;
using System.Net.Http.Headers;

namespace System.Net.Http.Formatting {

    public interface IContentNegotiator {
        ContentNegotiationResult Negotiate(Type type, HttpRequestMessage request,
            IEnumerable<MediaTypeFormatter> formatters);
    }
}

The Negotiate method is called to examine a request and is passed the Type of the data to be serialized, the HttpRequestMessage that represents the HTTP request from the client, and an enumeration of the available media formatters, which are responsible for serializing content and are derived from the MediaTypeFormatter class (which I describe in Chapter 12).

The result from the Negotiate method is an instance of the ContentNegotiationResult class, which defines the properties shown in Table 11-9.

Table 11-9. The Properties Defined by the ContentNegotiationResult

Name

Description

Formatter

Returns the instance of the MediaTypeFormatter that will be used to serialize the data. I describe media formatters in Chapter 12.

MediaType

Returns an instance of the MediaTypeHeaderValue class, which details the headers that will be added to the response to describe the selected format.

Image Tip  Returning null from the Negotiate method in a custom negotiator returns a 406 (Unacceptable) response to the client, indicating that there is no overlap between the data formats that the web service can produce and that the client can process. However, the default content negotiator class doesn’t return a 406 response by default even where there is no suitable content type available; see Chapter 13 for details.

The MediaType property returns an instance of the MediaTypeHeaderValue class, which contains the details required to set the Content-Type header for the HTTP response. The MediaTypeHeaderValue class defines the members shown in Table 11-10.

Table 11-10. The Members Defined by the MediaTypeHeaderValue

Name

Description

CharSet

Gets or sets the character set component of the Content-Type header.

MediaType

Gets or sets the MIME type that will be used in the Content-Type header.

Parameters

Returns a collection that can be used to add properties to the Content-Type header.

Parse(header)

A static method that parses a header string and returns a MediaTypeHeaderValue object. This method is used by the model binding feature, which I described in Chapters 1417.

TryParse(header, output)

A static method that attempts to parse the header string and populates the output argument, which is a MediaTypeHeaderValue parameter decorated with the out keyword. This method is used by the model binding feature, which I described in Chapters 1417.

Web API includes a default content negotiator class, called DefaultContentNegotiator, that inspects the Accept header and selects a media formatter based on the preferences expressed by the client. Content negotiation can take into account any aspect of the request, and I am going to create a custom negotiator that builds on the default behavior but ensures that requests from Chrome receive JSON responses rather than XML. I added a class file called CustomNegotiator.cs to the Infrastructure folder and used it to define the class shown in Listing 11-12.

Listing 11-12. The Contents of the CustomNegotiator.cs File

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;

namespace ExampleApp.Infrastructure {
    public class CustomNegotiator : DefaultContentNegotiator {

        public override ContentNegotiationResult Negotiate(Type type,
                HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters) {

            if (request.Headers.UserAgent.Where(x => x.Product != null
                && x.Product.Name.ToLower().Equals("chrome")).Count() > 0) {

                return new ContentNegotiationResult(new JsonMediaTypeFormatter(),
                    new MediaTypeHeaderValue("application/json")
                );

            } else {
                return base.Negotiate(type, request, formatters);
            }
        }
    }
}

Rather than implement my custom negotiator directly from the IContentNegotiator interface, I have derived my CustomNegotiator from the DefaultContentNegotiator class so that I can benefit from the built-in support for dealing with the Accept header for those requests that don’t come from Chrome. I have overridden the Negotiate method to inspect the User-Agent header and look for requests that have been made from Chrome; see the “Working with Request Headers” sidebar for details.

When I identify a Chrome request, I return a new ContentNegotiationResult that specifies one of the built-in media formatters, JsonMediaTypeFormatter, and the application/json MIME type. I explain how media formatters work in Chapter 12 (and hard-coding a dependency on a specific class is far from ideal), but it is enough to demonstrate the role that the content negotiator plays in Web API.

WORKING WITH REQUEST HEADERS

The headers sent by a client in an HTTP request are available through the HttpRequestMessage.Headers property, which returns an instance of the System.Net.Http.Headers.HttpRequestHeaders class. The HttpRequestHeaders class defines properties for each of the headers defined by the HTTP standard, such as Accept and UserAgent, as well as Contains and GetValues methods that let you check to see whether a header is present and get the value of an arbitrary header.

The header values are processed to make them easier to work with. In the case of the User-Agent header, for example, the HttpRequestHeaders.UserAgent property returns an HttpHeaderValueCollection<ProductInfoValueHeader>, which is essentially an enumeration of ProductInfoValueHeader objects, each of which represents part of the User-Agent header. The ProductInfoValueHeader class defines Comment and Product properties. The Comment property returns a string, and the Product property returns a ProductValueHeader object, which in turn defines Name and Version properties.  

It may seem confusing, but the effect is that headers are parsed into their constituent parts, which makes them easy to work with. As an example, Google Chrome sends a User-Agent string like this:

User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36
    (KHTML, like Gecko) Chrome/35.0.1901.0 Safari/537.36

The header string is broken into its individual components, which are separated by spaces, and each component is represented by the ProductInfoValueHeader. The components that contains the / character are represented by a ProductValueHeader object. For example, the Mozilla/5.0 component is represented by a ProductValueHeader whose Name is Mozilla and Version is 5.0. The components in parentheses are available through the Comment property of the ProductInfoValueHeader that represents them.

This may seem like a mass of confusing types, but it comes together when you use LINQ to process the headers. In my custom content negotiator, I am looking for requests that come from Chrome, which means that I need to locate a ProductInfoValueHeader object whose Product property returns a ProductValueHeader whose Name property is set to Chrome, which I can do like this:

...
request.Headers.UserAgent.Where(x => x.Product != null
    && x.Product.Name.ToLower().Equals("chrome")).Count() > 0
...

The advantage of this approach is it reduces the scope for errors because there is no chance of my matching on a User-Agent header that has Chrome as part of a comment or part of another browser name. By contrast, here is how I matched the User-Agent header in one of the examples for my Pro ASP.NET MVC 5 book in order to demonstrate the URL routing feature:

...
httpContext.Request.UserAgent.Contains(requiredUserAgent)
...

Far fewer classes are involved, but there’s a higher chance of misidentifying the client. The way that headers are processed by the System.Net.Http classes may seem awkward at first but is more flexible and useful than parsing them manually, especially when combined with LINQ.

Configuring the Content Negotiator

I need to tell Web API that I want to use my custom content negotiator. Listing 11-13 shows the changes I made to the WebApiConfig.cs file to register my CustomNegotiator class.

Listing 11-13. Registering a Custom Content Negotiator in the WebApiConfig.cs File

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using ExampleApp.Infrastructure;
using System.Net.Http.Formatting;

namespace ExampleApp {
    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {

            config.DependencyResolver = new NinjectResolver();

            config.Services.Replace(typeof(IContentNegotiator), new CustomNegotiator());

            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

I have called the HttpConfig.Services.Replace method to replace the default implementation of the IContentNegotiator with an instance of my CustomNegotiator class. Web API offers extensibility in different ways, and this is the approach you should use if you have not set up dependency injection in your application. If you have set up DI, as I have for the example project, then you can set up the mapping in the DI container because Web API calls the IDependencyResolver.GetService method before creating the default service classes. Listing 11-14 shows the mapping I added to the NinjectResolver class.

Image Note  You need to register the custom negotiator using only one of these techniques, so I commented out the statement in Listing 11-13.

Listing 11-14. Registering a Custom Content Negotiator in the NinjectResolver.cs File

using System;
using System.Collections.Generic;
using System.Web.Http.Dependencies;
using ExampleApp.Models;
using Ninject;
using Ninject.Extensions.ChildKernel;
using Ninject.Web.Common;
using System.Net.Http.Formatting;

namespace ExampleApp.Infrastructure {

    public class NinjectResolver : System.Web.Http.Dependencies.IDependencyResolver,
            System.Web.Mvc.IDependencyResolver {
        private IKernel kernel;

        public NinjectResolver() : this(new StandardKernel()) { }

        public NinjectResolver(IKernel ninjectKernel) {
            kernel = ninjectKernel;
            AddBindings(kernel);
        }

        public IDependencyScope BeginScope() {
            return this;
        }

        public object GetService(Type serviceType) {
            return kernel.TryGet(serviceType);
        }

        public IEnumerable<object> GetServices(Type serviceType) {
            return kernel.GetAll(serviceType);
        }

        public void Dispose() {
            // do nothing
        }

        private void AddBindings(IKernel kernel) {
            kernel.Bind<IRepository>().To<Repository>().InSingletonScope();
            kernel.Bind<IContentNegotiator>().To<CustomNegotiator>();
        }
    }
}

Notice that I have not set a scope on the mapping between the IContentNegotiator interface and the CustomNegotiator class, as described in Chapter 10. Web API will make only one request to the resolver for each of its service classes, which means I don’t have to worry about dealing with the life cycle of multiple instances.

Testing the Content Negotiator

To test the custom content negotiator, start the application and request the /api/products URL. JSON data will be displayed instead of the XML response you received earlier, as shown in Figure 11-4.

9781484200865_Fig11-04.jpg

Figure 11-4. The data sent by a custom content negotiator

Image Caution  Care is required with custom content negotiators that don’t use the standard HTTP negotiation headers. They result in web services that may ignore the preferences of the client and send a data format that can’t be processed. As a general rule, most clients will process JSON these days, but you can’t rely on this always being true, especially if you are supporting legacy clients. Test content negotiators thoroughly and stop to check that ignoring the Accept header is the best solution to the problem you are trying to solve.

Bypassing Content Negotiation

The ApiController class defines a group of methods that allow action methods to override the regular content negotiation process and specify the data format that should be used, as described in Table 11-11.

Table 11-11. The ApiController Methods That Bypass Content Negotiation

Name

Description

Json(data)

Returns a JsonResult, which serializes the data as JSON, irrespective of the preferences expressed by the client.

Content(status, data, formatter)

Returns a FormattedContentResult, which bypasses the content negotiation process and uses the specified formatter to serialize the data. The specified status code is used in the response. The formatter is responsible for setting the value of the Content-Type header.

Content(status, data, formatter, mimeType)

Like the previous method but uses the specified MIME type, expressed as a MediaTypeHeaderValue object, for the Content-Type header in the response.

Content(status, data, formatter, mimeString)

Like the previous method but uses the specified MIME type, expressed as a string, for the Content-Type header in the response.

Implementing a custom content negotiator allows you to select a data format based on the characteristic of the request from the client. The methods shown in Table 11-11 allow you to select a data format based on all the context information that is available to an action method. This includes the request, of course, but also the data that is going to be returned in the response.

Bypassing the content negotiation process is not a decision to make lightly because it exists to ensure that clients get content they can process, based on the preferences they express. If you need to force the data format your application uses, then you can change the configuration of the media formatters, which I describe in Chapter 12, or implement a custom content negotiator that gives precedence to a particular formatter (as I demonstrated in the “Implementing a Custom Content Negotiator” section earlier in this chapter). Not only do these approaches better respect the separation of concerns between Web API components, but they also consolidate the decision-making logic in one place, making it easier to change the formats that are used and to perform unit testing.

Returning Negotiable Action Results

Being able to return model objects and let Web API figure out what to do with them is, without a doubt, a helpful and elegant feature, but it does assume that you will return the data to the client with a 200 (OK) status code in the response. A negotiable action result is one that allows you to produce a different HTTP status code but still take advantage of the content negotiation and data formatting features. Table 11-12 puts negotiable action results in context.

Table 11-12. Putting Negotiable Action Results in Context

Question

Answer

What is it?

A negotiable action result allows you more control over the HttpResponseMessage that is sent to the client while still benefitting from the content negotiation and data formatting features.

When should you use it?

Use a negotiable action result whenever you need to send data to the client with a status code other than 200.

What do you need to know?

Most web clients will expect to receive data with a 200 status code, and breaking this convention—even to increase adherence to the HTTP standard as I describe next—creates the risk of making the client misbehave, especially when the client predates the web service or is written by programmers who have a different interpretation of REST.

Creating Negotiable Action Results

The ApiController class defines a set of methods that return implementations of the IHttpActionResult interface that let you take more control over the response, providing the benefits of the negotiation and formatting processes while allowing you to select the status code that will be used. Table 11-13 describes these methods.

Table 11-13. The ApiController Methods That Return Negotiable Action Results

Name

Description

Ok(data)

This method returns an OkNegotiatedContentResult object, which sets the result status code to 200 and is equivalent to returning the model objects as the result of the action method.

Created(url, data)

This method returns a CreatedNegotiatedContentResult object, which sets the response status code to 201, indicating that a new resource has been created as a consequence of the request. The url argument is used to specify the URL that can be used to request the new object.

CreatedAtRoute(name, values, data)

This method returns a CreatedAtRouteNegotiatedContentResult object, which uses a 201 status code and generates the URL that refers to the new object using the named route and route values. See Chapters 20 and 21 for details of Web API routing.

Content(staus, data)

This method creates a NegotiatedContentResult object, which allows an arbitrary status code to be set for the HTTP response.

You will rarely need to use these methods, and so far, the only time I have found them useful is when replacing a legacy web service that made unusual—and entirely nonstandard—use of HTTP status codes to signal service status to its equally nonstandard clients. That said, make sure you read the “Returning 200 or 201 Results from POST Requests” sidebar to learn about why the Created and CreatedAtRoute methods are sometimes used. Listing 11-15 shows the use of the Ok method applied to the GetAll action to re-create the same effect achieved by returning model objects as the method result.

Listing 11-15. Using the Ok Method in the ProductsController.cs File

using System.Collections.Generic;
using System.Web.Http;
using ExampleApp.Models;
using System.Net;
using System.Net.Http;
using ExampleApp.Infrastructure;

namespace ExampleApp.Controllers {
    public class ProductsController : ApiController {
        IRepository repo;

        public ProductsController(IRepository repoImpl) {
            repo = repoImpl;
        }

        public IHttpActionResult GetAll() {
            return Ok(repo.Products);
        }

        public IHttpActionResult Delete(int id) {
            repo.DeleteProduct(id);
            return new NoContentResult();
        }

        [HttpGet]
        [Route("api/products/noop")]
        public IHttpActionResult NoOp() {
            return Ok();
        }
    }
}

RETURNING 200 OR 201 RESULTS FROM POST REQUESTS

The Created and CreatedAtRoute methods are interesting because they touch on a design decision about how a RESTful web service responds to POST requests. Most web services will return a 200 status code and include the new data object in the response to the client. The new object will, at least, contain the unique key that can be used to refer to the object and a set of HATEOAS links if that pattern is being followed.

This is the most common approach, but it doesn’t follow the HTTP specification that states that the web service should return a 201 response that contains a Location header with a URL that can be requested to get the newly created resource. The client can then request this URL to retrieve the new data item.

The reason that most web services return a 200 response that includes the newly created object is because most clients will display newly created data items to the user, and including the data in the response preempts the obvious next task for the client, avoiding an additional request.

Adhering to the HTTP specification is generally a good thing, but returning a 201 response that requires another request to be made immediately is needless pattern purity for most web services, especially since using a 200 response has become the accepted convention. Unless you have a compelling need to the contrary, avoid the complexity (and the additional bandwidth) required for the 201 response and use the 200 status code to response to POST messages with the data that the client is likely to need.

Summary

In this chapter, I showed you the different kinds of results that an action method can return and how these affect the responses sent to the client. I started by demonstrating how void methods produce responses with the 204 status code and how action methods can return IHttpActionResult objects to further control the response. One of the headline features of Web API is the automatic serialization of model data objects, and I explained the first part of this process: content negotiation. I explained how the client sends the Accept header to detail the data formats that it is willing to receive and how the IContentNegotiator is used to select a media formatter to serialize the data based on those preferences. In the next chapter, I explain how media formatters work and show you how to create a custom one.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.2.240