2

Working in ASP.NET Core and using iTextSharp, I'm building a PDF and save it to the local system. I now want to open that file in the browser but can't seem to make that work since I get a FileStream error in one try and nothing at all in another try.

My logic is in the controller below. I've replaced the unnecessary code with // region description. The important code (the things I tried) is inside the TODO: Open the file region.

Controller

    [HttpPost]
    public (JsonResult, IActionResult) CreatePDF([FromBody] ReportViewModel model)
    {
        try
        {

            // region Logic code
            using (System.IO.MemoryStream memoryStream = new System.IO.MemoryStream())
            {
                // Create the document
                iTextSharp.text.Document document = new iTextSharp.text.Document();
                // Place the document in the PDFWriter
                iTextSharp.text.pdf.PdfWriter PDFWriter =
                    iTextSharp.text.pdf.PdfWriter.GetInstance(document, memoryStream);

                // region Initialize Fonts

                // Open the document
                document.Open();

                // region Add content to document

                // Close the document
                document.Close();

                #region Create and Write the file
                // Create the directory
                string directory = $"{_settings.Value.ReportDirectory}\\";
                System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(directory));

                // region Create the fileName

                // Combine the directory and the fileName
                directory = System.IO.Path.Combine(directory, fileName);

                // Create the file
                byte[] content = memoryStream.ToArray();
                using (System.IO.FileStream fs = System.IO.File.Create(directory))
                {
                    fs.Write(content, 0, (int)content.Length);
                }
                #endregion

                #region TODO: Open the file
                // TRY: File(Stream, type) => Newtonsoft.Json.JsonSerializationException:
                // Error getting value from 'ReadTimeout' on 'System.IO.FileStream'.
                // ---> System.InvalidOperationException: Timeouts are supported for this stream.
                System.IO.FileStream fileStream = new System.IO.FileStream(directory, System.IO.FileMode.Open);
                var returnPDF = File(fileStream, contentType: "application/pdf");

                // TRY: File(path, type) => Newtonsoft.Json.JsonSerializationException:
                // Error getting value from 'ReadTimeout' on 'System.IO.FileStream'.
                // ---> System.InvalidOperationException: Timeouts are supported for this stream.
                returnPDF = File(System.IO.File.OpenRead(directory), contentType: "application/pdf" );

                // TRY: File(byte[], type) => Nothing happened
                returnPDF = File(content, contentType: "apllication/pdf");
                #endregion

                return ( Json(new { isError = false }), returnPDF );
            }
        }
        catch (System.Exception ex)
        {
            Serilog.Log.Error($"ReportController.CreatePDF() - {ex}");
            return ( Json(new { isError = true, errorMessage = ex.Message }), null );
        }
    }

References to useful Stackoverflow answers

Adam Vincent
  • 3,281
  • 14
  • 38
Wouter Vanherck
  • 2,070
  • 3
  • 27
  • 41

3 Answers3

7

Instead of loading the file again from the directory you can just create a FileContentResult and pass the byte[] to it, that you already allocated.

return ( Json(new { isError = false }), new FileContentResult(content, "application/pdf"));

Keep in mind that the browser will not download the file this way. It will extract the response-stream but not download it because it is within your json response and therefore the response-content-type will be application/json.

You could instead just return an IActionResult and just do it like that:

return new FileContentResult(content, "application/pdf");

If you persist on having the information "isError", then you could provide an url additionally and instead of the fileresult. For that you can register the IActionContextAccessor class in your ConfigureServices(...) method.

services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();

Provide a seperate route and use the IUrlHelperFactory to generate an url.

A sample can be found here.

This url can then be "clicked" programatically within the front-end when the response isError equals false.

alsami
  • 8,996
  • 3
  • 25
  • 36
  • If I understand what you said about loading the file correctly, then I load it when I do `File(path, "application/pdf")`? I'm not actually reloading the file with `File(Stream, "application/pdf")` or `File(byte[], "application/pdf")` right? And that should be the same as `new FileContentResult(content, "application/pdf")`? Thanks for clarifying that! About the rest, `return new FileContentResult(content, "application/pdf");` doesn't do anything in my case (same as my last try). And you're right, I don't really need to return `isError`. I've modified the code and really appreciate the link. – Wouter Vanherck Jul 31 '18 at 15:05
  • 1
    I meant the filestream you manually open after you already allocated the file bytes in the content byte-array. If it doesn't work, your file bytes must be corrupt. Use a static pdf file and see if it works. – alsami Jul 31 '18 at 15:13
1

Action should return a single result, be it PDF or JSON, so that the client can handle the response appropriately.

I've refactored the code to make it easier to read by extracting the major logic out into a separate function.

The primary focus of the action being on how the response is returned.

[HttpPost]
public IActionResult CreatePDF([FromBody] ReportViewModel model) {
    try {
        // region Logic code
        byte[] content = CreateFile(model);
        return File(content, contentType: "application/pdf");
    } catch (System.Exception ex) {
        Serilog.Log.Error($"ReportController.CreatePDF() - {ex}");
        return Json(new { isError = true, errorMessage = ex.Message });
    }
}

byte[] CreateFile(ReportViewModel model) {
    using (System.IO.MemoryStream memoryStream = new System.IO.MemoryStream()) {
        // Create the document
        iTextSharp.text.Document document = new iTextSharp.text.Document();
        // Place the document in the PDFWriter
        iTextSharp.text.pdf.PdfWriter PDFWriter =
            iTextSharp.text.pdf.PdfWriter.GetInstance(document, memoryStream);

        // region Initialize Fonts
        // Open the document
        document.Open();
        // region Add content to document
        // Close the document
        document.Close();
        #region Create and Write the file
        // Create the directory
        string directory = $"{_settings.Value.ReportDirectory}\\";            
        System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(directory));
        // region Create the fileName
        // Combine the directory and the fileName
        directory = System.IO.Path.Combine(directory, fileName);
        // Create the file
        byte[] content = memoryStream.ToArray();
        using (System.IO.FileStream fs = System.IO.File.Create(directory)) {
            fs.Write(content, 0, (int)content.Length);
        }
        #endregion            
        return content;
    }    
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • So if I didn't miss anything: The only thing you changed is the endpoint which is now split in 2 different functions (including the endpoint only returning one item)? If it's not recomended to return 2 values, the why is `System.ValueTuple` a thing? – Wouter Vanherck Jul 31 '18 at 14:02
  • @WouterVanherck the value tuple is a thing yes but it was not meant for the MVC action results. – Nkosi Jul 31 '18 at 14:05
  • @WouterVanherck second if you review the refactored code you will see how the action is returning just the `IActionResult` abstraction, which can either be the PDF file or the JSON error response but not both. – Nkosi Jul 31 '18 at 14:06
  • Yes, I noticed that you're only returning the Json when an error is thrown and that there's no Json returned when the code is successful. I'm trying this out now since I agree that I can do without the Json in the success. E: Thanks for the post edit, makes me realise that I don't NEED 2 functions here – Wouter Vanherck Jul 31 '18 at 14:09
  • Tried it out, doesn't throw any errors but doesn't open a PDF either. Same result as described earlier when I passed content – Wouter Vanherck Jul 31 '18 at 14:19
0

This is slightly adapted from my working code, but should suit your purposes. I believe there's some room for improvement (Like properly disposing of the MemoryStream) but it's succinct, clean and best of all, it works.

There's no need for the extra serialization, or read / write to the file system if it's not a requirement.

Controller

[HttpPost]
public (JsonResult, IActionResult) CreatePDF([FromBody] ReportViewModel model)
{
    try
    {
        return ( Json(new { isError = false }), GetDocument() );
    }

    catch (System.Exception ex)
    {
        Serilog.Log.Error($"ReportController.CreatePDF() - {ex}");
        return (Json(new {isError = true, errorMessage = ex.Message}), null);
    }
}

public FileContentResult GetDocument()
{
    var fc = new MyPdfCreator();
    var document = fc.GetDocumentStream();
    return File(document.ToArray(), "application/pdf", "File Name.pdf");
}

MyPdfCreator

public class MyPdfCreator 
{
    public MemoryStream GetDocumentStream()
    {
        var ms = new MemoryStream();
        var doc = new Document();
        var writer = PdfWriter.GetInstance(doc, ms);
        writer.CloseStream = false;
        doc.Open();
        doc.Add(new Paragraph("Hello World"));
        doc.Close();
        writer.Close();
        return ms;
    }
}
Adam Vincent
  • 3,281
  • 14
  • 38
  • When using your MyPdfCreator class: `'PdfWriter: type used in a using statement must be implicitly convertible to 'System.IDisposable'` – Wouter Vanherck Aug 01 '18 at 12:14
  • ew. apologies. I was using `PdfStamper`, i swapped in PdfWriter. I should have known better. – Adam Vincent Aug 01 '18 at 12:30
  • please see the updated code. I have added in proper disposal like I wanted. I've also tested it this time. =P – Adam Vincent Aug 01 '18 at 13:17
  • also, while correcting my code I noticed that iTextSharp is obsolete. They did a complete re-write of itext since the last version of itextsharp. `Install-Package itext7` – Adam Vincent Aug 01 '18 at 13:21
  • When using your updated version: `'Document': type used in a using statement must be implicitly convertible to 'System.IDisposable'`, you sure it compiled? - Also: iText7 falls under the AGPL license, while we're using a version with the LGPL license. Our code will not be open source – Wouter Vanherck Aug 01 '18 at 13:28
  • Looking further at your controller code, it returns a `FileContentResult`, which doesn't allow me to return Json if needed – Wouter Vanherck Aug 01 '18 at 13:32
  • Fully converted my code to suit your answer. Like the others, there were no errors but nothing happened :( – Wouter Vanherck Aug 01 '18 at 13:54
  • What I posted wasn't meant to be 100% replacement for your controller. Call GetDocument() from your `CreatePDF` controller method. I added in the implementation above. – Adam Vincent Aug 01 '18 at 13:58
  • It may not have displayed a PDF. Did you check your downloads folder? ^^ – Adam Vincent Aug 01 '18 at 14:01
  • I'll scratch my remark about not being able to return Json. I didn't replace my controller but tried your answer in a similar way before you updated it. Still doesn't show anything. My downloads folder is also empty. Quick notice about your updated code: `Document` doesn't implement `IDisposable`, if it does on your end, might this be a version issue? – Wouter Vanherck Aug 01 '18 at 14:07
  • could be, what version do you have? I've got 5.5.13 – Adam Vincent Aug 01 '18 at 14:16
  • I use `iTextSharp-LGPL v4.1.6` because of the license. But I've changed the code to work around the error and still got no working result. Should be something else. I'm also using `iTextSharp.text.Document` – Wouter Vanherck Aug 01 '18 at 14:30
  • Yeah, the older version handles disposal differently. I've updated the code again and tested with `iTextSharp-LGPL v4.1.6` – Adam Vincent Aug 01 '18 at 14:42