Building the API - Designing Evolvable Web APIs with ASP.NET (2012)

Designing Evolvable Web APIs with ASP.NET (2012)

Chapter 7. Building the API

The proof of the pudding is in the eating, so let’s eat.

In the previous two chapters, you learned about the design of the issue tracker system, and the media types that it will support for its interactions. Throughout this chapter, you’ll see how to build the basic implementation of the Web API that supports that design. The goal for this exercise is not that the API should be fully functional or implement the entire design. It is to get the essential pieces in place that will enable us to address other concerns and to evolve the system.

This chapter is also not going to delve into too much detail on any of the individual parts, as the focus here is to put the pieces together. Later chapters will cover each of the different aspects of ASP.NET Web API in more detail.

The Design

At a high level, the design of the system is the following:

1. There is a backend system (such as GitHub) that manages issues.

2. The Issue collection resource retrieves items from the backend. It returns a response in either the Issue+Json or Collection+Json formats. This resource can also be used for creating new issues via an HTTP POST.

3. The Issue item resources contain representations of a single issue from the backend system. Issues can be updated via PATCH or deleted via a DELETE request.

4. Each issue contains links with the following rel values:

self

Contains the URI for the issue itself

open

Requests that the issue status be changed to Closed

close

Requests that the issue status be be changed to Open

transition

Requests to move the issue to the next appropriate status (e.g., from Open to Closed)

5. A set of Issue processor resources handles the actions related to transitioning the state of the issue.

Getting the Source

The implementation and unit tests for the API are available in the WebApiBook repo, or by cloning the issuetracker repo and checking out the dev BuildingTheApi branch.

Building the Implementation Using BDD

The API was built in a test-driven manner using BDD-style acceptance tests to drive out the implementation. The main difference between this and traditional TDD style is its focus on the end-to-end scenarios rather than the implementation. With acceptance-style tests, you’ll get to see the full end-to-end process starting with the initial request.

BDD PRIMER

Behavior-driven development (BDD) is a style of test-driven development (TDD) that focuses on verifying the behavior of the system, whereas traditional TDD focuses on the implementation of different components. In BDD, requirements are generally written by a business expert in a form that can then be executed by the developer.

There are various forms of BDD, but the most common uses the Gherkin syntax or Given, When, Then syntax. This syntax breaks up tests into features and scenarios. A feature is a single component that is being tested. Each feature has one or more scenarios that cover different parts of the feature. Each scenario is then broken down by steps, where each step is a Given, When, and Then, And, or But statement.

The Given clause sets the initial state of the system, When specifies something performed on the system, and Then is an assertion of the expected behavior. Each clause can have multiple parts joined together with And for inclusion or But for exclusion.

Navigating the Solution

Open up the WebApiBook.IssueTrackerApi.sln, located in the src folder. You’ll notice the following projects:

WebApiBook.IssueTrackerApi

Contains the API implementation.

WebApiBook.IssueTrackerApi.AcceptanceTests

Contains BDD acceptance tests that verify the behavior of the system. Within the project file, you will see a Features folder with test files per feature, each of which contains one or more tests for that feature.

WebApiBook.IssueTrackerApi.SelfHost

Contains a self-host for the API.

Packages and Libraries

Throughout the code, you’ll notice the following packages and tools:

Microsoft.AspNet.WebApi.Core

ASP.NET Web API is used for authoring and hosting our API. The Core package provides the minimum set of functionality needed.

Microsoft.AspNet.WebAp.SelfHost

This package provides the ability to host an API outside of IIS.

Autofac.WebApi

Autofac is used for dependency and lifetime management.

xunit

XUnit is used as the test framework/runner.

Moq

Moq is used for mocking objects within tests.

Should

The Should library is used for “Should” assertion syntax.

XBehave

The XBehave library is used for Gherkin-style syntax in the tests.

CollectionJson

This adds support for the Collection+Json media type.

Self-Host

Included in the source is a self-host for the Issue Tracker API. This will allow you to fire up the API and send it HTTP requests using a browser or a tool such as Fiddler. This is one of the nice features of ASP.NET Web API that make it really easy to develop with. Open the application (make sure to use admin privileges) and run it. Immediately you will see you have a host up and running, as shown in Figure 7-1.

Self-host

Figure 7-1. Self-host

One thing to keep in mind is that running self-hosted projects in Visual Studio requires either running as an administrator or reserving a port using the netsh command.

Sending a request to http://localhost:8080 using an Accept header of application/vnd.image+json will give you the collection of issues shown in Figure 7-2.

Sending a request for issues to the self-hosted API

Figure 7-2. Sending a request for issues to the self-hosted API

If at any time throughout this chapter, you want to try out the API directly, using the self-host is the key! You can then put breakpoints in the API and step through to see exactly what is going on.

Now, on to the API!

Models and Services

The Issue Tracker API relies on a set of core services and models in its implementation.

Issue and Issue Store

As this is an issue tracker project, there needs to be a place to store and retrieve issues. The IIssueStore interface (WebApiBook.IssueTrackerApi\Infrastructure\IIssueStore.cs) defines methods for the creation, retrieval, and persistence of issues as shown in Example 7-1. Notice all the methods are async, as they will likely be network I/O-bound and should not block the application threads.

Example 7-1. IIssueStore interface

public interface IIssueStore

{

Task<IEnumerable<Issue>> FindAsync();

Task<Issue> FindAsync(string issueId);

Task<IEnumerable<Issue>> FindAsyncQuery(string searchText);

Task UpdateAsync(Issue issue);

Task DeleteAsync(string issueId);

Task CreateAsync(Issue issue);

}

The Issue class (WebApiBook.IssueTrackerApi\Models\Issue.cs) in Example 7-2 is a data model and contains data that is persisted for an issue in the store. It carries only the resource state and does not contain any links. Links are application state and do not belong in the domain, as they are an API-level concern.

Example 7-2. Issue class

public class Issue

{

public string Id { get; set; }

public string Title { get; set; }

public string Description { get; set; }

public IssueStatus Status { get; set; }

}

public enum IssueStatus {Open, Closed}

IssueState

The IssueState class (WebApiBook.IssueTrackerApi\Models\IssueState.cs) in Example 7-3 is a state model designed to carry both resource and application state. It can then be represented in one or more media types as part of an HTTP response.

Example 7-3. IssueState class

public class IssueState

{

public IssueState()

{

Links = new List<Link>();

}

public string Id { get; set; }

public string Title { get; set; }

public string Description { get; set; }

public IssueStatus Status { get; set; }

public IList<Link> Links { get; private set; }

}

Notice the IssueState class has the same members as the Issue class with the addition of a collection of links. You might wonder why the IssueState class doesn’t inherit from Issue. The answer is to have better separation of concerns. If IssueState inherits from Issue, then it is tightly coupled, meaning any changes to Issue will affect it. Evolvability is one of the qualities we want for the system; having good separation contributes to this, as parts can be modified independently of one another.

IssuesState

The IssuesState class (WebApiBook.IssueTrackerApi\Models\IssuesState.cs) in Example 7-4 is used for returning a collection of issues. The collection contains a set of top-level links. Notice the collection also explicitly implements the CollectionJson library’s IReadDocumentinterface. This interface, as you will see, is used by the CollectionJsonFormatter to write out the Collection+Json format if the client sends an Accept of application/vnd.collection+json. The standard formatters, however, will use the public surface.

Example 7-4. IssuesState class

using CJLink = WebApiContrib.CollectionJson.Link;

public class IssuesState : IReadDocument

{

public IssuesState()

{

Links = new List<Link>();

}

public IEnumerable<IssueState> Issues { get; set; }

public IList<Link> Links { get; private set; }

Collection IReadDocument.Collection

{

get

{

var collection = new Collection(); // <1>

collection.Href = Links.SingleOrDefault(l => l.Rel ==

IssueLinkFactory.Rels.Self).Href; // <2>

collection.Links.Add(new CJLink {Rel="profile",

Href = new Uri("http://webapibook.net/profile")}); // <3>

foreach (var issue in Issues) // <4>

{

var item = new Item(); // <5>

item.Data.Add(new Data {Name="Description",

Value=issue.Description}); // <6>

item.Data.Add(new Data {Name = "Status",

Value = issue.Status});

item.Data.Add(new Data {Name="Title",

Value = issue.Title});

foreach (var link in issue.Links) // <7>

{

if (link.Rel == IssueLinkFactory.Rels.Self)

item.Href = link.Href;

else

{

item.Links.Add(new CJLink{Href = link.Href,

Rel = link.Rel});

}

}

collection.Items.Add(item);

}

var query = new Query {

Rel=IssueLinkFactory.Rels.SearchQuery,

Href = new Uri("/issue", UriKind.Relative),

Prompt="Issue search" }; // <8>

query.Data.Add(

new Data() { Name = "SearchText",

Prompt = "Text to match against Title and Description" });

collection.Queries.Add(query);

return collection; // <9>

}

}

}

The most interesting logic is the Collection, which manufactures a Collection+Json document:

§ A new Collection+Json Collection is instantiated. <1>

§ The collection’s href is set. <2>

§ A profile link is added to link to a description of the collection <3>.

§ The issues state collection is iterated through <4>, creating corresponding Collection+Json Item instances <5> and setting the Data <6> and Links <7>.

§ An “Issue search” query is created and added to the document’s query collection. <8>

§ The collection is returned. <9>

Link

The Link class (WebApiBook.IssueTrackerApi\Models\Link.cs) in Example 7-5 carries the standard Rel and Href shown earlier and includes additional metadata for describing an optional action associated with that link.

Example 7-5. Link class

public class Link

{

public string Rel { get; set; }

public Uri Href { get; set; }

public string Action { get; set; }

}

IssueStateFactory

Now that the system has an Issue and an IssueState, there needs to be a way to get from the Issue to the State. The IssueStateFactory (WebApiBook.IssueTrackerApi\Infrastructure\IssueStateFactory.cs) in Example 7-6 takes an Issue instance and manufactures a correspondingIssueState instance including its links.

Example 7-6. IssueStateFactory class

public class IssueStateFactory : IStateFactory<Issue, IssueState> // <1>

{

private readonly IssueLinkFactory _links;

public IssueStateFactory(IssueLinkFactory links)

{

_links = links;

}

public IssueState Create(Issue issue)

{

var model = new IssueState // <2>

{

Id = issue.Id,

Title = issue.Title,

Description = issue.Description,

Status = Enum.GetName(typeof(IssueStatus),

issue.Status)

};

//add hypermedia

model.Links.Add(_links.Self(issue.Id)); // <2>

model.Links.Add(_links.Transition(issue.Id));

switch (issue.Status) { // <3>

case IssueStatus.Closed:

model.Links.Add(_links.Open(issue.Id));

break;

case IssueStatus.Open:

model.Links.Add(_links.Close(issue.Id));

break;

}

return model;

}

}

Here is how the code works:

§ The factory implements IStateFactory<Issue, IssueState>. This interface is implemented so that callers can depend on it rather than the concrete class, thereby making it easier to mock in a unit test.

§ The create method initializes an IssueState instance and copies over the data from the Issue <1>.

§ Next, it contains business logic for applying standard links, like Self and Transition <2>, as well as context-specific links, like Open and Close <3>.

LinkFactory

Whereas the StateFactory contains the logic for adding links, the IssueLinkFactory creates the link objects themselves. It provides strongly typed accessors for each link in order to make the consuming code easier to read and maintain.

First comes the LinkFactory class (WebApiBook.IssueTrackerApi\Infrastructure\LinkFactory.cs) in Example 7-7, which other factories derive from.

Example 7-7. LinkFactory class

public abstract class LinkFactory

{

private readonly UrlHelper _urlHelper;

private readonly string _controllerName;

private const string DefaultApi = "DefaultApi";

protected LinkFactory(HttpRequestMessage request, Type controllerType) // <1>

{

_urlHelper = new UrlHelper(request); // <2>

_controllerName = GetControllerName(controllerType);

}

protected Link GetLink<TController>(string rel, object id, string action,

string route = DefaultApi) // <3>

{

var uri = GetUri(new { controller=GetControllerName(

typeof(TController)), id, action}, route);

return new Link {Action = action, Href = uri, Rel = rel};

}

private string GetControllerName(Type controllerType) // <4>

{

var name = controllerType.Name;

return name.Substring(0, name.Length - "controller".Length).ToLower();

}

protected Uri GetUri(object routeValues, string route = DefaultApi) // <5>

{

return new Uri(_urlHelper.Link(route, routeValues));

}

public Link Self(string id, string route = DefaultApi) // <6>

{

return new Link { Rel = Rels.Self, Href = GetUri(

new { controller = _controllerName, id = id }, route) };

}

public class Rels

{

public const string Self = "self";

}

}

public abstract class LinkFactory<TController> : LinkFactory // <7>

{

public LinkFactory(HttpRequestMessage request) :

base(request, typeof(TController)) { }

}

This factory generates URIs given route values and a default route name:

§ It takes the HttpRequestMessage as a constructor parameter <1>, which it uses to construct a UrlHelper instance <2>. It also takes a controller type which it will use for generating a “self” link.

§ The GetLink generic method manufactures a link based on a rel, a controller to link to, and additional parameters. <3>

§ The GetControllerName method extracts the controller name given a type. It is used by the GetLink method. <4>

§ The GetUri method uses the UrlHelper method to generate the actual URI. <5>

§ The base factory returns a Self link <6> for the specified controller. Derived factories can add additional links, as you will see shortly.

§ The LinkFactory<TController> convenience class <7> is provided to offer a more strongly typed experience that does not rely on magic strings.

IssueLinkFactory

The IssueLinkFactory (WebApiBook.IssueTrackerApi\Infrastructure\IssueLinkFactory.cs) in Example 7-8 generates all the links specific to the Issue resource. It does not contain the logic for whether or not the link should be present in the response, as that is handled in theIssueStateFactory.

Example 7-8. IssueLinkFactory class

public class IssueLinkFactory : LinkFactory<IssueController> // <1>

{

private const string Prefix = "http://webapibook.net/rels#"; // <5>

public new class Rels : LinkFactory.Rels { // <3>

public const string IssueProcessor = Prefix + "issue-processor";

public const string SearchQuery = Prefix + "search";

}

public class Actions { // <4>

public const string Open="open";

public const string Close="close";

public const string Transition="transition";

}

public IssueLinkFactory(HttpRequestMessage request) // <2>

{

}

public Link Transition(string id) // <6>

{

return GetLink<IssueProcessorController>(

Rels.IssueProcessor, id, Actions.Transition);

}

public Link Open(string id) { // <7>

return GetLink<IssueProcessorController>(

Rels.IssueProcessor, id, Actions.Open);

}

public Link Close(string id) { // <8>

return GetLink<IssueProcessorController>(

Rels.IssueProcessor, id, Actions.Close);

}

}

Here’s how the class works:

§ This factory derives from LinkFactory<IssueController> as the self link it generates is for the IssueController <1>.

§ In the constructor it takes an HttpRequestMessage instance, which it passes to the base. It also passes the controller name, which the base factory uses for route generation <2>.

§ The factory also contains inner classes for Rels <3> and Actions <4>, removing the need for magic strings in the calling code.

§ Notice the base Rel <5> is a URI pointing to documentation on our website with a # to get to the specific Rel.

§ The factory includes Transition <6>, Open <7>, and Close <8> methods to generate links for transitioning the state of the system.

Acceptance Criteria

Before getting started, let’s identify at a high level acceptance criteria for the code using the BDD Gherkin syntax.

Following are the tests for the Issue Tracker API, which covers CRUD (create-read-update-delete) access to issues as well as issue processing:

Feature: Retrieving issues

Scenario: Retrieving an existing issue

Given an existing issue

When it is retrieved

Then a '200 OK' status is returned

Then it is returned

Then it should have an id

Then it should have a title

Then it should have a description

Then it should have a state

Then it should have a 'self' link

Then it should have a 'transition' link

Scenario: Retrieving an open issue

Given an existing open issue

When it is retrieved

Then it should have a 'close' link

Scenario: Retrieving a closed issue

Given an existing closed issue

When it is retrieved

Then it should have an 'open' link

Scenario: Retrieving an issue that does not exist

Given an issue does not exist

When it is retrieved

Then a '404 Not Found' status is returned

Scenario: Retrieving all issues

Given existing issues

When all issues are retrieved

Then a '200 OK' status is returned

Then all issues are returned

Then the collection should have a 'self' link

Scenario: Retrieving all issues as Collection+Json

Given existing issues

When all issues are retrieved as Collection+Json

Then a '200 OK' status is returned

Then Collection+Json is returned

Then the href should be set

Then all issues are returned

Then the search query is returned

Scenario: Searching issues

Given existing issues

When issues are searched

Then a '200 OK' status is returned

Then the collection should have a 'self' link

Then the matching issues are returned

Feature: Creating issues

Scenario: Creating a new issue

Given a new issue

When a POST request is made

Then a '201 Created' status is returned

Then the issue should be added

Then the response location header will be set to the resource location

Feature: Updating issues

Scenario: Updating an issue

Given an existing issue

When a PATCH request is made

Then a '200 OK' is returned

Then the issue should be updated

Scenario: Updating an issue that does not exist

Given an issue does not exist

When a PATCH request is made

Then a '404 Not Found' status is returned

Feature: Deleting issues

Scenario: Deleting an issue

Give an existing issue

When a DELETE request is made

Then a '200 OK' status is returned

Then the issue should be removed

Scenario: Deleting an issue that does not exist

Given an issue does not exist

When a DELETE request is made

Then a '404 Not Found' status is returned

Feature: Processing issues

Scenario: Closing an open issue

Given an existing open issue

When a POST request is made to the issue processor

And the action is 'close'

Then a '200 OK' status is returned

Then the issue is closed

Scenario: Transitioning an open issue

Given an existing open issue

When a POST request is made to the issue processor

And the action is 'transition'

Then a '200 OK' status is returned

The issue is closed

Scenario: Closing a closed issue

Given an existing closed issue

When a POST request is made to the issue processor

And the action is 'close'

Then a '400 Bad Request' status is returned

Scenario: Opening a closed issue

Given an existing closed issue

When a POST request is made to the issue processor

And the action is 'open'

Then a '200 OK' status is returned

Then it is opened

Scenario: Transitioning a closed issue

Given an existing closed issue

When a POST request is made to the issue processor

And the action is 'transition'

Then a '200 OK' status is returned

Then it is opened

Scenario: Opening an open issue

Given an existing open issue

When a POST request is made to the issue processor

And the action is 'open'

Then a '400 Bad Request' status is returned

Scenario: Performing an invalid action

Given an existing issue

When a POST request is made to the issue processor

And the action is not valid

Then a '400 Bad Request' status is returned

Scenario: Opening an issue that does not exist

Given an issue does not exist

When a POST request is made to the issue processor

And the action is 'open'

Then a '404 Not Found' status is returned

Scenario: Closing an issue that does not exist

Given an issue does not exist

When a POST request is made to the issue processor

And the action is 'close'

Then a '404 Not Found' status is returned

Scenario: Transitioning an issue that does not exist

Given an issue does not exist

When a POST request is made to the issue processor

And the action is 'transition'

Then a '404 Not Found' status is returned

Throughout the remainder of the chapter, you will delve into all the tests and implementation for retrieval, creation, updating, and deletion. There are additional tests for issue processing, which will not be covered. The IssueProcessor controller, however, will be covered, and all the code and implementation is available in the GitHub repo.

Feature: Retrieving Issues

This feature covers retrieving one or more issues from the API using an HTTP GET method. The tests for this feature are comprehensive in particular because the responses contain hypermedia, which is dynamically generated based on the state of the issues.

Open the RetrievingIssues.cs tests (WebApiBook.IssueTrackerApi.AcceptanceTests/Features/RetrievingIssues.cs). Notice the class derives from IssuesFeature, demonstrated in Example 7-9 (IssuesFeature.cs). This class is a common base for all the tests. It sets up an in-memory host for our API, which the tests can use to issue HTTP requests against.

Example 7-9. IssuesFeature class

public abstract class IssuesFeature

{

public Mock<IIssueStore> MockIssueStore;

public HttpResponseMessage Response;

public IssueLinkFactory IssueLinks;

public IssueStateFactory StateFactory;

public IEnumerable<Issue> FakeIssues;

public HttpRequestMessage Request { get; private set; }

public HttpClient Client;

public IssuesFeature()

{

MockIssueStore = new Mock<IIssueStore>(); // <1>

Request = new HttpRequestMessage();

Request.Headers.Accept.Add(

new MediaTypeWithQualityHeaderValue("application/vnd.issue+json"));

IssueLinks = new IssueLinkFactory(Request);

StateFactory = new IssueStateFactory(IssueLinks);

FakeIssues = GetFakeIssues(); // <2>

var config = new HttpConfiguration();

WebApiConfiguration.Configure(

config, MockIssueStore.Object);

var server = new HttpServer(config); // <3>

Client = new HttpClient(server); // <4>

}

private IEnumerable<Issue> GetFakeIssues()

{

var fakeIssues = new List<Issue>();

fakeIssues.Add(new Issue { Id = "1", Title = "An issue",

Description = "This is an issue",

Status = IssueStatus.Open });

fakeIssues.Add(new Issue { Id = "2", Title = "Another issue",

Description = "This is another issue",

Status = IssueStatus.Closed });

return fakeIssues;

}

}

The IssuesFeature constructor initializes instances/mocks of the services previously mentioned, which are common to all the tests:

§ Creates an HttpRequest <1> and sets up test data <2>.

§ Initializes an HttpServer, passing in the configuration object configured via the Configure method <3>.

§ Sets the Client property to a new HttpClient instance, passing the HttpServer in the constructor <4>.

Example 7-10 demonstrates the WebApiConfiguration class.

Example 7-10. WebApiConfiguration class

public static class WebApiConfiguration

{

public static void Configure(HttpConfiguration config,

IIssueStore issueStore = null)

{

config.Routes.MapHttpRoute("DefaultApi", // <1>

"{controller}/{id}", new { id = RouteParameter.Optional });

ConfigureFormatters(config);

ConfigureAutofac(config, issueStore);

}

private static void ConfigureFormatters(HttpConfiguration config)

{

config.Formatters.Add(new CollectionJsonFormatter()); // <2>

JsonSerializerSettings settings = config.Formatters.JsonFormatter.

SerializerSettings; // <3>

settings.NullValueHandling = NullValueHandling.Ignore;

settings.Formatting = Formatting.Indented;

settings.ContractResolver =

new CamelCasePropertyNamesContractResolver();

config.Formatters.JsonFormatter.SupportedMediaTypes.Add(

new MediaTypeHeaderValue("application/vnd.issue+json"));

}

private static void ConfigureAutofac(HttpConfiguration config,

IIssueStore issueStore)

{

var builder = new ContainerBuilder(); // <4>

builder.RegisterApiControllers(typeof(IssueController).Assembly);

if (issueStore == null) // <5>

builder.RegisterType<InMemoryIssueStore>().As<IIssueStore>().

InstancePerLifetimeScope();

else

builder.RegisterInstance(issueStore);

builder.RegisterType<IssueStateFactory>(). // <6>

As<IStateFactory<Issue, IssueState>>().InstancePerLifetimeScope();

builder.RegisterType<IssueLinkFactory>().InstancePerLifetimeScope();

builder.RegisterHttpRequestMessage(config); // <7>

var container = builder.Build(); // <8>

config.DependencyResolver = new AutofacWebApiDependencyResolver(container);

}

}

The WebApiConfiguration.Configure method in Example 7-10 does the following:

§ Registers the default route <1>.

§ Adds the Collection+Json formatter <2>.

§ Configures the default JSON formatter to ignore nulls, force camel casing for properties, and support the Issue media type <3>.

§ Creates an Autofac ContainerBuilder and registers all controllers <4>.

§ Registers the store using the passed-in store instance if provided (used for passing in a mock instance) <5> and otherwise defaults to the InMemoryStore.

§ Registers the remaining services <6>.

§ Wires up Autofac to inject the current HttpRequestMessage as a dependency <7>. This enables services such as the IssueLinkFactory to get the request.

§ Creates the container and passes it to the Autofac dependency resolver <8>.

Retrieving an Issue

The first set of tests verifies retrieval of an individual issue and that all the necessary data is present:

Scenario: Retrieving an existing issue

Given an existing issue

When it is retrieved

Then a '200 OK' status is returned

Then it is returned

Then it should have an id

Then it should have a title

Then it should have a description

Then it should have a state

Then it should have a 'self' link

Then it should have a 'transition' link

The associated tests are in Example 7-11.

Example 7-11. Retrieving an issue

[Scenario]

public void RetrievingAnIssue(IssueState issue, Issue fakeIssue)

{

"Given an existing issue".

f(() =>

{

fakeIssue = FakeIssues.FirstOrDefault();

MockIssueStore.Setup(i => i.FindAsync("1")).

Returns(Task.FromResult(fakeIssue)); // <1>

});

"When it is retrieved".

f(() =>

{

Request.RequestUri = _uriIssue1; // <2>

Response = Client.SendAsync(Request).Result; // <3>

issue = Response.Content.ReadAsAsync<IssueState>().Result; // <4>

});

"Then a '200 OK' status is returned".

f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.OK)); // <5>

"Then it is returned".

f(() => issue.ShouldNotBeNull()); // <6>

"Then it should have an id".

f(() => issue.Id.ShouldEqual(fakeIssue.Id)); // <7>

"Then it should have a title".

f(() => issue.Title.ShouldEqual(fakeIssue.Title)); // <8>

"Then it should have a description".

f(() => issue.Description.ShouldEqual(fakeIssue.Description)); // <9>

"Then it should have a state".

f(() => issue.Status.ShouldEqual(fakeIssue.Status)); // <10>

"Then it should have a 'self' link".

f(() =>

{

var link = issue.Links.FirstOrDefault(l => l.Rel ==

IssueLinkFactory.Rels.Self);

link.ShouldNotBeNull(); // <11>

link.Href.AbsoluteUri.ShouldEqual(

"http://localhost/issue/1"); // <12>

});

"Then it should have a transition link".

f(() =>

{

var link = issue.Links.FirstOrDefault(l =>

l.Rel == IssueLinkFactory.Rels.IssueProcessor &&

l.Action == IssueLinkFactory.Actions.Transition);

link.ShouldNotBeNull(); // <13>

link.Href.AbsoluteUri.ShouldEqual(

"http://localhost/issueprocessor/1?action=transition"); // <14>

});

}

Understanding the tests

For those who are not familiar with XBehave.NET, the test syntax used here might look confusing. In XBehave, tests for a specific scenario are grouped together in a single class method, which is annotated with a [Scenario] attribute. Each method can have one or more parameters (e.g.,issue and fakeIssue), which XBehave will set to their default values rather than defining variables inline.

Within each method there is one more test that will be executed. XBehave allows a “free from string” syntax that allows for describing the test in plain English. The f() function is an extension method of System.String, which takes a lambda. The string provided is only documentation for the user reading the test code and/or viewing the results—it has no meaning to XBehave itself. In practice, Gherkin syntax will be used within the strings, but this is not actually required. XBehave cares only about the lambdas, which it executes in the order that they are defined.

Another common pattern you will see in the tests is the usage of the Should library. This library introduces a set of extension methods that start with Should and perform assertions. The syntax it provides is more terse than Assert methods. In the retrieving issue tests, ShouldEqual andShouldNotBeNull method calls are both examples of using this library.

Here is an overview of what the preceding tests perform:

§ Sets up the mock store to return an issue <1>.

§ Sets the request URI to the issue resource <2>.

§ Sends the request <3> and extracts the issue from the response <4>.

§ Verifies that the status code is 200 <5>.

§ Verifies that the issue is not null <6>.

§ Verifies that the id <7>, title <8>, description <9>, and status <10> match the issue that was passed to the mock store.

§ Verifies that a Self link was added, pointing to the issue resource.

§ Verifies that a Transition link was added, pointing to the issue processor resource.

Requests for an individual issue are handled by the Get overload on the IssueController, as shown in Example 7-12.

Example 7-12. IssueController Get overload method

public async Task<HttpResponseMessage> Get(string id)

{

var result = await _store.FindAsync(id); // <1>

if (result == null)

return Request.CreateResponse(HttpStatusCode.NotFound); // <2>

return Request.CreateResponse(HttpStatusCode.OK,

_stateFactory.Create(result)); // <3>

}

This method queries for a single issue <1>, returns a 404 Not Found status code if the resource cannot be found <2>, and returns only a single item rather then a higher-level document <3>.

As you’ll see, most of these tests are actually not testing the controller itself but rather the IssueStateFactory.Create method shown earlier in Example 7-6.

Retrieving Open and Closed Issues

Scenario: Retrieving an open issue

Given an existing open issue

When it is retrieved

Then it should have a 'close' link

Scenario: Retrieving a closed issue

Given an existing closed issue

When it is retrieved

Then it should have an 'open' link

The scenario tests can be seen in Examples 7-13 and 7-14.

The next set of tests are very similar, checking for a close link on an open issue (Example 7-13) and an open link on a closed issue (Example 7-14).

Example 7-13. Retrieving an open issue

[Scenario]

public void RetrievingAnOpenIssue(Issue fakeIssue, IssueState issue)

{

"Given an existing open issue".

f(() =>

{

fakeIssue = FakeIssues.Single(i =>

i.Status == IssueStatus.Open);

MockIssueStore.Setup(i => i.FindAsync("1")).Returns(

Task.FromResult(fakeIssue)); // <1>

});

"When it is retrieved".

f(() =>

{

Request.RequestUri = _uriIssue1; // <2>

issue = Client.SendAsync(Request).Result.Content.

ReadAsAsync<IssueState>().Result; // <3>

});

"Then it should have a 'close' action link".

f(() =>

{

var link = issue.Links.FirstOrDefault(

l => l.Rel == IssueLinkFactory.Rels.IssueProcessor &&

l.Action == IssueLinkFactory.Actions.Close); // <4>

link.ShouldNotBeNull();

link.Href.AbsoluteUri.ShouldEqual(

"http://localhost/issueprocessor/1?action=close");

});

}

Example 7-14. Retrieving a closed issue

public void RetrievingAClosedIssue(Issue fakeIssue, IssueState issue)

{

"Given an existing closed issue".

f(() =>

{

fakeIssue = FakeIssues.Single(i =>

i.Status == IssueStatus.Closed);

MockIssueStore.Setup(i => i.FindAsync("2")).Returns(

Task.FromResult(fakeIssue)); // <1>

});

"When it is retrieved".

f(() =>

{

Request.RequestUri = _uriIssue2; // <2>

issue = Client.SendAsync(Request).Result.Content.

ReadAsAsync<IssueState>().Result; // <3>

});

"Then it should have a 'open' action link".

f(() =>

{

var link = issue.Links.FirstOrDefault(

l => l.Rel == IssueLinkFactory.Rels.IssueProcessor &&

l.Action == IssueLinkFactory.Actions.Open); // <4>

link.ShouldNotBeNull();

link.Href.AbsoluteUri.ShouldEqual(

"http://localhost/issueprocessor/2?action=open");

});

}

The implementation for each test is also very similar:

§ Sets up the mock store to return the open (id=1) or closed issue (id=2) appropriate for the test <1>.

§ Sets the request URI for the resource being retrieved <2>.

§ Sends the request and captures the issue in the result <3>.

§ Verifies that the appropriate Open or Close link is present <4>.

Similar to the previous test, this test also verifies logic present in the IssueStateFactory, which is shown in Example 7-15. It adds the appropriate links depending on the status of the issue.

Example 7-15. IssueStateFactory Create method

public IssueState Create(Issue issue)

{

...

switch (model.Status) {

case IssueStatus.Closed:

model.Links.Add(_links.Open(issue.Id));

break;

case IssueStatus.Open:

model.Links.Add(_links.Close(issue.Id));

break;

}

return model;

}

Retrieving an Issue That Does Not Exist

The next scenario verifies the system returns a 404 Not Found if the resource does not exist:

Scenario: Retrieving an issue that does not exist

Given an issue does not exist

When it is retrieved

Then a '404 Not Found' status is returned

The scenario tests are in Example 7-16.

Example 7-16. Retrieving an issue that does not exist

[Scenario]

public void RetrievingAnIssueThatDoesNotExist()

{

"Given an issue does not exist".

f(() => MockIssueStore.Setup(i =>

i.FindAsync("1")).Returns(Task.FromResult((Issue)null))); // <1>

"When it is retrieved".

f(() =>

{

Request.RequestUri = _uriIssue1; // <2>

Response = Client.SendAsync(Request).Result; // <3>

});

"Then a '404 Not Found' status is returned".

f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.NotFound)); // <4>

}

How the tests work:

§ Sets up the store to return a null issue <1>. Notice the Task.FromResult extension is used to easily create a Task that contains a null object in its result.

§ Sets the request URI <2>.

§ Issues the request and captures the response <3>.

§ Verifies the code is verified to be HttpStatusCode.NotFound <4>.

In the IssueController.Get method, this scenario is handled with the code in Example 7-17.

Example 7-17. IssueController Get method returning a 404

if (result == null)

return Request.CreateResponse(HttpStatusCode.NotFound);

Retrieving All Issues

This scenario verifies that the issue collection can be properly retrieved:

Scenario: Retrieving all issues

Given existing issues

When all issues are retrieved

Then a '200 OK' status is returned

Then all issues are returned

Then the collection should have a 'self' link

The tests for this scenario are shown in Example 7-18.

Example 7-18. Retrieving all issues

private Uri _uriIssues = new Uri("http://localhost/issue");

private Uri _uriIssue1 = new Uri("http://localhost/issue/1");

private Uri _uriIssue2 = new Uri("http://localhost/issue/2");

[Scenario]

public void RetrievingAllIssues(IssuesState issuesState)

{

"Given existing issues".

f(() => MockIssueStore.Setup(i => i.FindAsync()).Returns(

Task.FromResult(FakeIssues))); // <1>

"When all issues are retrieved".

f(() =>

{

Request.RequestUri = _uriIssues; // <2>

Response = Client.SendAsync(Request).Result; // <3>

issuesState = Response.Content.

ReadAsAsync<IssuesState>().Result; // <4>

});

"Then a '200 OK' status is returned".

f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.OK)); // <5>

"Then they are returned".

f(() =>

{

issuesState.Issues.FirstOrDefault(i => i.Id == "1").

ShouldNotBeNull(); // <6>

issuesState.Issues.FirstOrDefault(i => i.Id == "2").

ShouldNotBeNull();

});

"Then the collection should have a 'self' link".

f(() =>

{

var link = issuesState.Links.FirstOrDefault(

l => l.Rel == IssueLinkFactory.Rels.Self); // <7>

link.ShouldNotBeNull();

link.Href.AbsoluteUri.ShouldEqual("http://localhost/issue");

});

}

These tests verify that a request sent to /issue returns all the issues:

§ Sets up the mock store to return the collection of fake issues <1>.

§ Sets the request URI to the issue resource <2>.

§ Sends the request and captures the response <3>.

§ Reads the response content and converts it to an IssuesState instance <4>. The ReadAsAsync method uses the formatter associated with the HttpContent instance to manufacture an object from the contents.

§ Verifies that the returned status is OK <5>.

§ Verifies that the correct issues are returned <6>.

§ Verifies that the Self link is returned <7>.

On the server, the issue resource is handled by the IssueController.cs file (WebApiBook.IssueTrackerApi/Controllers/IssueController). The controller takes an issues store, an issue state factory, and an issue link factory as dependencies (as shown in Example 7-19).

Example 7-19. IssueController constructor

public class IssueController : ApiController

{

private readonly IIssueStore _store;

private readonly IStateFactory<Issue, IssueState> _stateFactory;

private readonly IssueLinkFactory _linkFactory;

public IssueController(IIssueStore store,

IStateFactory<Issue, IssueState> stateFactory,

IssueLinkFactory linkFactory)

{

_store = store;

_stateFactory = stateFactory;

_linkFactory = linkFactory;

}

...

}

The request for all issues is handled by the parameterless Get method (Example 7-20).

Example 7-20. IssueController Get method

public async Task<HttpResponseMessage> Get()

{

var result = await _store.FindAsync(); // <1>

var issuesState = new IssuesState(); // <2>

issuesState.Issues = result.Select(i => _stateFactory.Create(i)); // <3>

issuesState.Links.Add(new Link{

Href=Request.RequestUri, Rel = LinkFactory.Rels.Self}); // <4>

return Request.CreateResponse(HttpStatusCode.OK, issuesState); // <5>

}

Notice the method is marked with the async modifier and returns Task<HttpResponseMessage>. By default, API controller operations are sync; thus, as the call is executing it will block the calling thread. In the case of operations that are making I/O calls, this is bad—it will reduce the number of threads that can handle incoming requests. In the case of the issue controller, all of the calls involve I/O, so using async and returning a Task make sense. I/O-intensive operations are then awaited via the await keyword.

Here is what the code is doing:

§ First, an async call is made to the issue store FindAsync method to get the issues <1>.

§ An IssuesState instance is created for carrying issue data <2>.

§ The issues collection is set, but invokes the Create method on the state factory for each issue <3>.

§ The Self link is added via the URI of the incoming request <4>.

§ The response is created, passing the IssuesState instance for the content <5>.

In the previous snippet, the Request.CreateResponse method is used to return an HttpResponseMessage. You might ask, why not just return a model instead? Returning an HttpResponseMessage allows for directly manipulating the components of the HttpResponse, such as the status and the headers. Although currently the response headers are not modified for this specific controller action, this will likely happen in the future. You will also see that the rest of the actions do manipulate the response.

WHERE IS THE PROPER PLACE TO HANDLE HYPERMEDIA?

The following question often arises: where in the system should hypermedia controls be applied? Should they be handled in the controller, or via the pipeline with a message handler, filter, or formatter? There is no one right answer—all of these are valid places to handle hypermedia—but there are trade-offs to consider:

§ If links are handled in a controller, they are more explicit/obvious and easier to step through.

§ If links are handled in the pipeline, controller actions are leaner and have less logic.

§ Message handlers, filters, and controllers have easy access to the request, which they can use for link generation.

In this book, we’ve chosen to handle the logic in the controller, either inline as in the Get method for retrieving multiple issues, or via an injected service as in the Get method for a single issue. The reasoning for this is that the link logic is more explicit/closer to the controller. The controller’s job is to translate between the business domain and the HTTP world. As links are an HTTP-specific concern, handling them in a controller is perfectly reasonable.

That being said, the other approaches are workable and there is nothing fundamentally wrong with using them.

Retrieving All Issues as Collection+Json

As mentioned in the previous chapter, Collection+Json is a format that is well suited for managing and querying lists of data. The issue resource supports Collection+Json for requests on resources that return multiple items. This test verifies that it can return Collection+Jsonresponses.

The next scenario verifies that the API properly handles requests for Collection+Json:

Scenario: Retrieving all issues as Collection+Json

Given existing issues

When all issues are retrieved as Collection+Json

Then a '200 OK' status is returned

Then Collection+Json is returned

Then the href should be set

Then all issues are returned

Then the search query is returned

The test in Example 7-21 issues such a request and validates that the correct format is returned.

Example 7-21. Retrieving all issues as Collection+Json

[Scenario]

public void RetrievingAllIssuesAsCollectionJson(IReadDocument readDocument)

{

"Given existing issues".

f(() => MockIssueStore.Setup(i => i.FindAsync()).

Returns(Task.FromResult(FakeIssues)));

"When all issues are retrieved as Collection+Json".

f(() =>

{

Request.RequestUri = _uriIssues;

Request.Headers.Accept.Clear(); // <1>

Request.Headers.Accept.Add(

new MediaTypeWithQualityHeaderValue(

"application/vnd.collection+json"));

Response = Client.SendAsync(Request).Result;

readDocument = Response.Content.ReadAsAsync<ReadDocument>(

new[] {new CollectionJsonFormatter()}).Result; // <2>

});

"Then a '200 OK' status is returned".

f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.OK)); // <3>

"Then Collection+Json is returned".

f(() => readDocument.ShouldNotBeNull()); // <4>

"Then the href should be set".

f(() => readDocument.Collection.Href.AbsoluteUri.ShouldEqual(

"http://localhost/issue")); // <5>

"Then all issues are returned"

f(() =>

{

readDocument.Collection.Items.FirstOrDefault(

i=>i.Href.AbsoluteUri=="http://localhost/issue/1").

ShouldNotBeNull(); // <6>

readDocument.Collection.Items.FirstOrDefault(

i=>i.Href.AbsoluteUri=="http://localhost/issue/2").

ShouldNotBeNull();

});

"Then the search query is returned".

f(() => readDocument.Collection.Queries.SingleOrDefault(

q => q.Rel == IssueLinkFactory.Rels.SearchQuery).

ShouldNotBeNull()); // <7>

}

After the standard setup, the tests do the following:

§ Sets the Accept header to application/vnd.collection+json and sends the request <1>.

§ Reads the content using the CollectionJson packages’ ReadDocument <2>.

§ Verifies that a 200 OK status is returned <3>.

§ Verifies that the returned document is not null (this means valid Collection+Json was returned) <4>.

§ Checks that the document’s href (self) URI is set <5>.

§ Checks that the expected items are present <6>.

§ Checks that the search query is present in the Queries collection <7>.

On the server, the same method as in the previous test is invoked—that is, IssueController.Get(). However, because the CollectionJsonFormatter is used, the returned IssuesState object will be written via the IReadDocument interface that it implements, as shown previously in Example 7-4.

Searching Issues

This scenario validates that the API allows users to perform a search and that the results are returned:

Scenario: Searching issues

Given existing issues

When issues are searched

Then a '200 OK' status is returned

Then the collection should have a 'self' link

Then the matching issues are returned

The tests for this scenario are shown in Example 7-22.

Example 7-22. Searching issues

[Scenario]

public void SearchingIssues(IssuesState issuesState)

{

"Given existing issues".

f(() => MockIssueStore.Setup(i => i.FindAsyncQuery("another"))

.Returns(Task.FromResult(FakeIssues.Where(i=>i.Id == "2")))); // <1>

"When issues are searched".

f(() =>

{

Request.RequestUri = new Uri(_uriIssues, "?searchtext=another");

Response = Client.SendAsync(Request).Result;

issuesState = Response.Content.ReadAsAsync<IssuesState>().Result; // <2>

});

"Then a '200 OK' status is returned".

f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.OK)); // <3>

"Then the collection should have a 'self' link".

f(() =>

{

var link = issuesState.Links.FirstOrDefault(

l => l.Rel == IssueLinkFactory.Rels.Self); // <4>

link.ShouldNotBeNull();

link.Href.AbsoluteUri.ShouldEqual(

"http://localhost/issue?searchtext=another");

});

"Then the matching issues are returned".

f(() =>

{

var issue = issuesState.Issues.FirstOrDefault(); // <5>

issue.ShouldNotBeNull();

issue.Id.ShouldEqual("2");

});

}

Here’s how the tests work:

§ Sets the mock issue store to return issue 2 when FindAsyncQuery is invoked <1>.

§ Appends the query string to the query URI, issues a request, and reads the content as an IssuesState instance <2>.

§ Verifies that a 200 OK status is returned <3>.

§ Verifies that the Self link is set for collection <4>.

§ Verifies that the expected issue is returned <5>.

The code for the search functionality is shown in Example 7-23.

Example 7-23. IssueController GetSearch method

public async Task<HttpResponseMessage> GetSearch(string searchText) // <1>

{

var issues = await _store.FindAsyncQuery(searchText); // <2>

var issuesState = new IssuesState();

issuesState.Issues = issues.Select(i => _stateFactory.Create(i)); // <3>

issuesState.Links.Add( new Link {

Href = Request.RequestUri, Rel = LinkFactory.Rels.Self }); // <4>

return Request.CreateResponse(HttpStatusCode.OK, issuesState); // <5>

}

§ The method name is GetSearch <1>. ASP.NET Web API’s selector matches the current HTTP method conventionally against methods that start with the same HTTP method name. Thus, it is reachable by an HTTP GET. The parameter of the method matches against the query string param searchtext.

§ Issues matching the search are retrieved with the FindAsyncQuery method <2>.

§ An IssuesState instance is created and its issues are populated with the result of the search <3>.

§ A Self link is added, pointing to the original request <4>.

§ An OK response is returned with the issues as the payload <5>.

NOTE

Similar to requests for all issues, this resource also supports returning a Collection+Json representation.

This finishes off all of the scenarios for the issue retrieval feature; now, on to creation!

Feature: Creating Issues

This feature contains a single scenario that covers when a client creates a new issue using an HTTP POST:

Scenario: Creating a new issue

Given a new issue

When a POST request is made

Then the issue should be added

Then a '201 Created' status is returned

Then the response location header will be set to the new resource location

The test is in Example 7-24.

Example 7-24. Creating issues

[Scenario]

public void CreatingANewIssue(dynamic newIssue)

{

"Given a new issue".

f(() =>

{

newIssue = new JObject();

newIssue.description = "A new issue";

newIssue.title = "NewIssue"; // <1>

MockIssueStore.Setup(i => i.CreateAsync(It.IsAny<Issue>())).

Returns<Issue>(issue=>

{

issue.Id = "1";

return Task.FromResult("");

}); // <2>

});

"When a POST request is made".

f(() =>

{

Request.Method = HttpMethod.Post;

Request.RequestUri = _issues;

Request.Content = new ObjectContent<dynamic>(

newIssue, new JsonMediaTypeFormatter()); // <3>

Response = Client.SendAsync(Request).Result;

});

"Then the issue should be added".

f(() => MockIssueStore.Verify(i => i.CreateAsync(

It.IsAny<Issue>()))); // <4>

"Then a '201 Created' status is returned".

f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.Created)); // <5>

"Then the response location header will be set to the resource location".

f(() => Response.Headers.Location.AbsoluteUri.ShouldEqual(

"http://localhost/issue/1")); // <6>

}

Here’s how the tests work:

§ Creates a new issue to be sent to the server <1>.

§ Configures the mock store to set the issue’s Id <2>. Notice the call to Task.FromResult. The CreateAsync method expects a Task to be returned. This is a simple way to create a dummy task. You will see the same approach is used in other tests if the method on the store returns aTask.

§ Configures the request to be a POST with the request content being set to the new issue <3>. Notice here that instead of using a static CLR type like Issue, it uses a JObject instance (from Json.NET) cast to dynamic. We can use a similar approach for staying typeless on the server, which you’ll see shortly.

§ Verifies that the CreateAsync method was called to create the issue <4>.

§ Verifies that the status code was set to a 201 in accordance with the HTTP spec (covered in Chapter 1) <5>.

§ Verifies that the location header is set to the location of the created resource <6>.

THE WEB IS TYPELESS

In Example 7-24 the client creates a dynamic type to send to the server rather than a static type. This may make a whole bunch of people in the room ask, “Where is my static type?” It is true that .NET is a (mostly) static language; the Web, however, is typeless. As we saw in Chapter 1, the foundation of the Web is message based, not type based. Clients send messages to servers in a set of known formats (media types). A media type describes the structure of a message; it is not the same as a static type in a programming stack like .NET. This typelessness is not an accident; it is by design.

Because the Web is typeless, resources are easily accessible to the largest possible set of clients and servers. Typelessness is also a big factor in allowing clients and servers to independently evolve and allowing side-by-side versioning. Servers can understand new elements in the messages they receive without requiring existing clients to upgrade. The message formats can even evolve in ways that are breaking but still don’t force clients to upgrade. In a typed world, this is not possible due to the inherent constraints that a type imposes.

SOAP Services are an example of a protocol that introduced typing to the Web, and that was wrought with issues. One of the biggest pains you hear in practice from companies that implemented SOAP Services is around the clients. Communication with a SOAP Service requires a WSDL document describing the operations and the types. Whenever the services change in a significant way, they generally break all the clients. This either requires clients to upgrade, or calls for a parallel version of the service to be deployed, which newer clients can access only by getting the WSDL.

All this being said, this is not a recommendation to not use types at all as a more developer-friendly way to access API requests and responses. Those types should not, however, become a requirement for interacting with the system, and there is nothing wrong with using dynamic types (in fact, there may be cases where it is even advantageous).

The implementation within the controller is shown in Example 7-25.

Example 7-25. IssueController Post method

public async Task<HttpResponseMessage> Post(dynamic newIssue) // <1>

{

var issue = new Issue {

Title = newIssue.title, Description = newIssue.description}; // <2>

await _store.CreateAsync(issue); // <3>

var response = Request.CreateResponse(HttpStatusCode.Created); // <4>

response.Headers.Location = _linkFactory.Self(issue.Id).Href; // <5>

return response; // <6>.

}

The code works as follows:

§ The method itself is named Post in order to match the POST HTTP method <1>. Similarly to the client in test, this method accepts dynamic. On the server, Json.NET will create a JObject instance automatically if it sees dynamic. Though JSON is supported by default, we could add custom formatters for supporting alternative media types like application/x-www-form-urlencoded.

§ We create a new issue by passing the properties from the dynamic instance <2>.

§ The CreateAsync method is invoked on the store to store the issue <3>.

§ The response is created to return a 201 Created status <4>.

§ We set the location header on the response by invoking the Self method of the _linkFactory <5>, and the response is returned <6>.

This covers creation; next, on to updating!

Feature: Updating Issues

This feature covers updating issues using HTTP PATCH. PATCH was chosen because it allows the client to send partial data that will modify the existing resource. PUT, on the other hand, completely replaces the state of the resource.

Updating an Issue

This scenario verifies that when a client sends a PATCH request, the corresponding resource is updated:

Scenario: Updating an issue

Given an existing issue

When a PATCH request is made

Then a '200 OK' is returned

Then the issue should be updated

The test for this scenario is shown in Example 7-26.

Example 7-26. IssueController PATCH method

[Scenario]

public void UpdatingAnIssue(Issue fakeIssue)

{

"Given an existing issue".

f(() =>

{

fakeIssue = FakeIssues.FirstOrDefault();

MockIssueStore.Setup(i => i.FindAsync("1")).Returns(

Task.FromResult(fakeIssue)); // <1>

MockIssueStore.Setup(i => i.UpdateAsync(It.IsAny<Issue>())).

Returns(Task.FromResult(""));

});

"When a PATCH request is made".

f(() =>

{

dynamic issue = new JObject(); // <2>

issue.description = "Updated description";

Request.Method = new HttpMethod("PATCH"); // <3>

Request.RequestUri = _uriIssue1;

Request.Content = new ObjectContent<dynamic>(issue,

new JsonMediaTypeFormatter()); // <4>

Response = Client.SendAsync(Request).Result;

});

"Then a '200 OK' status is returned".

f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.OK)); // <5>

"Then the issue should be updated".

f(() => MockIssueStore.Verify(i =>

i.UpdateAsync(It.IsAny<Issue>()))); // <6>

"Then the descripton should be updated".

f(() => fakeIssue.Description.ShouldEqual("Updated description")); // <7>

"Then the title should not change".

f(() => fakeIssue.Title.ShouldEqual(title)); // <8>

}

Here’s how the tests work:

§ Sets up the mock store to return the expected issue that will be updated when FindAsync is called and to handle the call to UpdateAsync <1>.

§ News up a JObject instance, and only the description to be changed is set <2>.

§ Sets the request method to PATCH <3>. Notice here an HttpMethod instance is constructed, passing in the method name. This is the approach to use when you are using an HTTP method that does not have a predefined static property off the HttpMethod class, such as GET, PUT, POST, and DELETE.

§ News up an ObjectContent<dynamic> instance with the issue and sets it to the request content. The request is then sent <4>. Notice the usage of dynamic: it works well for PATCH because it allows the client to just send the properties of the issue that it wants to update.

§ Validates that the status code is 200 OK <5>.

§ Validates that the UpdateAsync method was called, passing the issue <6>.

§ Validates that the description of the issue was updated <7>.

§ Validates that the title has not changed <8>.

The implementation is handled in the Patch method of the controller, as Example 7-27 demonstrates.

Example 7-27. IssueController Patch method

public async Task<HttpResponseMessage> Patch(string id, dynamic issueUpdate) // <1>

{

var issue = await _store.FindAsync(id); // <2>

if (issue == null) // <3>

return Request.CreateResponse(HttpStatusCode.NotFound);

foreach (JProperty prop in issueUpdate) // <4>

{

if (prop.Name == "title")

issue.Title = prop.Value.ToObject<string>();

else if (prop.Name == "description")

issue.Description = prop.Value.ToObject<string>();

}

await _store.UpdateAsync(issue); // <5>

return Request.CreateResponse(HttpStatusCode.OK); // <6>

}

Here’s what the code does:

§ The method accepts two parameters <1>. The id comes from the URI (http://localhost/issue/1, in this case) of the request. The issueUpdate, however, comes from the JSON content of the request.

§ The issue to be updated is retrieved from the store <2>.

§ If no issue is found, a 404 Not Found is immediately returned <3>.

§ A loop walks through the properties of issueUpdate, updating only those properties that are present <4>.

§ The store is invoked to update the issue <5>.

§ A 200 OK status is returned <6>.

Updating an Issue That Does Not Exist

This scenario ensures that when a client sends a PATCH request for a missing or deleted issue, a 404 Not Found status is returned:

Scenario: Updating an issue that does not exist

Given an issue does not exist

When a PATCH request is made

Then a '404 Not Found' status is returned

We’ve already seen the code for this in the controller in the previous section, but the test in Example 7-28 verifies that it actually works!

Example 7-28. Updating an issue that does not exist

[Scenario]

public void UpdatingAnIssueThatDoesNotExist()

{

"Given an issue does not exist".

f(() => MockIssueStore.Setup(i => i.FindAsync("1")).

Returns(Task.FromResult((Issue)null))); // <1>

"When a PATCH request is made".

f(() =>

{

Request.Method = new HttpMethod("PATCH"); // <2>

Request.RequestUri = _uriIssue1;

Request.Content = new ObjectContent<dynamic>(new JObject(),

new JsonMediaTypeFormatter()); // <3>

response = Client.SendAsync(Request).Result; // <4>

});

"Then a 404 Not Found status is returned".

f(() => response.StatusCode.ShouldEqual(HttpStatusCode.NotFound)); // <5>

}

Here’s how the tests work:

§ Sets up the mock store to return a null issue when FindAsync is called.

§ Sets the request method to PATCH <2>.

§ Sets the content to an empty JObject instance. The content here really doesn’t matter <3>.

§ Sends the request <4>.

§ Validates that the 404 Not Found status is returned.

This completes the section on updates.

Feature: Deleting Issues

This feature covers handling of HTTP DELETE requests for removing issues.

Deleting an Issue

This scenario verifies that when a client sends a DELETE request, the corresponding issue is removed:

Scenario: Deleting an issue

Give an existing issue

When a DELETE request is made

Then a '200 OK' status is returned

Then the issue should be removed

The tests (Example 7-29) for this scenario are very straightforward, using concepts already covered throughout the chapter.

Example 7-29. Deleting an issue

[Scenario]

public void DeletingAnIssue(Issue fakeIssue)

{

"Given an existing issue".

f(() =>

{

fakeIssue = FakeIssues.FirstOrDefault();

MockIssueStore.Setup(i => i.FindAsync("1")).Returns(

Task.FromResult(fakeIssue)); // <1>

MockIssueStore.Setup(i => i.DeleteAsync("1")).Returns(

Task.FromResult(""));

});

"When a DELETE request is made".

f(() =>

{

Request.RequestUri = _uriIssue;

Request.Method = HttpMethod.Delete; // <2>

Response = Client.SendAsync(Request).Result; // <3>

});

"Then the issue should be removed".

f(() => MockIssueStore.Verify(i => i.DeleteAsync("1"))); // <4>

"Then a '200 OK status' is returned".

f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.OK)); // <5>

}

Here’s how the tests work:

§ Configures the mock issue store to return the issue to be deleted when FindAsync is called, and to handle the DeleteAsync call <1>.

§ Sets the request to use DELETE <2> and sends it <3>.

§ Validates that the DeleteAsync method was called, passing in the Id <4>.

§ Validates that the response is a 200 OK <5>.

The implementation can be seen in Example 7-30.

Example 7-30. IssueController Delete method

public async Task<HttpResponseMessage> Delete(string id) // <1>

{

var issue = await _store.FindAsync(id); // <2>

if (issue == null)

return Request.CreateResponse(HttpStatusCode.NotFound); // <3>

await _store.DeleteAsync(id); // <4>

return Request.CreateResponse(HttpStatusCode.OK); // <5>

}

The code does the following:

§ The method name is Delete to match against an HTTP DELETE <1>. It accepts the id of the issue to be deleted.

§ The issue is retrieved from the store for the selected id <2>.

§ If the issue does not exist, a 404 Not Found status is returned <3>.

§ The DeleteAsync method is invoked on the store to remove the issue <4>.

§ A 200 OK is returned to the client <5>.

Deleting an Issue That Does Not Exist

This scenario verifies that if a client sends a DELETE request for a nonexistent issue, a 404 Not Found status is returned:

Scenario: Deleting an issue that does not exist

Given an issue does not exist

When a DELETE request is made

Then a '404 Not Found' status is returned

The test in Example 7-31 is very similar to the previous test for updating a missing issue.

Example 7-31. Deleting an issue that does not exist

[Scenario]

public void DeletingAnIssueThatDoesNotExist()

{

"Given an issue does not exist".

f(() => MockIssueStore.Setup(i => i.FindAsync("1")).Returns(

Task.FromResult((Issue) null))); // <1>

"When a DELETE request is made".

f(() =>

{

Request.RequestUri = _uriIssue;

Request.Method = HttpMethod.Delete; // <2>

Response = Client.SendAsync(Request).Result;

});

"Then a '404 Not Found' status is returned".

f(() => Response.StatusCode.ShouldEqual(HttpStatusCode.NotFound)); // <3>

}

Here’s how the tests work:

§ Sets up the mock store to return null when the issue is requested <1>.

§ Sends the request to delete the resource <2>.

§ Validates that a 404 Not Found is returned <3>.

Feature: Processing Issues

The Tests

As mentioned earlier, discussing the tests for this feature is beyond the scope of this chapter. However, you now have all the concepts necessary to understand the code, which can be found in the GitHub repo.

Separating out processing resources provides better separation for the API implementation, making the code more readable and easier to maintain. It also helps with evolvabililty, as you can make changes to handle processing without needing to touch the IssueController, which is also fulfilling the Single Responsibility Principle.

The Implementation

The issue processor resources are backed by the IssueProcessorController shown in Example 7-32.

Example 7-32. IssueProcessorController

public class IssueProcessorController : ApiController

{

private readonly IIssueStore _issueStore;

public IssueProcessorController(IIssueStore issueStore)

{

_issueStore = issueStore; // <1>

}

public async Task<HttpResponseMessage> Post(string id, string action) // <2>

{

bool isValid = IsValidAction(action); // <3>

Issue issue = null;

if (isValid)

{

issue = await _issueStore.FindAsync(id); // <4>

if (issue == null)

return Request.CreateResponse(HttpStatusCode.NotFound); // <5>

if ((action == IssueLinkFactory.Actions.Open ||

action == IssueLinkFactory.Actions.Transition) &&

issue.Status == IssueStatus.Closed)

{

issue.Status = IssueStatus.Open; // <6>

}

else if ((action == IssueLinkFactory.Actions.Close ||

action == IssueLinkFactory.Actions.Transition) &&

issue.Status == IssueStatus.Open)

{

issue.Status = IssueStatus.Closed; // <7>

}

else

isValid = false; // <8>

}

if (!isValid)

return Request.CreateErrorResponse(HttpStatusCode.BadRequest,

string.Format("Action '{0}' is invalid", action)); // <9>

await _issueStore.UpdateAsync(issue); // <10>

return Request.CreateResponse(HttpStatusCode.OK); // <11>

}

public bool IsValidAction(string action)

{

return (action == IssueLinkFactory.Actions.Close ||

action == IssueLinkFactory.Actions.Open ||

action == IssueLinkFactory.Actions.Transition);

}

}

Here’s how the code works:

§ The IssueProcessorController accepts an IIssueStore in its constructor similar to the IssueController <1>.

§ The method is Post and accepts the id and action from the request URI <2>.

§ The IsValidAction method is called to check if the action is recognized <3>.

§ The FindAsync method is invoked to retrive the issue <4>.

§ If the issue is not found, then a 400 Not Found is immediately returned <5>.

§ If the action is open or transition and the issue is closed, the issue is opened <6>.

§ If the action is close or transition and the issue is open, the issue is closed <7>.

§ If neither clause matched, the action is flagged as invalid for the current state <8>.

§ If the action is invalid, then an error is returned via CreateErrorResponse. This method is used because we want an error response that contains a payload <9>.

§ We update the issue by calling UpdateAsync <10>, and a 200 OK status is returned <11>.

This completes coverage of the Issue Tracker API!

Conclusion

This chapter covered a lot of ground. We went from the high-level design of the system to the detailed requirements of the API and the actual implementation. Along the way, we learned about many aspects of Web API in practice, as well as how to do integration testing with in-memory hosting. These concepts are a big part of your journey toward building evolvable APIs with ASP.NET. Now the fun stuff starts! In the next chapter, you’ll see how to harden up that API and the tools that are necessary to really allow it to scale, like caching.