Introduction

So I have been doing this Nancy series for a while now and the demoproject is still up at Github.

Today I’m going to try and return a pdf from a request.

The Response.

So instead of returning json or html or some such I want to return a pdf page. I guess I could write a complete viewengine if needed but I don’t need that I just want my trees module to return a pdf in a certain format when requested.

This is actually very simple to do you just need to return an object that inherits from Response and that makes the pdf. I used itextsharp to make the pdf and Nancy did all the heavy lifting from there on in ;-). ITextsharp is on nuget so you should use that.

And here is my response.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using System;
using System.IO;
using Nancy;
using NancyDemo.Csharp.Model;
using iTextSharp.text;
using iTextSharp.text.pdf;
 
namespace NancyDemo.Csharp.Processors
{
    public class TreeModelPdfResponse : Response
    {
        public TreeModelPdfResponse(TreeModel model)
        {
            this.Contents = GetPdfContents(model);
            this.ContentType = "application/pdf";
            this.StatusCode = HttpStatusCode.OK;
        }
 
        private Action<Stream> GetPdfContents(TreeModel model)
        {
            return stream =>
                {
                    var oDoc = new Document(PageSize.A4);
                    PdfWriter.GetInstance(oDoc, stream);
                    oDoc.Open();
                    oDoc.Add(new Paragraph("This a Tree"));
                    oDoc.Add(new Paragraph("Id: " + model.Id));
                    oDoc.Add(new Paragraph("Genus: " + model.Genus));
                    oDoc.Close();
                };
        }
    }
}
using System;
using System.IO;
using Nancy;
using NancyDemo.Csharp.Model;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace NancyDemo.Csharp.Processors
{
    public class TreeModelPdfResponse : Response
    {
        public TreeModelPdfResponse(TreeModel model)
        {
            this.Contents = GetPdfContents(model);
            this.ContentType = "application/pdf";
            this.StatusCode = HttpStatusCode.OK;
        }

        private Action<Stream> GetPdfContents(TreeModel model)
        {
            return stream =>
                {
                    var oDoc = new Document(PageSize.A4);
                    PdfWriter.GetInstance(oDoc, stream);
                    oDoc.Open();
                    oDoc.Add(new Paragraph("This a Tree"));
                    oDoc.Add(new Paragraph("Id: " + model.Id));
                    oDoc.Add(new Paragraph("Genus: " + model.Genus));
                    oDoc.Close();
                };
        }
    }
}

And in my Treesmodule I will add this Get-method.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Get["/trees/pdf/{Id}"] = parameters =>
            {
                int result;
                var isInteger = int.TryParse(parameters.id, out result);
                var tree = treeService.FindById(result);
                if (isInteger && tree != null)
                {
                    return new TreeModelPdfResponse(tree);
                }
                else
                {
                    return HttpStatusCode.NotFound;
                }
            };
Get["/trees/pdf/{Id}"] = parameters =>
            {
                int result;
                var isInteger = int.TryParse(parameters.id, out result);
                var tree = treeService.FindById(result);
                if (isInteger && tree != null)
                {
                    return new TreeModelPdfResponse(tree);
                }
                else
                {
                    return HttpStatusCode.NotFound;
                }
            };

So if I now go to my page http://localhost/trees/pdf/1 I will get my custom pdf.

Or I can just embed it in an iframe of course.

Which would look like this in our code.

XML
1
2
3
<div id="pdf">
    <iframe width="400" height="500" src="/trees/pdf/@Model.Id" id="pdf_content" />
</div>
<div id="pdf">
    <iframe width="400" height="500" src="/trees/pdf/@Model.Id" id="pdf_content" />
</div>

Now that was pretty simples, huh.

Content negotiation.

Of course you might have noticed that my get looks a lot like the get I already have and a pdf is just a MIME-type like json or xml. So if I could just tell my service that I want application/pdf than why wouldnt it just give me that? Well it can.

To test this I used easyhttp (because I could not find how to do this from within a html page(yet).

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.IO;
using EasyHttp.Http;
 
namespace NancyDemo.Csharp.Easyhttp
{
    class Program
    {
        private static void Main(string[] args)
        {
            var http = new HttpClient();
            http.Request.Accept = "application/pdf";
            http.Request.ParametersAsSegments = true;
            http.GetAsFile("http://localhost:65367/trees/1", "e:\temp\Test.pdf");
            Console.ReadLine();
        }
    }
}
using System;
using System.IO;
using EasyHttp.Http;

namespace NancyDemo.Csharp.Easyhttp
{
    class Program
    {
        private static void Main(string[] args)
        {
            var http = new HttpClient();
            http.Request.Accept = "application/pdf";
            http.Request.ParametersAsSegments = true;
            http.GetAsFile("http://localhost:65367/trees/1", "e:\temp\Test.pdf");
            Console.ReadLine();
        }
    }
}

My Get in my NancyModule uses the Negotiate.

Like this.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Get["/trees/{Id}"] = parameters =>
                {
                    int result;
                    var isInteger = int.TryParse(parameters.id, out result);
                    var tree = treeService.FindById(result);
                    if(isInteger && tree != null)
                    {
                        return Negotiate.WithModel(tree);
                    }
                    else
                    {
                        return HttpStatusCode.NotFound;
                    }
                };
Get["/trees/{Id}"] = parameters =>
                {
                    int result;
                    var isInteger = int.TryParse(parameters.id, out result);
                    var tree = treeService.FindById(result);
                    if(isInteger && tree != null)
                    {
                        return Negotiate.WithModel(tree);
                    }
                    else
                    {
                        return HttpStatusCode.NotFound;
                    }
                };

I then just need to add a Processor class.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System;
using System.Collections.Generic;
using Nancy;
using Nancy.Responses.Negotiation;
 
namespace NancyDemo.Csharp.Processors
{
    public class PdfProcessor : IResponseProcessor
    {
        private static readonly IEnumerable<Tuple<string, MediaRange>> extensionMappings =
            new[] { new Tuple<string, MediaRange>("pdf", MediaRange.FromString("application/pdf")) };
 
        public ProcessorMatch CanProcess(MediaRange requestedMediaRange, dynamic model, NancyContext context)
        {
            if (IsExactPdfContentType(requestedMediaRange))
            {
                return new ProcessorMatch
                    {
                        ModelResult = MatchResult.DontCare,
                        RequestedContentTypeResult = MatchResult.ExactMatch
                    };
            }
 
            return new ProcessorMatch
            {
                ModelResult = MatchResult.DontCare,
                RequestedContentTypeResult = MatchResult.NoMatch
            };
        }
 
        private static bool IsExactPdfContentType(MediaRange requestedContentType)
        {
            if (requestedContentType.Type.IsWildcard && requestedContentType.Subtype.IsWildcard)
            {
                return true;
            }
 
            return requestedContentType.Matches("application/pdf");
        }
 
        public Response Process(MediaRange requestedMediaRange, dynamic model, NancyContext context)
        {
            return new PdfResponse(model);
        }
 
        public IEnumerable<Tuple<string, MediaRange>> ExtensionMappings
        {
            get { return extensionMappings; }
        }
    }
}
using System;
using System.Collections.Generic;
using Nancy;
using Nancy.Responses.Negotiation;

namespace NancyDemo.Csharp.Processors
{
    public class PdfProcessor : IResponseProcessor
    {
        private static readonly IEnumerable<Tuple<string, MediaRange>> extensionMappings =
            new[] { new Tuple<string, MediaRange>("pdf", MediaRange.FromString("application/pdf")) };

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

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

        private static bool IsExactPdfContentType(MediaRange requestedContentType)
        {
            if (requestedContentType.Type.IsWildcard && requestedContentType.Subtype.IsWildcard)
            {
                return true;
            }

            return requestedContentType.Matches("application/pdf");
        }

        public Response Process(MediaRange requestedMediaRange, dynamic model, NancyContext context)
        {
            return new PdfResponse(model);
        }

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

And a response class.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System;
using System.IO;
using Nancy;
using NancyDemo.Csharp.Model;
using iTextSharp.text;
using iTextSharp.text.pdf;
 
namespace NancyDemo.Csharp.Processors
{
    public class PdfResponse<TModel> : Response
    {
        public PdfResponse(TModel model)
        {
            this.Contents = GetPdfContents(model);
            this.ContentType = "application/pdf";
            this.StatusCode = HttpStatusCode.OK;
        }
 
        protected virtual Action<Stream> GetPdfContents(TModel model)
        {
            return stream =>
                {
                    var oDoc = new Document(PageSize.A4);
                    PdfWriter.GetInstance(oDoc, stream);
                    oDoc.Open();
                    foreach (var prop in model.GetType().GetProperties())
                    {
                        oDoc.Add(new Paragraph(prop.Name + ":" + prop.GetValue(model).ToString()));    
                    }
                    oDoc.Close();
                };
        }
    }
 
    public class PdfResponse : PdfResponse<object>
    {
        public PdfResponse(object model)
            : base(model)
        {
        }
    }
}
using System;
using System.IO;
using Nancy;
using NancyDemo.Csharp.Model;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace NancyDemo.Csharp.Processors
{
    public class PdfResponse<TModel> : Response
    {
        public PdfResponse(TModel model)
        {
            this.Contents = GetPdfContents(model);
            this.ContentType = "application/pdf";
            this.StatusCode = HttpStatusCode.OK;
        }

        protected virtual Action<Stream> GetPdfContents(TModel model)
        {
            return stream =>
                {
                    var oDoc = new Document(PageSize.A4);
                    PdfWriter.GetInstance(oDoc, stream);
                    oDoc.Open();
                    foreach (var prop in model.GetType().GetProperties())
                    {
                        oDoc.Add(new Paragraph(prop.Name + ":" + prop.GetValue(model).ToString()));    
                    }
                    oDoc.Close();
                };
        }
    }

    public class PdfResponse : PdfResponse<object>
    {
        public PdfResponse(object model)
            : base(model)
        {
        }
    }
}

This will work for other objets than TreeModel too, it’s far from complete though.

And now my servicecall with easyhttp will create a file for me and that looks like this.

Conclusion

Again that was easy. But if anyone can tell me how to get the content negotiation to work from within a html page I would be even more happy.