-
-
Notifications
You must be signed in to change notification settings - Fork 558
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Is there an example of streaming directly to the response stream in ASP.NET Core? #52
Comments
Hello, I think that you are incorrectly utilizing the streaming capability within the asp mvc. Please try this and let me know if it works 😁 public async Task<ActionResult> DownloadReport()
{
await using var stream = new MemoryStream();
myReport.GeneratePdf(stream);
return File(stream, "application/pdf", "myReport.pdf");
} |
@MarcinZiabek Thanks for the prompt response. That does work, but as mentioned, I'm looking to avoid allocations by writing to an intermediate stream (the new I suspect that it may be due to the fact that the It would be great if this was supported, but I can make do with copying for now. |
Can you please provide more details and justification for why it may work this way? So far, I have been using this method to stream large files up to several gigabytes (from blob storage, through asp core, to the client) and never saw any significant memory allocations, in contrast to using just a byte array. Maybe I just miss some important detail here... |
That is correct, QuestPDF does not support async operations. The only place where the async pattern would be useful is streaming images. However, because of the layouting algorithm, the library needs to know the exact size of the image at the very beginning of the process (which usually require just loading the image into memory) and then keep it to nearly the very end when the PDF file is produced with SkiaSharp. Alternatively, the library would need to load the image into memory twice which is no good either. |
I have been able to get around my use case by rendering to a local array; because of the size of the PDFs, I worry about allocations building up on the LOH. |
Can you please provide more information? I was thinking about this use case and indeed, you may be right, the stream API does not help much when used with the MomeryStream object. It would be great to improve the library to reduce its memory consumption and GA pressure :) |
@MarcinZiabek The documents that I'm generating are currently ~1.6 MB, so greater than 85K which is required to go on the LOH. In a high-throughput situation, this can easily cause problems, as the LOH:
Source: |
This is truly an interesting case, thank you for sharing this link! I am planning to read more about this concept. We also need to make sure that the core platform operates in a similar fashion. It would be great to have any real benchmarks showing this is indeed a problem.
I am still not sure if optimizing this streaming access will result in any significant results at this point. When you generate the document with images, those images need to be present in memory during the entire generation process. So, by average, we can reduce LOH overhead only by half in the most optimistic scenario. And this is assuming that SkiaSharp does not introduce any buffering or workload on its own. I am not against any optimisation. In fact, any help is always more than welcome 😁 |
I can reproduce the issue with HttpContext.Response.Body stream. I think there are two possible solutions.
I will provide a PR for the wrapper solution. |
Thank you for providing this solution and improvement. I will analyse it as soon as I can. I understand your explanation. Is there any possibility that the wrapper idea may introduce any bugs in other areas? After all:
|
Is this fixed? Trying to pass |
I will take a look within a couple of days. Maybe something was incorrectly merged. Thank you for rising my attention! |
I decided to roll back changes developed in #65 in Quest 2022.2.5. Indeed, in some cases it allows streaming directly to the Response.Body stream. However, this also introduces a significant risk. When an exception is thrown, the QuestPDF library attempts to close and dispose the SkiaSharp SkDocument object. This involves disposing the managed stream provided as an argument. If the provided stream happens to be Response.Body, a momery security issue happens and the FatalException is thrown. This is unaccaptable on production environments. In other cases, the solution works but the "Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead" exception is thrown - this is caused by ASP.NET. Basically, it excepts that when we directly stream to the response stream, we should use the stream.WriteAsync method. Unfortunatelly, the SkiaSharp calls just the stream.Write method internally and this cannot be easily changed. It is possible that I do not fully understand the whole problem. For now, I want to mitigate the risk. If anyone wants to help me analyse this problem with more details and on all environments, please do 😁 |
Maybe you can use function GeneratePdf() to get byte[] first: |
You can use https://github.com/Microsoft/Microsoft.IO.RecyclableMemoryStream as MemoryStream Replacement for less LOH Fragmentation. |
Here an Example: public class QuestPdfResult : ActionResult
{
private readonly IDocument _document;
private readonly string _filename;
public QuestPdfResult(IDocument document, string filename)
{
_document = document;
_filename = filename;
}
public override async Task ExecuteResultAsync(ActionContext context)
{
var httpContext = context.HttpContext;
var streamManager = httpContext.RequestServices.GetRequiredService<RecyclableMemoryStreamManager>();
using var memoryStream = streamManager.GetStream();
_document.GeneratePdf(memoryStream);
httpContext.Response.ContentType = "application/pdf";
httpContext.Response.Headers.ContentDisposition = $"attachment; filename=\"{_filename}\"";
memoryStream.Position = 0;
await memoryStream.CopyToAsync(httpContext.Response.Body);
}
} you have to Add the RecyclableMemoryStreamManager to DI. services.AddSingleton<RecyclableMemoryStreamManager>(); |
Version: 2021.10.1
First, I want to say this popped up on my radar because of the Reddit thread that was posted:
https://www.reddit.com/r/csharp/comments/ox3klz/questpdf_my_opensource_c_library_for_creating_pdf/
And it's exactly what I was looking for.
That said, I've generated a document, and can get the results by calling the
GeneratePdf
method to return abyte
array or write to an instance of aMemoryStream
.However, in an attempt to save memory allocations, I'm looking to write directly to the result stream in ASP.NET Core.
When I do this:
I get a
NullReferenceException
with the following stack trace:I've looked in the closed issues and seen that there was an issue at one point around closing the stream, but it's been resolved.
Is there anything special that needs to be done when writing directly to the response stream in ASP.NET Core?
As mentioned, when making the following calls instead:
It does not throw.
The text was updated successfully, but these errors were encountered: