34

I have a Web API project that is running on a server. It is supposed to return PDFs from two different kinds of sources: an actual portable document file (PDF), and a base64 string stored in a database. The trouble I'm having is sending the document back to a client MVC application. The rest of this is the details on everything that's happened and that I've already tried.

I have written code that successfully translates those two formats into C# code and then (back) to PDF form. I have successfully transferred a byte array that was supposed to represent one of those documents, but I can't get it to display in browser (in a new tab by itself). I always get some kind of "cannot be displayed" error.

Recently, I made a way to view the documents on the server side to make sure I can at least get it to do that. It gets the document into the code and creates a FileStreamResult with it that it then returns as an (implicit cast) ActionResult. I got that to return to a server side MVC controller and threw it into a simple return (no view) that displays the PDF just fine in the browser. However, trying to simply go straight to the Web API function simply returns what looks like a JSON representation of a FileStreamResult.

When I try to get that to return properly to my client MVC application, it tells me that "_buffer" can't be directly set. Some error message to the effect that some of the properties being returned and thrown into an object are private and can't be accessed.

The aforementioned byte-array representation of the PDF, when translated to a base64 string, doesn't seem to have the same number of characters as the "_buffer" string returned in the JSON by a FileStreamResult. It's missing about 26k 'A's at the end.

Any ideas about how to get this PDF to return correctly? I can provide code if necessary, but there has to be some known way to return a PDF from a server-side Web API application to a client-side MVC application and display it as a web page in a browser.

P.S. I do know that the "client-side" application isn't technically on the client side. It will also be a server application, but that shouldn't matter in this case. Relative to the Web API server, my MVC application is "client-side".

Code For getting pdf:

private System.Web.Mvc.ActionResult GetPDF()
{
    int bufferSize = 100;
    int startIndex = 0;
    long retval;
    byte[] buffer = new byte[bufferSize];
    MemoryStream stream = new MemoryStream();
    SqlCommand command;
    SqlConnection sqlca;
    SqlDataReader reader;

    using (sqlca = new SqlConnection(CONNECTION_STRING))
    {
        command = new SqlCommand((LINQ_TO_GET_FILE).ToString(), sqlca);
        sqlca.Open();
        reader = command.ExecuteReader(CommandBehavior.SequentialAccess);
        try
        {
            while (reader.Read())
            {
                do
                {
                    retval = reader.GetBytes(0, startIndex, buffer, 0, bufferSize);
                    stream.Write(buffer, 0, bufferSize);
                    startIndex += bufferSize;
                } while (retval == bufferSize);
            }
        }
        finally
        {
            reader.Close();
            sqlca.Close();
        }
    }
    stream.Position = 0;
    System.Web.Mvc.FileStreamResult fsr = new System.Web.Mvc.FileStreamResult(stream, "application/pdf");
    return fsr;
}

API Function that gets from GetPDF:

    [AcceptVerbs("GET","POST")]
    public System.Web.Mvc.ActionResult getPdf()
    {
        System.Web.Mvc.ActionResult retVal = GetPDF();
        return retVal;
    }

For displaying PDF server-side:

public ActionResult getChart()
{
    return new PDFController().GetPDF();
}

The code in the MVC application has changed a lot over time. The way it is right now, it doesn't get to the stage where it tries to display in browser. It gets an error before that.

public async Task<ActionResult> get_pdf(args,keys)
{
    JObject jObj;
    StringBuilder argumentsSB = new StringBuilder();
    if (args.Length != 0)
    {
        argumentsSB.Append("?");
        argumentsSB.Append(keys[0]);
        argumentsSB.Append("=");
        argumentsSB.Append(args[0]);
        for (int i = 1; i < args.Length; i += 1)
        {
            argumentsSB.Append("&");
            argumentsSB.Append(keys[i]);
            argumentsSB.Append("=");
            argumentsSB.Append(args[i]);
        }
    }
    else
    {
        argumentsSB.Append("");
    }
    var arguments = argumentsSB.ToString();
    using (var client = new HttpClient())
    {
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        var response = await client.GetAsync(URL_OF_SERVER+"api/pdf/getPdf/" + arguments).ConfigureAwait(false);
        jObj = (JObject)JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().Result);
    }
    return jObj.ToObject<ActionResult>();
}

The JSON I get from running the method directly from the Web API controller is:

{
    "FileStream":{
        "_buffer":"JVBER...NjdENEUxAA...AA==",
        "_origin":0,
        "_position":0,
        "_length":45600,
        "_capacity":65536,
        "_expandable":true,
        "_writable":true,
        "_exposable":true,
        "_isOpen":true,
        "__identity":null},
    "ContentType":"application/pdf",
    "FileDownloadName":""
}

I shortened "_buffer" because it's ridiculously long. I currently get the error message below on the return line of get_pdf(args,keys)

Exception Details: Newtonsoft.Json.JsonSerializationException: Could not create an instance of type System.Web.Mvc.ActionResult. Type is an interface or abstract class and cannot be instantiated. Path 'FileStream'.

Back when I used to get a blank pdf reader (the reader was blank. no file), I used this code:

public async Task<ActionResult> get_pdf(args,keys)
{
    byte[] retArr;
    StringBuilder argumentsSB = new StringBuilder();
    if (args.Length != 0)
    {
        argumentsSB.Append("?");
        argumentsSB.Append(keys[0]);
        argumentsSB.Append("=");
        argumentsSB.Append(args[0]);
        for (int i = 1; i < args.Length; i += 1)
        {
            argumentsSB.Append("&");
            argumentsSB.Append(keys[i]);
            argumentsSB.Append("=");
            argumentsSB.Append(args[i]);
        }
    }
    else
    {
        argumentsSB.Append("");
    }
    var arguments = argumentsSB.ToString();
    using (var client = new HttpClient())
    {
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/pdf"));
        var response = await client.GetAsync(URL_OF_SERVER+"api/webservice/" + method + "/" + arguments).ConfigureAwait(false);
        retArr = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
    }
    var x = retArr.Skip(1).Take(y.Length-2).ToArray();
    /*Response.Clear();
    Response.ClearContent();
    Response.ClearHeaders();
    Response.ContentType = "application/pdf";
    Response.AppendHeader("Content-Disposition", "inline;filename=document.pdf");
    Response.BufferOutput = true;
    Response.BinaryWrite(x);
    Response.Flush();
    Response.End();*/
    return new FileStreamResult(new MemoryStream(x),MediaTypeNames.Application.Pdf);
    }

Commented out is code from some other attempts. When I was using that code, I was returning a byte array from the server. It looked like:

JVBER...NjdENEUx
Doctor93
  • 355
  • 1
  • 3
  • 10
  • Show some sample code, specific error messages, etc. – Mr. T Mar 16 '16 at 17:26
  • @Mr.T I've added everything I could think to add in my edit just now. – Doctor93 Mar 16 '16 at 18:17
  • Why are you using `JObject` at all in `get_pdf`? – Jacob Mar 16 '16 at 18:30
  • Also, which method is your `WebAPI` method? Sounds like the real issue here is just trying to deal with JSON where it doesn't make sense to do so. – Jacob Mar 16 '16 at 18:32
  • 1
    Why did you tag this as Web API and MVC? You say Web API but all of your code appears to be MVC. – mason Mar 16 '16 at 18:43
  • @Jacob The WebAPI method would be the one clearly labeled API Function. The one above it is in the same controller. – Doctor93 Mar 16 '16 at 18:57
  • @mason All that ActionResult stuff in my Web API controller was just an attempt to try to get this to return useful information. – Doctor93 Mar 16 '16 at 18:57
  • 1
    That's *not* Web API. That's just MVC. Web API 2 and MVC 5 are distinct from each other. As far as I can tell, you aren't actually using Web API at all. You are using MVC in a similar manner to Web API, but that doesn't make it Web API. – mason Mar 16 '16 at 19:03
  • @mason Thank you. I do actually know when I'm using Web API and when I'm not. The rest of this controller has other functions that are all Web API. There's really only one function in the API that is shown as an MVC function in my question. Now it's an HttpResponseMessage and is, beyond a doubt, an API function now. Nkosi's answer worked for me. – Doctor93 Mar 16 '16 at 19:11
  • 1
    In MVC 5, you can't mix and match Web API and MVC. WebAPI controllers inherit from ApiController and is not a subtype of the MVC Controller class. So that doesn't really make sense. – mason Mar 16 '16 at 19:14
  • public class WebServiceController : ApiController – Doctor93 Apr 08 '16 at 18:11
  • It's quite simple, actually. My controller is an ApiController using the namespace System.Web.Mvc. So it's both. – Doctor93 Apr 08 '16 at 18:12

1 Answers1

38

Some Server side code to return PDF (Web Api).

[HttpGet]
[Route("documents/{docid}")]
public HttpResponseMessage Display(string docid) {
    HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.BadRequest);
    var documents = reader.GetDocument(docid);
    if (documents != null && documents.Length == 1) {
        var document = documents[0];
        docid = document.docid;
        byte[] buffer = new byte[0];
        //generate pdf document
        MemoryStream memoryStream = new MemoryStream();
        MyPDFGenerator.New().PrintToStream(document, memoryStream);
        //get buffer
        buffer = memoryStream.ToArray();
        //content length for use in header
        var contentLength = buffer.Length;
        //200
        //successful
        var statuscode = HttpStatusCode.OK;
        response = Request.CreateResponse(statuscode);
        response.Content = new StreamContent(new MemoryStream(buffer));
        response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
        response.Content.Headers.ContentLength = contentLength;
        ContentDispositionHeaderValue contentDisposition = null;
        if (ContentDispositionHeaderValue.TryParse("inline; filename=" + document.Name + ".pdf", out contentDisposition)) {
            response.Content.Headers.ContentDisposition = contentDisposition;
        }
    } else {
        var statuscode = HttpStatusCode.NotFound;
        var message = String.Format("Unable to find resource. Resource \"{0}\" may not exist.", docid);
        var responseData = responseDataFactory.CreateWithOnlyMetadata(statuscode, message);
        response = Request.CreateResponse((HttpStatusCode)responseData.meta.code, responseData);
    }
    return response;
}

On my a View you could do something like this

<a href="api/documents/1234" target = "_blank" class = "btn btn-success" >View document</a>

which will call the web api and open the PDF document in a new tab in the browser.

Here is how i basically do the same thing but from a MVC controller

// NOTE: Original return type: FileContentResult, Changed to ActionResult to allow for error results
[Route("{docid}/Label")]
public ActionResult Label(Guid docid) {
    var timestamp = DateTime.Now;
    var shipment = objectFactory.Create<Document>();
    if (docid!= Guid.Empty) {
        var documents = reader.GetDocuments(docid);
        if (documents.Length > 0)
            document = documents[0];

            MemoryStream memoryStream = new MemoryStream();
            var printer = MyPDFGenerator.New();
            printer.PrintToStream(document, memoryStream);

            Response.AppendHeader("Content-Disposition", "inline; filename=" + timestamp.ToString("yyyyMMddHHmmss") + ".pdf");
            return File(memoryStream.ToArray(), "application/pdf");
        } else {
            return this.RedirectToAction(c => c.Details(id));
        }
    }
    return this.RedirectToAction(c => c.Index(null, null));
}

Hope this helps

Nkosi
  • 191,971
  • 29
  • 311
  • 378
  • 1
    Ok, that works, but I'm trying to remove the link from front end to back end. The front end should basically act as a middle man hiding ALL of the business logic. My attempt to do that returns this text to the browser "StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.StreamContent, Headers: { Pragma: no-cache Cache-Control: no-cache Date: Wed, 16 Mar 2016 18:48:38 GMT Server: Microsoft-IIS/10.0 X-AspNet-Version: 4.0.30319 X-Powered-By: ASP.NET Content-Length: 45600 Content-Disposition: inline; filename=chart.pdf Content-Type: application/pdf Expires: -1 }" – Doctor93 Mar 16 '16 at 18:55
  • 1
    I got it. Thank you! I just had to read the content as a Stream and throw it in a FileStreamResult implicitly cast as an ActionResult – Doctor93 Mar 16 '16 at 19:08
  • Hi! I was refactoring your example and hit a wall when I tried to simplify the line `response.Content = new StreamContent(new MemoryStream(buffer));` - can you help me understand **why** you can't just pass the MemoryStream created earlier to the StreamContent? I tried, and it didn't work, but I don't understand why not. – Sam Bartlett Mar 09 '17 at 15:57
  • 1
    @SamBartlett some how that stream was being disposed of before the response completed. my guess was that it had something to do with the scope of the variable. I am also guessing you are referring to the web API version of the answer as I had not experienced that issue with the MVC verison – Nkosi Mar 09 '17 at 16:10
  • @Nkosi yes, sorry, I am referring to the WebAPI version. It's interesting that I also hit that issue. Maybe I need to read a bit more about the lifecycle of the stream / MemoryStreams in general. – Sam Bartlett Mar 09 '17 at 19:25
  • This worked for me except I had the change the get buffer from: buffer = memoryStream.GetBuffer(); to: buffer = memoryStream.ToArray(); The problem with GetBuffer() is that it returns all the allocated memory for the buffer which may include a lot of null bytes. – Sgraffite Aug 01 '17 at 17:23