Book Image

Instant Nancy Web Development

By : Christian Horsdal
Book Image

Instant Nancy Web Development

By: Christian Horsdal

Overview of this book

Nancy is a C# web framework which aims to provide you ,the application developer, with an easy path to follow, where things will naturally fall into place and work. Nancy is a powerful, flexible, and lightweight web framework that allows you to get on with your job. Instant Nancy Web Development will give Readers practical introduction to building, testing, and deploying web applications with Nancy. You will learn how to take full advantage of Nancy to build clean application code, and will see how this code lends itself nicely to test driven development. You will also learn how to hook into Nancy to easily extend the framework. Instant Nancy Web Development offers you an in-depth exploration of all the major features of the Nancy web framework, from basic routing to deployment in the Cloud, and from model binding to automated tests. You will learn how to build web applications with Nancy and explore how to build web sites using Razor views. Next, you will learn how to build web based APIs suitable for JavaScript clients, mobile clients, and even desktop applications. In fact, you will learn how to easily combine the two into one. Finally, you will learn how to leverage Nancy to write clean and maintainable web applications quickly.
Table of Contents (7 chapters)

Routes and model binding (Intermediate)


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.

Getting ready

This section builds on the previous section and assumes you have the TodoNancy and TodoNancyTests projects all set up.

How to do it...

The following steps will help you to handle the other HTTP verbs and work with dynamic routes:

  1. Open the TodoNancy Visual Studio solution.

  2. Add a new class to the NancyTodoTests project, call it TodosModulesTests, and fill this test code for a GET and a POST 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);
        }
      }
  3. The main thing to notice new in these tests is the use of actual.Body.DesrializeJson<Todo[]>(), which takes the Body property of the BrowserResponse type, assumes it contains JSON formatted text, and then deserializes that string into an array of Todo objects.

  4. At the moment, these tests will not compile. To fix this, add this Todo class to the TodoNancy 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; }
      }
  5. Then, go to the TodoNancy project, and add a new C# file, call it TodosModule, and add the following code to body of the new class:

    public static Dictionary<long, Todo> store = new Dictionary<long, Todo>();
  6. 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);
          };
        }
  7. 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. The GET handler returns a list of todo items as a JSON array. The POST handler allows for creating new todos. Re-run the tests and watch them succeed.

  8. 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 the GET HTTP. This consistency extends to the other HTTP verbs too. Secondly, note that we pass the "todos"string to the base constructor. This tells Nancy that all routes in this module are related to /todos. Thirdly, notice the this.Bind<Todo>() call, which is Nancy's data binding in action; it deserializes the body of the POST HTTP into a Todo object.

  9. Now go back to the TodosModuleTests class and add these tests for the PUT and DELETE 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[]>());
    }
  10. 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;
          };
  11. All tests should now pass.

  12. Take a look at the routes to the new handlers for the PUT and DELETE 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 is 42. Notice that both these new handlers use their p argument to get the ID from the route in the p.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 is DynamicDictionary, which is a special Nancy type that lets you look up parts via either indexers (for example, p["id"]) like a dictionary, or dot notation (for example, p.id) like other dynamic C# objects.

There's more...

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.