Chapter 8. Improving the API

No pain, no gain. That’s what makes you a champion.

In the previous chapter we discussed the initial implementation of the issue tracker system. The idea was to have a fully functional implementation that we could use to discuss the design of the API and the media types to support it. As part of this chapter, we will try to improve that existing implementation by adding new features like caching, conflict detection, and security. All the requirements for these new features will be described in terms of BDD as we did with the initial implementation. As we add those new features, we will dive into the details of the implementation, showing real code, and also some of the introductory theory behind them. Later chapters will complement that theory in more detail.

Acceptance Criteria for the New Features

Following are the tests for our API, which cover the new requirements for the tracker system:

Feature: Output Caching
  Scenario: Retrieving existing issues
    Given existing issues
    When all issues are retrieved
    Then a CacheControl header is returned
    Then a '200 OK' status is returned
    Then all issues are returned

  Scenario: Retrieving an existing issue
    Given an existing issue
    When it is retrieved
    Then a LastModified header is returned
    Then a CacheControl header is returned
    Then a '200 OK' status is returned
    Then it is returned

Feature: Cache revalidation

  Scenario: Retrieving an existing issue that has not changed
    Given an existing issue
    When it is retrieved with an IfModifiedSince header
    Then a CacheControl header is returned
    Then a '304 NOT MODIFIED' status is returned
    Then it is returned

  Scenario: Retrieving an existing issue that has changed
    Given an existing issue
    When it is retrieved with an IfModifiedSince header
    Then a LastModified header is returned
    Then a CacheControl header is returned
    Then a '200 OK' status is returned
    Then it is returned

Feature: Conflict detection

  Scenario: Updating an issue with no conflicts
    Given an existing issue
    When a PATCH request is made with an IfModifiedSince header
    Then a '200 OK' is returned
    Then the issue should be updated

  Scenario: Updating an issue with conflicts
    Given an existing issue
    When a PATCH request is made with an IfModifiedSince header
    Then a '409 CONFLICT' is returned
    Then the issue is not updated

Feature: Change Auditing

  Scenario: Creating a new issue
    Given a new issue
    When a POST request is made with an Authorization header containing the user
        identifier
    Then a '201 Created' status is returned
    Then the issue should be added with auditing information
    Then the response location header will be set to the resource location

  Scenario: Updating an issue
    Given an existing issue
    When a PATCH request is made with an Authorization header containing the user
        identifier
    Then a '200 OK' is returned
    Then the issue should be updated with auditing information

Feature: Tracing

  Scenario: Creating, Updating, Deleting, or Retrieving an issue
    Given an existing or new issue
    When a request is made
    When the diagnostics tracing is enabled
    Then the diagnostics tracing information is generated

Implementing the Output Caching Support

Caching is one of the fundamental aspects that makes it possible to scale on the Internet, as it provides the following benefits when it is implemented correctly:

  • Reduces load on the origin servers.
  • Decreases network latencies. Clients can get responses much faster.
  • Saves network bandwidth. Fewer network hops are required, as the content might be found in some caching intermediary before the request reaches the origin server.

Implementing caching correctly on a Web API mainly involves two steps:

  1. Set the right headers to instruct intermediaries and clients (e.g., proxies, reverse proxies, local caches, browsers, etc.) to cache the responses.
  2. Implement conditional GETs so the intermediaries can revalidate the cached copies of the data after they become stale.

The first step requires the use of either the Expires or Cache-Control header. The Expires HTTP header is useful for expressing absolute expiration times. It only tells caches how long the associated representation is fresh for. Most implementations use this header to express the last time that the client retrieved the representation or the last time the document changed on your server. The value for this header has to be expressed in GTM, not a local time—for example, Expires: Mon, 1 Aug 2013 10:30:50 GMT. On the other hand, the Cache-Control header provides more granular control for expressing sliding expiration dates and also who is allowed to cache the data. The following list describes well-known values for the Cache-Control header:

no-store
Indicates that caches should not keep a copy of the data under any circumstance.
private
Indicates that the data is intended for a single user, so it should be cached on private caches like a browser but not on shared caches like proxies.
public
Indicates that the data can be cached anywhere.
no-cache
Forces caches to revalidate the cached copies after they become stale.
max-age
Indicates a delta in seconds representing the maximum amount of time that a cached copy will be considered fresh (e.g., max-age[300] means the cached copy will expire 300 seconds after the request was made).
s-maxage
Is equivalent to max-age but valid for shared caches only.

Adding the Tests for Output Caching

The first thing we need to do is add a new file, OutputCaching, for all our tests related to output caching. Our first test involves adding output caching support in the operation for returning all the issues:

Scenario: Retrieving existing issues
    Given existing issues
    When all issues are retrieved
    Then a CacheControl header is returned
    Then a '200 OK' status is returned
    Then all issues are returned

We translate this scenario to a unit test using BDD, as shown in Example 8-1.

Example 8-1. Retrieving all issues with caching headers
public class OutputCaching : IssuesFeature
{
  private Uri _uriIssues = new Uri("http://localhost/issue");

  [Scenario]
  public void RetrievingAllIssues()
  {
    IssuesState issuesState = null;

    "Given existing issues".
      f(() =>
      {
        MockIssueStore.Setup(i => i.FindAsync())
          .Returns(Task.FromResult(FakeIssues))
      });
    "When all issues are retrieved".
      f(() =>
      {
        Request.RequestUri = _uriIssues;
        Response = Client.SendAsync(Request).Result;
        issuesState = Response.Content
          .ReadAsAsync<IssuesState>()
          .Result;
      });
    "Then a CacheControl header is returned".
      f(() =>
      {
        Response.Headers.CacheControl.Public
          .ShouldBeTrue(); // <1>
        Response.Headers.CacheControl.MaxAge
          .ShouldEqual(TimeSpan.FromMinutes(5)); // <2>
      });
    "Then a '200 OK' status is returned".
      f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.OK));
    "Then they are returned".
      f(() =>
      {
        issuesState.Issues
          .FirstOrDefault(i => i.Id == "1")
          .ShouldNotBeNull();
        issuesState.Issues
          .FirstOrDefault(i => i.Id == "2")
          .ShouldNotBeNull();
      });
  }
}

The unit test is self-explanatory; the part that matters is in lines <1> and <2>, where the assertions for the CacheControl and MaxAge headers are made. To pass this test, the response message returned in the Get method of the IssuesController class is modified to include those two headers, as shown in Example 8-2.

Example 8-2. The new version of the Get method
public async Task<HttpResponseMessage> Get()
{
  var result = await _store.FindAsync();
  var issuesState = new IssuesState();
  issuesState.Issues = result.Select(i => _stateFactory.Create(i));

  var response = Request.CreateResponse(HttpStatusCode.OK, issuesState);

  response.Headers.CacheControl = new CacheControlHeaderValue();
  response.Headers.CacheControl.Public = true; // <1>
  response.Headers.CacheControl.MaxAge = TimeSpan.FromMinutes(5); // <2>

  return response;
}

The CacheControl header is set to Public <1>, so it can be cached anywhere, and the MaxAge header is set to a relative expiration of 5 minutes <2>.

The next scenario, shown in Example 8-3, involves adding output caching to the operation for retrieving a single issue:

    Scenario: Retrieving an existing issue
    Given an existing issue
    When it is retrieved
    Then a LastModified header is returned
    Then a CacheControl header is returned
    Then a '200 OK' status is returned
    Then it is returned
Example 8-3. Retrieving a single issue with caching headers
public class OutputCaching : IssuesFeature
{
  private Uri _uriIssue1 = new Uri("http://localhost/issue/1");

  [Scenario]
  public void RetrievingAnIssue()
  {
    IssueState issue = null;

    var fakeIssue = FakeIssues.FirstOrDefault();
    "Given an existing issue".
      f(() => MockIssueStore
        .Setup(i => i.FindAsync("1"))
        .Returns(Task.FromResult(fakeIssue)));
    "When it is retrieved".
      f(() =>
      {
        Request.RequestUri = _uriIssue1;
        Response = Client.SendAsync(Request).Result;
        issue = Response.Content.ReadAsAsync<IssueState>().Result;
      });
    "Then a LastModified header is returned".
      f(() =>
      {
        Response.Content.Headers.LastModified
          .ShouldEqual(new DateTimeOffset(new DateTime(2013, 9, 4))); // <1>
      });
    "Then a CacheControl header is returned".
      f(() =>
      {
        Response.Headers.CacheControl.Public
          .ShouldBeTrue(); // <2>
        Response.Headers.CacheControl.MaxAge
          .ShouldEqual(TimeSpan.FromMinutes(5)); // <3>
      });
    "Then a '200 OK' status is returned".
      f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.OK));
    "Then it is returned".
      f(() => issue.ShouldNotBeNull());
   }
}

The test in Example 8-3 is slightly different from the one we wrote for retrieving all the issues. In addition to retrieving a single issue, it checks for the LastModified header in the response <1>. This header will be used later in other scenarios for performing cache revalidation. Also, the expected values for the CacheControl <2> and MaxAge <3> headers are Public and 5 minutes, respectively.

Implementing Cache Revalidation

Once a cached copy of a resource representation becomes stale, a cache intermediary can revalidate that copy by sending a conditional GET to the origin server. A conditional GET involves the use of two response headers, If-None-Match and If-Modified-Since. If-None-Match corresponds to an Etag header, which represents an opaque value that only the server knows how to re-create. This Etag could represent anything, but it is typically a hash representing the resource version, which we can generate by hashing the whole representation content or just some parts of it like a timestamp. On the other hand, If-Modified-Since corresponds to the Last-Modified header, which represents a datetime that the server can use to determine whether the resource has changed since the last time it was served.

Example 8-4 illustrates a pair of request/response messages exchanged by the client/server with the corresponding caching headers.

Example 8-4. Pair of request and response messages with the caching headers
Response –>

Connection close
Date Thu, 02 Oct 2013 14:46:57 GMT
Expires Sat, 01 Nov 2013 14:46:57 GMT
Last-Modified Mon, 29 Sep 2013 15:40:27 GMT
Etag a9331828c518ac6d97f93b3cfdbcc9bc
Content-Type application/json

Request ->

Host localhost
Accept */*
If-Modified-Since Mon, 29 Sep 2013 15:40:27 GMT
If-None-Match a9331828c518ac6d97f93b3cfdbcc9bc

By using either of these two headers, a caching intermediary can determine whether the resource representation has changed in the origin server. If the resource has not changed according to the values in those headers (If-Modified-Since for Last-Modified and If-None-Match for Etag), the service can return an HTTP status code of 304 Not Modified, which instructs the intermediary to keep the cached version and refresh the expiration times. Example 8-4 shows both headers, but in practice, the intermediary uses only one of them.

Implementing Conditional GETs for Cache Revalidation

Our first test, shown in Example 8-5, will revalidate the cached representation of an issue that has not changed on the server. You will find these tests in the class CacheValidation.

Scenario: Retrieving an existing issue that has not changed
    Given an existing issue
    When it is retrieved with an IfModifiedSince header
    Then a CacheControl header is returned
    Then a '304 Not Modified' status is returned
    Then it is not returned
Example 8-5. Unit test for validating a cached copy that has not changed
private Uri _uriIssue1 = new Uri("http://localhost/issue/1");

[Scenario]
public void RetrievingNonModifiedIssue()
{
  IssueState issue = null;

  var fakeIssue = FakeIssues.FirstOrDefault();
  "Given an existing issue".
    f(() => MockIssueStore.Setup(i => i.FindAsync("1"))
        .Returns(Task.FromResult(fakeIssue)));
  "When it is retrieved with an IfModifiedSince header".
    f(() =>
    {
      Request.RequestUri = _uriIssue1;
      Request.Headers.IfModifiedSince = fakeIssue.LastModified; // <1>
      Response = Client.SendAsync(Request).Result;
    });
  "Then a CacheControl header is returned".
    f(() =>
    {
      Response.Headers.CacheControl.Public.ShouldBeTrue();
      Response.Headers.CacheControl.MaxAge.ShouldEqual(TimeSpan.FromMinutes(5));
    });
  "Then a '304 NOT MODIFIED' status is returned".
    f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.NotModified)); // <2>
  "Then it is not returned".
    f(() => Assert.Null(issue));
}

Example 8-5 shows the unit test that we created for validating the scenario in which the resource representation has not changed on the origin server since it was cached. This test emulates the behavior of a caching intermediary that sends a conditional GET to the server using the IfModifiedSince header that was previously stored <1>. As part of the expectations of the test, the status code in the response should be 304 NOT MODIFIED <2>.

The Get method in the IssuesController class has to be modified to include all the conditional GET logic (see Example 8-6). If a request message with an IfModifiedSince header is received, that date must be compared with the LastModified field in the requested issue to check whether the issue has changed since the last time it was served to the caching intermediary.

Example 8-6. The new version of the Get method that supports conditional GETs
public async Task<HttpResponseMessage> Get(string id)
{
  var result = await _store.FindAsync(id);
  if (result == null)
    return Request.CreateResponse(HttpStatusCode.NotFound);

  HttpResponseMessage response = null;

  if( Request.Headers.IfModifiedSince.HasValue &&
      Request.Headers.IfModifiedSince == result.LastModified) // <1>
  {
    response = Request
      .CreateResponse(HttpStatusCode.NotModified); // <2>
  }
  else
  {
    response = Request
      .CreateResponse(HttpStatusCode.OK, _stateFactory.Create(result));
    response.Content.Headers.LastModified = result.LastModified;
  }

  response.Headers.CacheControl = new CacheControlHeaderValue(); // <3>
  response.Headers.CacheControl.Public = true;
  response.Headers.CacheControl.MaxAge = TimeSpan.FromMinutes(5);

  return response;
}

Example 8-6 shows the new code that checks whether the IfModifiedSince header has been included in the request and is the same as the LastModified field in the retrieved issue <1>. If that condition is met, a response with the status code 304 Not Modified is returned <2>. Finally, the caching headers are updated and included as part of the response as well <3>.

Our next test, shown in Example 8-7, addresses the scenario in which the resource representation has changed on the origin server since the last time it was cached by the intermediary:

Scenario: Retrieving an existing issue that has changed
    Given an existing issue
    When it is retrieved with an IfModifiedSince header
    Then a LastModified header is returned
    Then a CacheControl header is returned
    Then a '200 OK' status is returned
    Then it is returned
Example 8-7. Unit test for validating a cached copy that has changed
private Uri _uriIssue1 = new Uri("http://localhost/issue/1");

[Scenario]
public void RetrievingModifiedIssue()
{
  IssueState issue = null;

  var fakeIssue = FakeIssues.FirstOrDefault();

  "Given an existing issue".
    f(() => MockIssueStore.Setup(i => i.FindAsync("1"))
        .Returns(Task.FromResult(fakeIssue)));
  "When it is retrieved with an IfModifiedSince header".
    f(() =>
  {
    Request.RequestUri = _uriIssue1;
    Request.Headers.IfModifiedSince = fakeIssue.LastModified
        .Subtract(TimeSpan.FromDays(1)); // <1>
    Response = Client.SendAsync(Request).Result;
    issue = Response.Content.ReadAsAsync<IssueState>().Result;
  });
  "Then a LastModified header is returned".
    f(() =>
    {
      Response.Content.Headers.LastModified.ShouldEqual(fakeIssue.LastModified);
    });
  "Then a CacheControl header is returned".
    f(() =>
    {
      Response.Headers.CacheControl.Public.ShouldBeTrue();
      Response.Headers.CacheControl.MaxAge.ShouldEqual(TimeSpan.FromMinutes(5));
    });
  "Then a '200 OK' status is returned".
    f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.OK)); // <2>
  "Then it is returned".
    f(() => issue.ShouldNotBeNull()); // <3>
}

There are some minor changes compared with the previous test that we implemented for sending a conditional GET. This test changes the value of the IfModifiedSince header to send a time in the past that differs from one set in the LastModified field for the issue. In this case, the implementation of the Get method will return a status code 200 OK with a fresh copy of the resource representation <3>.

Conflict Detection

We have discussed how you can use a conditional GET to revalidate a cached representation, and now we’ll cover the equivalent for updates: the conditional PUT or PATCH. A conditional PUT/PATCH can be used to detect possible conflicts when multiple updates are performed simultaneously over the same resource. It uses a first-write/first-win approach for conflict resolution, which means a client can commit an update operation only if the resource has not changed in the origin server since it was initially served; otherwise, it may receive a conflict error (HTTP status code 409 Conflict).

It also uses the If-None-Match and If-Modified-Since headers to represent the version or the timestamp associated with the resource representation that is going to be updated. The following steps illustrate how this approach works in detail with two clients (X1 and X2) trying to update the same resource R1:

  1. Client X1 performs a GET over R1 (version 1). The HTTP response includes the resource representation and an ETag header with the resource version—V1, in this case (Last-Modified could also be used).
  2. Client X2 performs a GET over the same resource R1 (version 1). It gets the same representation as client X1.
  3. Client X2 performs a PUT/PATCH over R1 to update its representation. This request includes the modified version of the resource representation and a header If-None-Match with the current resource version (V1). As a result of this update, the server returns a response with status code OK and increments the resource version by one (V2).
  4. Client X1 performs a PUT/PATCH over R1. This request message also includes a If-None-Match header with the resource version V1. The server detects that the resource has changed since it was obtained with version V1, so it returns a response with status code 409 Conflict.

Implementing Conflict Detection

Our first test, shown in Example 8-8, will update an issue with no conflicts, which means the value for IfModifiedSince will be the same as the one stored as part of the issue in the *LastModified() field. You will find these tests in the class ConflictDetection.

Scenario: Updating an issue with no conflicts
    Given an existing issue
    When a PATCH request is made with an IfModifiedSince header
    Then a '200 OK' is returned
    Then the issue should be updated
Example 8-8. Unit test for updating an issue with no conflicts
private Uri _uriIssue1 = new Uri("http://localhost/issue/1");

[Scenario]
public void UpdatingAnIssueWithNoConflict()
{
  var fakeIssue = FakeIssues.FirstOrDefault();

  "Given an existing issue".
    f(() =>
    {
      MockIssueStore.Setup(i => i.FindAsync("1"))
        .Returns(Task.FromResult(fakeIssue));
      MockIssueStore.Setup(i => i.UpdateAsync("1", It.IsAny<Object>()))
        .Returns(Task.FromResult(""));
    });
  "When a PATCH request is made with IfModifiedSince".
    f(() =>
    {
      var issue = new Issue();
      issue.Title = "Updated title";
      issue.Description = "Updated description";
      Request.Method = new HttpMethod("PATCH");
      Request.RequestUri = _uriIssue1;
      Request.Content = new ObjectContent<Issue>(issue,
         new JsonMediaTypeFormatter());
      Request.Headers.IfModifiedSince = fakeIssue.LastModified; // <1>
      Response = Client.SendAsync(Request).Result;
    });
  "Then a '200 OK' status is returned".
    f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.OK)); // <2>
  "Then the issue should be updated".
    f(() => MockIssueStore.Verify(i => i.UpdateAsync("1",
       It.IsAny<JObject>()))); // <3>
}

Example 8-8 shows the implementation of the first test scenario in which the IfModifiedSince header is set to the value of the LastModified property of the issue to be updated <1>. No conflicts should be detected on the server side, as the values for IfModifiedSince and LastModified should match, so a status code of 200 OK is returned <2>. Finally, the issue is updated in the issues store <3>.

The Patch method in the IssuesController class has to be modified to include all the conditional update logic, as Example 8-9 demonstrates.

Example 8-9. The new version of the Patch method that supports conditional updates
public async Task<HttpResponseMessage> Patch(string id, JObject issueUpdate)
{
    var issue = await _store.FindAsync(id);
    if (issue == null)
        return Request.CreateResponse(HttpStatusCode.NotFound);

    if (!Request.Headers.IfModifiedSince.HasValue) // <1>
        return Request.CreateResponse(HttpStatusCode.BadRequest,
                "Missing IfModifiedSince header");

    if (Request.Headers.IfModifiedSince != issue.LastModified) // <2>
       return Request.CreateResponse(HttpStatusCode.Conflict); // <3>

    await _store.UpdateAsync(id, issueUpdate);
    return Request.CreateResponse(HttpStatusCode.OK);

}

Example 8-9 shows the new changes introduced in the Patch method. When the client does not send an IfModifiedSince header, the implementation simply returns a response with status code 400 Bad Request, as the request is considered to be invalid <1>. Otherwise, the IfModifiedSince header received in the request message is compared with the LastModified field of the issue to be updated <2>. If they don’t match, a response with status code 409 Conflict is returned <3>. In any other case, the issue is finally updated and a response with status code 200 OK is returned.

The next test, shown in Example 8-10, addresses the scenario in which a conflict is detected:

Scenario: Updating an issue with conflicts
    Given an existing issue
    When a PATCH request is made with an IfModifiedSince header
    Then a '409 CONFLICT' is returned
    Then the issue is not updated
Example 8-10. Unit test for updating an issue with conflicts
[Scenario]
public void UpdatingAnIssueWithConflicts()
{
  var fakeIssue = FakeIssues.FirstOrDefault();

  "Given an existing issue".
    f(() =>
    {
      MockIssueStore.Setup(i => i.FindAsync("1"))
        .Returns(Task.FromResult(fakeIssue));
    });
  "When a PATCH request is made with IfModifiedSince".
    f(() =>
    {
      var issue = new Issue();
      issue.Title = "Updated title";
      issue.Description = "Updated description";
      Request.Method = new HttpMethod("PATCH");
      Request.RequestUri = _uriIssue1;
      Request.Content = new ObjectContent<Issue>(issue,
        new JsonMediaTypeFormatter());
      Request.Headers.IfModifiedSince = fakeIssue.LastModified.AddDays(1); // <1>
      Response = Client.SendAsync(Request).Result;
    });
  "Then a '409 CONFLICT' status is returned".
    f(() => Response.StatusCode
      .ShouldEqual(HttpStatusCode.Conflict)); // <2>
  "Then the issue should be not updated".
    f(() => MockIssueStore.Verify(i =>
      i.UpdateAsync("1", It.IsAny<JObject>()), Times.Never())); // <3>
}

Example 8-10 shows the implementation of the scenario in which a conflict is detected. We sent the IfModifiedSince header into the future by adding one day to the value of LastModified property in the issue that is going to be updated <1>. Since the values for IfModifiedSince and LastModified are different, the server will return a response with status code 409 Conflict, which is what the test expects <2>. Finally, the test also verifies that the issue was not updated in the issue store <3>.

Change Auditing

Another feature that Web API will support is the ability to identify the user or client who created a new issue or updated an existing one. That means the implementation has to authenticate the client using a predefined authentication scheme based on application keys, username/password, HMAC (hash-based message authentication code), or security tokens such as OAuth.

Using application keys is probably the simplest scenario. Every client application is identified with a simple and fixed application key. This authentication mechanism is perhaps a bit weak, but the data that the service has to offer is not sensitive at all. The data is available for everyone with a key, and it’s pretty much used for public services such as Google Maps or a search for public pictures (in Instagram, for example). The only purpose of the key is to identify clients and apply different service-level agreements such as API quotas or availability. Anyone can impersonate the client application by knowing the application key.

HMAC is similar to the application key authentication mechanism, but uses cryptography with a secret key to avoid the impersonation issues found in the first scheme. As opposed to basic authentication, the secret key or password is not sent on every message in plain text. A hash or HMAC is generated from some parts of the HTTP request message via the secret key, and that HMAC is included as part of the authorization header. The server can authenticate the client by validating the attached HMAC in the authorization header. This model fits well with cloud computing, where a vendor such as AWS (Amazon Web Services) or Windows Azure uses a key for identifying the tenant and provides the right services and private data. No matter which client application is used to consume the services and data, the main purpose of the key is to identify the tenant. Although there are several existing implementations of HMAC authentication, we will cover one called Hawk, which represents an emergent specification to standardize HMAC authentication.

The last scheme is based on security tokens, and it is probably the most complicated one. Here you can find OAuth, which was designed with the idea of delegating authorization in Web 2.0. The service that owns the data can use OAuth to share that data with other services or applications without compromising the owner credentials.

All these schemes will be discussed more in detail in Chapter 15. As part of this chapter, Hawk will be used to authenticate the client application before setting the auditing information on the issue.

Implementing Change Auditing with Hawk Authentication

The first test will create a new issue with auditing information about who created the issue. Therefore, this test will also have to authenticate the client first using HMAC authentication with Hawk. You will find the code for these tests in the CodeAuditing class.

Scenario: Creating a new issue
    Given a new issue
    When a POST request is made with an Authorization header containing the user
        identifier
    Then a '201 Created' status is returned
    Then the issue should be added with auditing information
    Then the response location header will be set to the resource location

To add Hawk authentication as part of the implementation, we’ll use an existing open source implementation called HawkNet, which is available on GitHub. This implementation provides integration with multiple Web API frameworks in .NET, including ASP.NET Web API. It accomplishes the integration with ASP.NET Web API through HTTP message handlers, as you can see in Example 8-11. One handler is used on the client side to automatically add the Hawk authorization header in every ongoing call, and another handler on the server side validates that header and authenticates the client.

Example 8-11. Injecting the HawkClientMessageHandler in the HttpClient instance
Credentials = new HawkCredential
{
    Id = "TestClient",
    Algorithm = "hmacsha256",
    Key = "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn",
    User = "test"
}; // <1>

var server = new HttpServer(GetConfiguration());
Client = new HttpClient(new HawkClientMessageHandler(server, Credentials)); // <2>

Example 8-11 shows how the HawkClientMessageHandler is injected into the HttpClient instance used by the tests. HawkCredential is the class used by HawkNet to configure different settings that specify how the Hawk header will be generated. The test configures this class to use SHA-256 as the algorithm for issuing the HMAC, the private key, the application id (TestClient), and the user associated with that key (test) <1>. Once the HawkCredential class is instantiated and configured, it is passed to the HawkClientMessageHandler injected in the HttpClient instance <2>.

In addition, the server also has to be configured with the message handler counterpart to validate the header and authenticate the client. HawkNet provides a HawkMessageHandler class for that purpose, which can be injected as part of the route configuration or as a global handler (see Example 8-12).

Example 8-12. Injecting the HawkMessageHandler in the route configuration
Credentials = new HawkCredential
{
    Id = "TestClient",
    Algorithm = "hmacsha256",
    Key = "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn",
    User = "test"
};

var config = new HttpConfiguration();

var serverHandler = new HawkMessageHandler(new HttpControllerDispatcher(config),
(id) => Credentials);

config.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id =
RouteParameter.Optional }, null, serverHandler);

Once the handlers for sending and authenticating the Hawk header are in place, we can finally start working on the tests for our first scenario about creating issues. Example 8-13 shows the final implementation of this test.

Example 8-13. Creating a new issue implementation
[Scenario]
public void CreatingANewIssue()
{
  Issue issue = null;

  "Given a new issue".
    f(() =>
    {
      issue = new Issue
      {
        Description = "A new issue",
        Title = "A new issue"
      };

      var newIssue = new Issue { Id = "1" };

      MockIssueStore
        .Setup(i => i.CreateAsync(issue, "test"))
        .Returns(Task.FromResult(newIssue));
    });
  "When a POST request is made with an Authorization header containing the user
  identifier".
    f(() =>
    {
        Request.Method = HttpMethod.Post;
        Request.RequestUri = _issues;
        Request.Content = new ObjectContent<Issue>(issue,
          new JsonMediaTypeFormatter());
        Response = Client.SendAsync(Request).Result;
    });
  "Then a '201 Created' status is returned".
    f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.Created));
  "Then the issue should be added with auditing information".
    f(() => MockIssueStore.Verify(i => i.CreateAsync(issue, "test"))); // <1>
  "Then the response location header will be set to the resource location".
    f(() => Response.Headers.Location.AbsoluteUri.ShouldEqual
        ("http://localhost/issue/1"));
}

The test mainly verifies that the issue is correctly persisted in the issue store along with the authenticated user test <1>. The CreateAsync method in the IIssueStore interface is modified to receive an additional argument representing the user who created the user. It is now the responsibility of the Post method in the IssueController class to pass that value inferred from the authenticated user (see Example 8-14).

Example 8-14. Updated version of the Post method
[Authorize]
public async Task<HttpResponseMessage> Post(Issue issue)
{
    var newIssue = await _store.CreateAsync(issue, User.Identity.Name); // <1>
    var response = Request.CreateResponse(HttpStatusCode.Created);
    response.Headers.Location = _linkFactory.Self(newIssue.Id).Href;
    return response;
}

The authenticated user becomes available in the User.Identity property, which was set by the HawkMessageHandler after the received Authorization header was validated. This user is passed to the CreateAsync method right after the received issue <1>. Also, the Post method has been decorated with the Authorize attribute to reject any anonymous call.

Scenario: Updating an issue
    Given an existing issue
    When a PATCH request is made with an Authorization header containing the
    user identifier
    Then a '200 OK' is returned
    Then the issue should be updated with auditing information

The implementation of the test for verifying this scenario also needs to check if changes are persisted in the IIssueStore along with the authenticated user, as shown in Example 8-15.

Example 8-15. Updating issue implementation
[Scenario]
public void UpdatingAnIssue()
{
  var fakeIssue = FakeIssues.FirstOrDefault();

  "Given an existing issue".
    f(() =>
    {
      MockIssueStore
        .Setup(i => i.FindAsync("1"))
        .Returns(Task.FromResult(fakeIssue));
      MockIssueStore
        .Setup(i => i.UpdateAsync("1", It.IsAny<Object>(), It.IsAny<string>()))
        .Returns(Task.FromResult(""));
    });
  "When a PATCH request is made with an Authorization header containing the user
  identifier".
    f(() =>
    {
      var issue = new Issue();
      issue.Title = "Updated title";
      issue.Description = "Updated description";
      Request.Method = new HttpMethod("PATCH");
      Request.Headers.IfModifiedSince = fakeIssue.LastModified;
      Request.RequestUri = _uriIssue1;
      Request.Content = new ObjectContent<Issue>(issue, new JsonMediaTypeFormatter());
      Response = Client.SendAsync(Request).Result;
    });
  "Then a '200 OK' status is returned".
    f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.OK));
  "Then the issue should be updated with auditing information".
    f(() => MockIssueStore.Verify(i => i.UpdateAsync("1", It.IsAny<JObject>(),
        "test"))); // <1>
}

The UpdateAsync method in the IIssueStore interface was also modified to receive an additional argument representing the user who created the user <1>.

Example 8-16 shows the modified version of the Patch method. The UpdateAsync call to the configured IIssueStore has been modified to pass the additional argument with the authenticated user.

Example 8-16. Updated version of the Patch method
[Authorize]
public async Task<HttpResponseMessage> Patch(string id, JObject issueUpdate)
{
  var issue = await _store.FindAsync(id);
  if (issue == null)
      return Request.CreateResponse(HttpStatusCode.NotFound);

  if (!Request.Headers.IfModifiedSince.HasValue)
      return Request.CreateResponse(HttpStatusCode.BadRequest,
          "Missing IfModifiedSince header");

  if (Request.Headers.IfModifiedSince != issue.LastModified)
     return Request.CreateResponse(HttpStatusCode.Conflict);

  await _store.UpdateAsync(id, issueUpdate, User.Identity.Name); // <1>
  return Request.CreateResponse(HttpStatusCode.OK);
}

Tracing

Tracing is an irreplaceable feature for troubleshooting or debugging a Web API in environments where a developer IDE or code debugging tool is not available, or in early stages of development when the API is not yet stabilized and some random, hard-to-identify issues occur. ASP.NET Web API ships with a tracing infrastructure out of the box that you can use to trace any activity performed by the framework itself or any custom code that is part of the Web API implementation.

The core component or service in this infrastructure is represented by the interface System.Web.Http.Tracing.ITraceWriter, which contains a single method, Trace, to generate a new trace entry.

Example 8-17. ITraceWriter interface definition
public interface ITraceWriter
{
  void Trace(HttpRequestMessage request, string category, TraceLevel level,
  Action<TraceRecord> traceAction);
}

The Trace method expects the following arguments:

request
Request message instance associated to the trace.
category
The category associated with the trace entry. This might become handy to group or filter the traces.
level
Detail level associated with the entry. This is also useful to filter the entries.
traceAction
A delegate to a method where the trace entry is generated.

Although this infrastructure is not tied to any existing logging framework in .NET—such as Log4Net, NLog, or Enterprise Library Logging—a default implementation has been provided. It is called System.Web.Http.Tracing.SystemDiagnosticsTraceWriter, and it uses System.Diagnostics.Trace.TraceSource. For the other frameworks, an implementation of the service interface ITraceWriter must be provided.

Example 8-18 illustrates how a custom implementation can be injected in the Web API configuration object.

Example 8-18. ITraceWriter configuration
HttpConfiguration config = new HttpConfiguration();
config.Services.Replace(typeof(ITraceWriter), new SystemDiagnosticsTraceWriter());

Implementing Tracing

There is a single scenario or test that covers tracing in general for all the methods in the IssueController class. That test can be found in the Tracing class.

Scenario: Creating, Updating, Deleting, or Retrieving an issue
    Given an existing or new issue
    When a request is made
    When the diagnostics tracing is enabled
    Then the diagnostics tracing information is generated

The first thing we’ll do before writing the test for this scenario is to configure an instance of ITraceWriter to check that tracing is actually working. See Example 8-19.

Example 8-19. ITraceWriter configuration for the tests
public abstract class IssuesFeature
{
  public Mock<ITraceWriter> MockTracer;

  public IssuesFeature()
  {
  }

  private HttpConfiguration GetConfiguration()
  {
    var config = new HttpConfiguration();

    MockTracer = new Mock<ITraceWriter>(MockBehavior.Loose);

    config.Services.Replace(typeof(ITraceWriter), MockTracer.Object); // <1>

    return config;
  }
}

Example 8-19 shows how a mock instance is injected in the HttpConfiguration instance used by Web API <1>. The test will use this mock instance (shown in Example 8-20) to verify the calls to the Trace method from the controller methods.

Example 8-20. Tracing test implementation
public class Tracing : IssuesFeature
{
  private Uri _uriIssue1 = new Uri("http://localhost/issue/1");

  [Scenario]
  public void RetrievingAnIssue()
  {
    IssueState issue = null;
    var fakeIssue = FakeIssues.FirstOrDefault();

    "Given an existing or new issue".
      f(() =>
      {
        MockIssueStore
          .Setup(i => i.FindAsync("1"))
          .Returns(Task.FromResult(fakeIssue)));
      }
    "When a request is made".
      f(() =>
      {
        Request.RequestUri = _uriIssue1;

        Response = Client
          .SendAsync(Request)
          .Result;

        issue = Response.Content
          .ReadAsAsync<IssueState>()
          .Result;
      });
    "When the diagnostics tracing is enabled".
      f(() =>
      {
        Configuration.Services
          .GetService(typeof(ITraceWriter)).ShouldNotBeNull(); // <1>
      });
    "Then the diagnostics tracing information is generated".
      f(() =>
      {
        MockTracer.Verify(m => m.Trace(It.IsAny<HttpRequestMessage>(), // <2>
          typeof(IssueController).FullName,
          TraceLevel.Debug,
          It.IsAny<Action<TraceRecord>>()));
      });
  }
}

The test implementation in Example 8-20 verifies that the ITraceWriter service is currently configured in the HttpConfiguration instance, and also checks that the IssueController class (shown in Example 8-21) is sending tracing messages to the configured mock instance.

Example 8-21. Tracing in the IssueController
public async Task<HttpResponseMessage> Get(string id)
{
    var tracer = this.Configuration.Services.GetTraceWriter(); // <1>

    var result = await _store.FindAsync(id);
    if (result == null)
    {
        tracer.Trace(Request,
            TraceCategory, TraceLevel.Debug,
               "Issue with id {0} not found", id); // <2>

        return Request.CreateResponse(HttpStatusCode.NotFound);
    }

    .....
}

The HttpConfiguration class provides an extension method or shortcut to obtain an instance of the configured ITraceWriter so it can be used by custom code in the implementation. Example 8-21 shows how the IssueController class has been modified to get a reference to the ITraceWriter <1>, which is used to trace information about an issue not found <2> before the response is returned.

Conclusion

This chapter covered several important aspects of improving an existing Web API, such as caching, conflict management, auditing, and tracing. Although they might not apply in certain scenarios, it is always useful to know which benefits they bring to the table so you can use them correctly.

Get Designing Evolvable Web APIs with ASP.NET now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.