Enterprise Integration Zone is brought to you in partnership with:

Just another Kiwi (New Zealander) working and traveling around the world as a .NET Developer. Love technology and turned my hobby into my career. No claims to fame, but working on my own project called ITCompiler to hopefully help the IT community. Phillip has posted 17 posts at DZone. You can read more from them at their website. View Full User Profile

NancyFX - Revisiting Content Negotiation & APIs (Part 3)

05.21.2013
| 1396 views |
  • submit to reddit

In this last part I want to show you creating your own media type, so far we have gone over a really basic example of returning a response and letting the accept header handle different results, and then we went over how explicitly using Negotiate allows you to customize your response more.

Creating your own Media Type may be something you never have to do, but it can help you solve some strange scenarios, for instance you could have a user request an invoice, rather than clutter your code with:

if (requestedInvoice.Type == "pdf")
{
    // get a pdf invoice
}
else if (requestedInvoice.Type == "word")
{

}
else if (...)

You get the picture, this is tedious problematic work, every time we want to supply a new invoice type we need to modify the existing request, what we want to do is just handle a new media type and process it if we know about it.

Note: To keep this simple I’m going to show a simple CSV Processor, you can look at NancyFX code on how a ViewProcessor works if you want to hook up a View to a Word/PDF processor.

Codez is what I need!

There’s 3 things we need to create

  1. CsvProcessor
  2. CsvSerializer
  3. CsvResponse

The CsvProcessor takes the request, checks to see if it can handle the request, and if it can, it uses the CsvSerializer to process the request and return a CsvResponse

Csv Response

This class is modeled off the Json Response, it takes a model and a serializer, sets all the correct headers for the response, serializes the response, and this is what is returned to the client.

public class CsvResponse<TModel> : Response
{
    public CsvResponse(TModel model, ISerializer serializer)
    {
        if (serializer == null)
        {
            throw new InvalidOperationException("CSV Serializer not set");
        }

        this.Contents = GetJsonContents(model, serializer);
        this.ContentType = "text/csv";
        this.StatusCode = HttpStatusCode.OK;
    }

    private static Action<Stream> GetJsonContents(TModel model, ISerializer serializer)
    {
        return stream => serializer.Serialize("text/csv", model, stream);
    }
}

public class CsvResponse : CsvResponse<object>
{
    public CsvResponse(object model, ISerializer serializer)
        : base(model, serializer)
    {
    }
}

You don’t REALLY need this class, I would say its a highly recommended optional. If you don’t create this class it just means your processor needs to do all the work of setting up the response.

Csv Serializer

To handle serialization of the model to a CSV file I’m using ServiceStack.Text which comes with its own serializer!

public class CsvSerializer : ISerializer
{
    public bool CanSerialize(string contentType)
    {
        return IsCsvType(contentType);
    }

    public IEnumerable<string> Extensions
    {
        get { yield return "csv"; }
    }

    public void Serialize<TModel>(string contentType, TModel model, Stream outputStream)
    {
        using (var writer = new StreamWriter(new UnclosableStreamWrapper(outputStream)))
        {
            ServiceStack.Text.CsvSerializer.SerializeToWriter(model, writer);
        }
    }

    private static bool IsCsvType(string contentType)
    {
        if (string.IsNullOrEmpty(contentType))
        {
            return false;
        }

        var contentMimeType = contentType.Split(';')[0];

        return contentMimeType.Equals("text/csv", StringComparison.InvariantCultureIgnoreCase) ||
               contentMimeType.StartsWith("text/csv", StringComparison.InvariantCultureIgnoreCase);
    }
}

The serializer needs to implement ISerializer, NancyFX will automatically pick up and register this class for you, so all you need to do is create it.

The CanSerialize method is very important, in the CsvProcessor we will invoke all serializers until we find one that can handle serializing the content for the requested processor.

Csv Processor

Lastly we have the processor.

public class CsvProcessor : IResponseProcessor
{
    private readonly ISerializer serializer;

    private static readonly IEnumerable<Tuple<string, MediaRange>> extensionMappings =
        new[] { new Tuple<string, MediaRange>("csv", MediaRange.FromString("text/csv")) };

    public CsvProcessor(IEnumerable<ISerializer> serializers)
    {
        this.serializer = serializers.FirstOrDefault(x => x.CanSerialize("text/csv"));
    }

    public IEnumerable<Tuple<string, MediaRange>> ExtensionMappings
    {
        get { return extensionMappings; }
    }

    public ProcessorMatch CanProcess(MediaRange requestedMediaRange, dynamic model, NancyContext context)
    {
        if (requestedMediaRange.Matches("text/csv"))
        {
            return new ProcessorMatch
            {
                ModelResult = MatchResult.DontCare,
                RequestedContentTypeResult = MatchResult.ExactMatch
            };
        }

        return new ProcessorMatch
        {
            ModelResult = MatchResult.DontCare,
            RequestedContentTypeResult = MatchResult.NoMatch
        };
    }

    public Response Process(MediaRange requestedMediaRange, dynamic model, NancyContext context)
    {
        return new CsvResponse(model, this.serializer);
    }
}

This class has 3 methods and a constructor.

The Constructor simply works out which serializer it requires.

ExtensionMapping specifies which extension can be used on a request to call the processor. i.e if you can’t pass in accepts headers, you can end the url with .csv and the processor will be invoked!

CanProcess checks to see if the request can be processed.

Lastly Process simply processes the request.

Lets see it in action!

Using the EXACT same routes we defined on the previous posts, we can simply modify the header to ask for text/csv

And we get all our data serialized to CSV format! We didn’t have to modify the bootstrapper or anything, Nancy just sees the implemented interfaces and says MINE! :D

What about the Extension mentioned earlier!

In the processor we had the following

private static readonly IEnumerable<Tuple<string, MediaRange>> extensionMappings =
    new[] { new Tuple<string, MediaRange>("csv", MediaRange.FromString("text/csv")) };

public IEnumerable<Tuple<string, MediaRange>> ExtensionMappings
{
    get { return extensionMappings; }
}

Well when you access a url with .json or .csv, it will response with that as a file!

If we have the following page

We add the extension .json to the end

Nancy Replies! (The JSON format shown done using JSONView chrome extension

If we update to .csv

And if we open the file up

That’s all there is to it!

Hope this helps anyone with Conneg or Conneg with NancyFX :)


Published at DZone with permission of its author, Phillip Haydon. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)