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:
- There is a backend system (such as GitHub) that manages issues.
-
The
Issue collection
resource retrieves items from the backend. It returns a response in either theIssue+Json
orCollection+Json
formats. This resource can also be used for creating new issues via an HTTPPOST
. -
The
Issue item
resources contain representations of a single issue from the backend system. Issues can be updated viaPATCH
or deleted via aDELETE
request. 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
toClosed
)
-
-
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.
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.
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.
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.
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.
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.
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 correspondingCollection+Json
Item
instances<5>
and setting theData
<6>
andLinks
<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.
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.
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 anIssueState
instance and copies over the data from theIssue
<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.
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 aUrlHelper
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 theGetLink
method.<4>
-
The
GetUri
method uses theUrlHelper
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
.
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 theIssueController
<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
Rel
s<3>
andAction
s<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 specificRel
. -
The factory includes
Transition
<6>
,Open
<7>
, andClose
<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.
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 theConfigure
method<3>
. -
Sets the
Client
property to a newHttpClient
instance, passing theHttpServer
in the constructor<4>
.
Example 7-10 demonstrates the 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 theInMemoryStore
. -
Registers the remaining services
<6>
. -
Wires up Autofac to inject the current
HttpRequestMessage
as a dependency<7>
. This enables services such as theIssueLinkFactory
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.
[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>
, andstatus
<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.
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).
[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"
);
});
}
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.
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.
[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 theTask.FromResult
extension is used to easily create aTask
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.
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.
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>
. TheReadAsAsync
method uses the formatter associated with theHttpContent
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).
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).
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.
[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 toapplication/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.
[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.
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 HTTPGET
. The parameter of the method matches against the query string paramsearchtext
. -
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.
[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 toTask.FromResult
. TheCreateAsync
method expects aTask
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 likeIssue
, it uses aJObject
instance (from Json.NET) cast todynamic
. 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.
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 thePOST
HTTP method<1>
. Similarly to the client intest
, this method acceptsdynamic
. On the server, Json.NET will create aJObject
instance automatically if it seesdynamic
. Though JSON is supported by default, we could add custom formatters for supporting alternative media types likeapplication/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>
.
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.
[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 toUpdateAsync
<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 anHttpMethod
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 theHttpMethod
class, such asGET
,PUT
,POST
, andDELETE
. -
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 ofdynamic
: it works well forPATCH
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.
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>
. Theid
comes from the URI (http://localhost/issue/1, in this case) of the request. TheissueUpdate
, 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!
[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.
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.
[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 theDeleteAsync
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.
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 HTTPDELETE
<1>
. It accepts theid
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.
[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:
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.
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 anIIssueStore
in its constructor similar to theIssueController
<1>
. -
The method is
Post
and accepts theid
andaction
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
ortransition
and the issue is closed, the issue is opened<6>
. -
If the action is
close
ortransition
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 a200 OK
status is returned<11>
.
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.