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)

Content negotiation and more model binding (Advanced)


This recipe shows you how to do content negotiation with Nancy. Content negotiation is the mechanism used in HTTP to decide which content type—for example, JSON or XML—to use in response to a request. This involves the client indicating in HTTP Accept headers on the requests as to which content type it prefers and which other ones it can also accept. A browser, for instance, can indicate that it prefers HTML but can also accept X-HTML or XML, whereas a JavaScript application will probably prefer JSON and a mobile app might prefer XML. The server side reads the HTTP Accept headers and chooses the most suitable content type it is capable of responding with. Nancy can handle the server side of this process for you automatically. Out of the box, Nancy is capable of returning JSON, XML, and, as we will see in the next recipe, HTML. On top of this, Nancy makes it easy to plug your own content types to the automatic content negotiation, which we will see towards the end of this recipe where we add support for protocol buffers to our application.

Along the way, we will also make sure that our application accepts the same formats as inputs; that is, the model binding we added in the Routes and model binding (Intermediate) recipe works with JSON, XML, and finally protocol buffers.

Getting ready

This recipe builds on the previous recipe, so to follow along you need either your own version of the code from the Taking a dependency – introducing the bootstrapper (Intermediate) recipe or the one from the code download.

How to do it...

The following steps show you how to use content negotiation as well as model binding in your Nancy applications:

  1. First, we add tests to demonstrate that the model binding already supports XML. Add the following test to TodoModuleTests:

        [Fact]
        public void Should_be_able_to_get_posted_xml_todo()
        {
          var actual = sut.Post("/todos/", with =>
          {
            with.XMLBody(aTodo);
            with.Accept("application/xml");
          })
          .Then
          .Get("/todos/", with => with.Accept("application/json"));
    
          var actualBody = actual.Body.DeserializeJson<Todo[]>();
          Assert.Equal(1, actualBody.Length);
          Assertions.AreSame(aTodo, actualBody[0]);
        }
  2. This test is very close to a test we already have in the test suite except it performs the action of POSTing a todo object as XML instead of a JSON. It also explicitly sets the Accept header to indicate application/json. This test should pass without changes to the production code.

  3. The next step is to add a test for getting back the response from a GET /todos/ HTTP as XML. You can modify the test you just added or you can add this new test—just make sure to set the Accept header on the GET request to application/xml and use XML deserialization to read the response as shown in the following code snippet:

        [Fact]
        public void Should_be_able_to_get_posted_todo_as_xml()
        {
          var actual = sut.Post("/todos/", with =>
          {
            with.XMLBody(aTodo);
            with.Accept("application/xml");
          })
          .Then
          .Get("/todos/", with => with.Accept("application/xml"));
    
          var actualBody = actual.Body.DeserializeXml<Todo[]>();
          Assert.Equal(1, actualBody.Length);
          Assertions.AreSame(aTodo, actualBody[0]);
        }
  4. This test should fail, but to make the test pass, we actually only have to remove a bit of code from our existing Get handler; so, it becomes as follows:

          Get["/"] = _ => todoStore.GetAll();
  5. When a handler returns an object that is not a Nancy type, Nancy will assume that it is supposed to be serialized into the body of the response and will decide how to do so based on content negotiation.

  6. If you re-run all the tests at this point, you will see most of them fail. This is because they do not indicate which content type they accept in responses from our application. To fix this, you will have to go to the tests and add with.Accept("application/xml") to each Get call to "/todos/". I will leave this as an exercise for you.

  7. We also want the Post handler in TodosModule to use content negotiation, so we add a test to make it capable of returning XML as follows:

        [Fact]
        public void Should_return_created_todo_as_xml_when_a_todo_is_posted()
        {
          var actual = sut.Post("/todos/", 
            with =>
            {
              with.JsonBody(aTodo);
              with.Accept("application/xml");
            });
    
          var actualBody = actual.Body.DeserializeXml<Todo>();
          Assertions.AreSame(aTodo, actualBody);
        }
  8. This should fail, but to make it pass, we simply need to change the Post handler to the following, which tells Nancy to use content negotiation to decide how to serialize the new todo object and set the HTTPStatusCode to 201 Created as shown in the following code snippet:

          Post["/"] = _ =>
          {
            var newTodo = this.Bind<Todo>();
            if (newTodo.id == 0)
              newTodo.id = todoStore.Count + 1;
    
            if (!todoStore.TryAdd(newTodo))
              return HttpStatusCode.NotAcceptable;
    
            return Negotiate.WithModel(newTodo)
              .WithStatusCode(HttpStatusCode.Created);
          };
  9. I will leave it as an exercise for you to add similar tests for Put and for all the combinations of JSON and XML. At the end of this exercise, your TodosModule should look like the following code:

      public class TodosModule : NancyModule
      {
        public TodosModule(IDataStore todoStore) : base("todos")
        {
          Get["/"] = _ => todoStore.GetAll();
    
          Post["/"] = _ =>
          {
            var newTodo = this.Bind<Todo>();
            if (newTodo.id == 0)
              newTodo.id = todoStore.Count + 1;
    
            if (!todoStore.TryAdd(newTodo))
              return HttpStatusCode.NotAcceptable;
    
            return Negotiate.WithModel(newTodo)
              .WithStatusCode(HttpStatusCode.Created);
          };
    
          Put["/{id}"] = p =>
          {
            var updatedTodo = this.Bind<Todo>();
            if (!todoStore.TryUpdate(updatedTodo))
              return HttpStatusCode.NotFound;
    
            return updatedTodo;
          };
    
          Delete["/{id}"] = p =>
          {
            if (!todoStore.TryRmove(p.id))
              return HttpStatusCode.NotFound;
    
            return HttpStatusCode.OK;
          };
        }
      }
  10. So far so good, but now we also want to send todos to our application and get them back again using the highly efficient protocol buffer format. The tests for this are similar to the ones you have just added; now, we only need to use the "application/x-protobuf"content type and use a protocol buffer's serializer and deserializer.

  11. First, add the protobuf-net NuGet package to both the TodosNancy and TodosNancyTests project.

  12. Then add the following test to TodoModuleTests:

        [Fact]
        public void Should_be_able_to_get_posted_todo_as_protobuf()
        {
          var actual = sut.Post("/todos/", with =>
          {
            var stream = new MemoryStream();
            Serializer.Serialize(stream, aTodo);
            with.Body(stream, "application/x-protobuf");
             with.Accept("application/xml");
          })
          .Then
          .Get("/todos/", with => with.Accept("application/x-protobuf"));
    
          var actualBody = Serializer.Deserialize<Todo[]>(actual.Body.AsStream());
          Assert.Equal(1, actualBody.Length);
          Assertions.AreSame(aTodo, actualBody[0]);
        }
  13. To no surprise, this test fails because we have nothing in our application that can deserialize protocol buffers from an HTTP body or that can serialize an object as protocol buffers into an HTTP body.

  14. In order to make the test pass, we add the following two classes to TodosNancy: The first class hooks into Nancy's content negotiation and adds support for responding with protocol buffers. The second class hooks into Nancy's model binding and adds support for deserializing protocol buffers from the body of a request into a model object. The key is to implement the IResponseProcessor and IBodyDeserializer interfaces respectively:

      public class ProtoBufProcessor : IResponseProcessor
      {
        public ProcessorMatch CanProcess(MediaRange requestedMediaRange, dynamic model, NancyContext context)
        {
          if (requestedMediaRange.Matches(MediaRange.FromString("application/x-protobuf")))
            return new ProcessorMatch { ModelResult = MatchResult.DontCare,  RequestedContentTypeResult = MatchResult.ExactMatch};
    
          if (requestedMediaRange.Subtype.ToString().EndsWith("protobuf"))
            return new ProcessorMatch { ModelResult = MatchResult.DontCare, RequestedContentTypeResult = MatchResult.NonExactMatch };
    
          return new ProcessorMatch { ModelResult = MatchResult.DontCare, RequestedContentTypeResult = MatchResult.NoMatch };
        }
    
        public Response Process(MediaRange requestedMediaRange, dynamic model, NancyContext context)
        {
          return new Response
          {
            Contents = stream => Serializer.Serialize(stream, model),
            ContentType = "application/x-protobuf"
          };
        }
    
        public IEnumerable<Tuple<string, MediaRange>> ExtensionMappings
        {
          get { return new[] { new Tuple<string, MediaRange>(".protobuf", MediaRange.FromString("application/x-protobuf")) }; }
        }
      }
    
      public class ProtobufBodyDeserializer : IBodyDeserializer
      {
        public bool CanDeserialize(string contentType)
        {
          return contentType == "application/x-protobuf";
        }
        public object Deserialize(string contentType, Stream bodyStream, BindingContext context)
        {
          return Serializer.NonGeneric.Deserialize(context.DestinationType, bodyStream);
        }
      }
  15. Now the test should pass and our application supports three formats—JSON, XML, and protocol buffers—both in and out.

How it works...

Looking at the changes we made in the previous section to the TodosModule class, we see that they are quite small but have a large impact; we went from supporting one format to as many formats as are hooked into Nancy. This is because Nancy handles all the logic involved in content negotiation for us and likewise handles all the logic around picking a deserializer for model binding. In other words, Nancy handles all the looking at HTTP headers and matching them up against registered serializers and deserializers.

In that last part, we saw how amazingly easy it is to add another format to these processes. Note that we made no changes to the TodosModule class whatsoever while adding the support for protocol buffers. All we had to do was to add two classes, each one implementing a rather simple interface. This works because Nancy scans all loaded assemblies during start up and finds all implementations of these interfaces. Nancy can then probe each found implementation in order to find out which content types it can handle; this is what the ProtoBufProcessor.CanProcess, ProtoBufProcessor.ExtensionMappings methods and the ProtobufBodyDeserializer.CanDeserialize methods are for. The information about which content types are handled is returned by these methods and is used by Nancy during model binding and during content negation. If Nancy decides to use the ProtoBufProcessor.Process protocol buffers, ProtobufBodyDeserializer.Deserialize is called.