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.
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.
The following steps show you how to use content negotiation as well as model binding in your Nancy applications:
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]); }
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 theAccept
header to indicateapplication/json
. This test should pass without changes to the production code.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 theAccept
header on theGET
request toapplication/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]); }
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();
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.
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 eachGet
call to"/todos/"
. I will leave this as an exercise for you.We also want the
Post
handler inTodosModule
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); }
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 newtodo
object and set theHTTPStatusCode
to201 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); };
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, yourTodosModule
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; }; } }
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.First, add the
protobuf-net NuGet
package to both theTodosNancy
andTodosNancyTests
project.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]); }
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.
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 theIResponseProcessor
andIBodyDeserializer
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); } }
Now the test should pass and our application supports three formats—JSON, XML, and protocol buffers—both in and out.
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.