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.

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 IReadDocument interface. 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>

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 corresponding IssueState 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 the IssueStateFactory.

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 and ShouldNotBeNull 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.

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+Json responses.

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 a Task.
  • 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 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.

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.