Starting from this recipe, we will slightly speed things up by leaving out details of Visual Studio usage, and in some cases only code snippets will be shown instead of full classes. It should be clear from the concepts where the snippets fit, and they can be further investigated in the code downloads for individual recipes.
For the sake of brevity, I will also show several tests in just one step and also the necessary production code in one go. Though, I strongly recommend that you stick to good TDD practices, and add the tests one at a time and make each one pass before moving on to the next.
In this recipe, we take a look at how to handle the other HTTP verbs apart from GET
and how to work with dynamic routes such as /custumer/42
, where 42
is the customer ID. We will also look at how to work with JSON data and how to do model binding.
In this recipe, we will see the Todo
application, which is the running example of the book, take shape. In fact at the end of this recipe, you will have a backend—if hooked up correctly— that works with the canonical JavaScript library todo
samples. The downloadable code for this recipe is attached with the backbone.js
todo sample.
This section builds on the previous section and assumes you have the TodoNancy
and TodoNancyTests
projects all set up.
The following steps will help you to handle the other HTTP verbs and work with dynamic routes:
Open the
TodoNancy
Visual Studio solution.Add a new class to the
NancyTodoTests
project, call itTodosModulesTests
, and fill this test code for aGET
and aPOST
route into it:public class TodosModuleTests { private Browser sut; private Todo aTodo; private Todo anEditedTodo; public TodosModuleTests() { TodosModule.store.Clear(); sut = new Browser(new DefaultNancyBootstrapper()); aTodo = new Todo { title = "task 1", order = 0, completed = false }; anEditedTodo = new Todo() { id = 42, title = "edited name", order = 0, completed = false }; } [Fact] public void Should_return_empty_list_on_get_when_no_todos_have_been_posted() { var actual = sut.Get("/todos/"); Assert.Equal(HttpStatusCode.OK, actual.StatusCode); Assert.Empty(actual.Body.DeserializeJson<Todo[]>()); } [Fact] public void Should_return_201_create_when_a_todo_is_posted() { var actual = sut.Post("/todos/", with => with.JsonBody(aTodo)); Assert.Equal(HttpStatusCode.Created, actual.StatusCode); } [Fact] public void Should_not_accept_posting_to_with_duplicate_id() { var actual = sut.Post("/todos/", with => with.JsonBody(anEditedTodo)) .Then .Post("/todos/", with => with.JsonBody(anEditedTodo)); Assert.Equal(HttpStatusCode.NotAcceptable, actual.StatusCode); } [Fact] public void Should_be_able_to_get_posted_todo() { var actual = sut.Post("/todos/", with => with.JsonBody(aTodo) ) .Then .Get("/todos/"); var actualBody = actual.Body.DeserializeJson<Todo[]>(); Assert.Equal(1, actualBody.Length); AssertAreSame(aTodo, actualBody[0]); } private void AssertAreSame(Todo expected, Todo actual) { Assert.Equal(expected.title, actual.title); Assert.Equal(expected.order, actual.order); Assert.Equal(expected.completed, actual.completed); } }
The main thing to notice new in these tests is the use of
actual.Body.DesrializeJson<Todo[]>()
, which takes theBody
property of theBrowserResponse
type, assumes it contains JSON formatted text, and then deserializes that string into an array ofTodo
objects.At the moment, these tests will not compile. To fix this, add this
Todo
class to theTodoNancy
project as follows:public class Todo { public long id { get; set; } public string title { get; set; } public int order { get; set; } public bool completed { get; set; } }
Then, go to the
TodoNancy
project, and add a new C# file, call itTodosModule
, and add the following code to body of the new class:public static Dictionary<long, Todo> store = new Dictionary<long, Todo>();
Run the tests and watch them fail. Then add the following code to
TodosModule
:public TodosModule() : base("todos") { Get["/"] = _ => Response.AsJson(store.Values); Post["/"] = _ => { var newTodo = this.Bind<Todo>(); if (newTodo.id == 0) newTodo.id = store.Count + 1; if (store.ContainsKey(newTodo.id)) return HttpStatusCode.NotAcceptable; store.Add(newTodo.id, newTodo); return Response.AsJson(newTodo) .WithStatusCode(HttpStatusCode.Created); }; }
The previous code adds two new handlers to our application. One handler for the GET
"/todos/"
HTTP and the other handler for the POST"/todos/"
HTTP. TheGET
handler returns a list oftodo
items as a JSON array. ThePOST
handler allows for creating newtodos
. Re-run the tests and watch them succeed.Now let's take a closer look at the code. Firstly, note how adding a handler for the
POST
HTTP is similar to adding handlers for theGET
HTTP. This consistency extends to the other HTTP verbs too. Secondly, note that we pass the"todos"
string to thebase
constructor. This tells Nancy that all routes in this module are related to/todos
. Thirdly, notice thethis.Bind<Todo>()
call, which is Nancy's data binding in action; it deserializes the body of thePOST
HTTP into aTodo
object.Now go back to the
TodosModuleTests
class and add these tests for thePUT
andDELETE
HTTP as follows:[Fact] public void Should_be_able_to_edit_todo_with_put() { var actual = sut.Post("/todos/", with => with.JsonBody(aTodo)) .Then .Put("/todos/1", with => with.JsonBody(anEditedTodo)) .Then .Get("/todos/"); var actualBody = actual.Body.DeserializeJson<Todo[]>(); Assert.Equal(1, actualBody.Length); AssertAreSame(anEditedTodo, actualBody[0]); } [Fact] public void Should_be_able_to_delete_todo_with_delete() { var actual = sut.Post("/todos/", with => with.Body(aTodo.ToJSON())) .Then .Delete("/todos/1") .Then .Get("/todos/"); Assert.Equal(HttpStatusCode.OK, actual.StatusCode); Assert.Empty(actual.Body.DeserializeJson<Todo[]>()); }
After watching these tests fail, make them pass by adding this code to the constructor of
TodosModule
:Put["/{id}"] = p => { if (!store.ContainsKey(p.id)) return HttpStatusCode.NotFound; var updatedTodo = this.Bind<Todo>(); store[p.id] = updatedTodo; return Response.AsJson(updatedTodo); }; Delete["/{id}"] = p => { if (!store.ContainsKey(p.id)) return HttpStatusCode.NotFound; store.Remove(p.id); return HttpStatusCode.OK; };
All tests should now pass.
Take a look at the routes to the new handlers for the
PUT
andDELETE
HTTP. Both are defined as"/{id}"
. This will match any route that starts with/todos/
and then something more that appears after the trailing/
, such as/todos/42
and the{id}
part of the route definition is42
. Notice that both these new handlers use theirp
argument to get the ID from the route in thep.id
expression. Nancy lets you define very flexible routes. You can use any regular expression to define a route. All named parts of such regular expressions are put into the argument for the handler. The type of this argument isDynamicDictionary
, which is a special Nancy type that lets you look up parts via eitherindexers
(for example,p["id"]
) like a dictionary, or dot notation (for example,p.id
) like other dynamic C# objects.
In addition to the handlers for GET
, POST
, PUT
, and DELETE
, which we added in this recipe, we can go ahead and add handler for PATCH
and OPTIONS
by following the exact same pattern.
Out of the box, Nancy automatically supports HEAD
and OPTIONS
for you. To handle the HEAD
HTTP request, Nancy will run the corresponding GET
handler but only return the headers. To handle OPTIONS
, Nancy will inspect which routes you have defined and respond accordingly.