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:
Implementing caching correctly on a Web API mainly involves two steps:
- Set the right headers to instruct intermediaries and clients (e.g., proxies, reverse proxies, local caches, browsers, etc.) to cache the responses.
-
Implement conditional
GET
s 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.
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.
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
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.
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
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.
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
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:
-
Client X1 performs a
GET
over R1 (version 1). The HTTP response includes the resource representation and anETag
header with the resource version—V1
, in this case (Last-Modified
could also be used). -
Client X2 performs a
GET
over the same resource R1 (version 1). It gets the same representation as client X1. -
Client X2 performs a
PUT
/PATCH
over R1 to update its representation. This request includes the modified version of the resource representation and a headerIf-None-Match
with the current resource version (V1
). As a result of this update, the server returns a response with status codeOK
and increments the resource version by one (V2
). -
Client X1 performs a
PUT
/PATCH
over R1. This request message also includes aIf-None-Match
header with the resource versionV1
. The server detects that the resource has changed since it was obtained with versionV1
, so it returns a response with status code409 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
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.
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
[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.
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).
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.
[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).
[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.
[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.
[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.
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.
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.
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.
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.
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.