Skip to footer content
USING IRONPDF

ASP.NET Core: Create PDF on the Fly with IronPDF

Generate professional PDF documents dynamically in ASP.NET Core by converting HTML content into polished PDFs and streaming them directly to the browser -- no disk storage required, no temporary files to manage.

When building modern web applications in ASP.NET Core, the ability to produce PDF documents on demand is a recurring requirement. Invoices need to download the moment a payment clears. Compliance reports must appear instantly when an auditor clicks "Export." Certificates should be ready before the user has time to wonder if something went wrong. IronPDF handles all of these scenarios through its Chromium-based PDF library, which converts HTML -- including CSS, JavaScript, and web fonts -- into pixel-accurate PDF output without writing anything to disk.

This guide covers everything you need to know: installing the library, generating invoices from HTML strings, streaming reports from Entity Framework data, applying page headers and security settings, and adopting the best practices that keep high-traffic ASP.NET applications performing well.

What Does Creating PDFs on the Fly Mean?

"On the fly" means the document is constructed in memory at the moment of the HTTP request and sent directly to the caller. No PDF file is written to the file system, no background job queues the work, and no cache stores the result between requests.

This approach matters for several reasons. First, cloud deployment targets -- Azure App Service, AWS Lambda, Docker containers -- often run in environments where the local file system is ephemeral or read-only. Generating a PDF to a temp folder and then reading it back is fragile in these environments. Second, avoiding disk writes reduces attack surface: there is no residual file a subsequent request could accidentally serve to the wrong user. Third, memory-only generation is typically faster because it eliminates two I/O operations (write and read) in the critical path.

IronPDF's ChromePdfRenderer exposes a .BinaryData property and a .Stream property on every generated document. Either can be passed directly to an ASP.NET Core FileResult, making streaming a one-liner in practice.

Get stated with IronPDF now.
green arrow pointer

How Do You Install IronPDF in an ASP.NET Core Project?

Add the NuGet package through either the Package Manager Console or the .NET CLI:

Install-Package IronPdf
dotnet add package IronPdf
Install-Package IronPdf
dotnet add package IronPdf
SHELL

Once the package is installed, set your license key at application startup -- typically in Program.cs before the first renderer is created:

using IronPdf;

// Place license activation before any IronPDF call
License.LicenseKey = "YOUR-LICENSE-KEY";

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();

// Register ChromePdfRenderer as a singleton so the Chromium engine
// is initialised once and reused across all requests.
builder.Services.AddSingleton<ChromePdfRenderer>();

var app = builder.Build();
app.MapDefaultControllerRoute();
app.Run();
using IronPdf;

// Place license activation before any IronPDF call
License.LicenseKey = "YOUR-LICENSE-KEY";

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();

// Register ChromePdfRenderer as a singleton so the Chromium engine
// is initialised once and reused across all requests.
builder.Services.AddSingleton<ChromePdfRenderer>();

var app = builder.Build();
app.MapDefaultControllerRoute();
app.Run();
Imports IronPdf

' Place license activation before any IronPDF call
License.LicenseKey = "YOUR-LICENSE-KEY"

Dim builder = WebApplication.CreateBuilder(args)
builder.Services.AddControllersWithViews()

' Register ChromePdfRenderer as a singleton so the Chromium engine
' is initialised once and reused across all requests.
builder.Services.AddSingleton(Of ChromePdfRenderer)()

Dim app = builder.Build()
app.MapDefaultControllerRoute()
app.Run()
$vbLabelText   $csharpLabel

Registering ChromePdfRenderer as a singleton is important. The renderer starts an internal Chromium subprocess the first time it is used. If you create a new instance per request you pay that startup cost on every call, which adds hundreds of milliseconds of latency under load. A singleton instance is thread-safe and handles concurrent render requests without additional configuration.

For a broader look at installation options including NuGet.config setups for private feeds, visit the installation overview.

How Do You Generate an Invoice PDF from an HTML String?

The most common on-the-fly use case is generating transactional documents -- invoices, receipts, order confirmations -- where the content changes per request but the layout stays constant.

The pattern is: build an HTML string with interpolated data, pass it to RenderHtmlAsPdf, and return the binary result as a file download.

using IronPdf;
using Microsoft.AspNetCore.Mvc;

public class DocumentController : Controller
{
    private readonly ChromePdfRenderer _renderer;

    public DocumentController(ChromePdfRenderer renderer)
    {
        _renderer = renderer;
    }

    [HttpGet("invoice/{orderId:int}")]
    public IActionResult GetInvoice(int orderId)
    {
        // In a real application, fetch this from your database or order service.
        var order = GetOrderData(orderId);

        string html = $"""
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="utf-8">
                <style>
                    body  {{ font-family: Arial, sans-serif; margin: 40px; color: #333; }}
                    h1   {{ color: #1a56db; }}
                    table {{ width: 100%; border-collapse: collapse; margin-top: 24px; }}
                    th, td {{ padding: 10px 14px; border: 1px solid #d1d5db; text-align: left; }}
                    th   {{ background: #f3f4f6; }}
                    tfoot td {{ font-weight: bold; }}
                </style>
            </head>
            <body>
                <h1>Invoice #{order.InvoiceNumber}</h1>
                <p>Date: {DateTime.UtcNow:yyyy-MM-dd} &nbsp;|&nbsp; Customer: {order.CustomerName}</p>
                <table>
                    <thead><tr><th>Item</th><th>Qty</th><th>Unit Price</th><th>Subtotal</th></tr></thead>
                    <tbody>
                        {string.Join("", order.Items.Select(i =>
                            $"<tr><td>{i.Name}</td><td>{i.Quantity}</td>" +
                            $"<td>${i.UnitPrice:F2}</td><td>${i.Quantity * i.UnitPrice:F2}</td></tr>"))}
                    </tbody>
                    <tfoot>
                        <tr><td colspan="3">Total</td><td>${order.Items.Sum(i => i.Quantity * i.UnitPrice):F2}</td></tr>
                    </tfoot>
                </table>
            </body>
            </html>
            """;

        var pdf = _renderer.RenderHtmlAsPdf(html);
        return File(pdf.BinaryData, "application/pdf", $"invoice-{orderId}.pdf");
    }
}
using IronPdf;
using Microsoft.AspNetCore.Mvc;

public class DocumentController : Controller
{
    private readonly ChromePdfRenderer _renderer;

    public DocumentController(ChromePdfRenderer renderer)
    {
        _renderer = renderer;
    }

    [HttpGet("invoice/{orderId:int}")]
    public IActionResult GetInvoice(int orderId)
    {
        // In a real application, fetch this from your database or order service.
        var order = GetOrderData(orderId);

        string html = $"""
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="utf-8">
                <style>
                    body  {{ font-family: Arial, sans-serif; margin: 40px; color: #333; }}
                    h1   {{ color: #1a56db; }}
                    table {{ width: 100%; border-collapse: collapse; margin-top: 24px; }}
                    th, td {{ padding: 10px 14px; border: 1px solid #d1d5db; text-align: left; }}
                    th   {{ background: #f3f4f6; }}
                    tfoot td {{ font-weight: bold; }}
                </style>
            </head>
            <body>
                <h1>Invoice #{order.InvoiceNumber}</h1>
                <p>Date: {DateTime.UtcNow:yyyy-MM-dd} &nbsp;|&nbsp; Customer: {order.CustomerName}</p>
                <table>
                    <thead><tr><th>Item</th><th>Qty</th><th>Unit Price</th><th>Subtotal</th></tr></thead>
                    <tbody>
                        {string.Join("", order.Items.Select(i =>
                            $"<tr><td>{i.Name}</td><td>{i.Quantity}</td>" +
                            $"<td>${i.UnitPrice:F2}</td><td>${i.Quantity * i.UnitPrice:F2}</td></tr>"))}
                    </tbody>
                    <tfoot>
                        <tr><td colspan="3">Total</td><td>${order.Items.Sum(i => i.Quantity * i.UnitPrice):F2}</td></tr>
                    </tfoot>
                </table>
            </body>
            </html>
            """;

        var pdf = _renderer.RenderHtmlAsPdf(html);
        return File(pdf.BinaryData, "application/pdf", $"invoice-{orderId}.pdf");
    }
}
Imports IronPdf
Imports Microsoft.AspNetCore.Mvc

Public Class DocumentController
    Inherits Controller

    Private ReadOnly _renderer As ChromePdfRenderer

    Public Sub New(renderer As ChromePdfRenderer)
        _renderer = renderer
    End Sub

    <HttpGet("invoice/{orderId:int}")>
    Public Function GetInvoice(orderId As Integer) As IActionResult
        ' In a real application, fetch this from your database or order service.
        Dim order = GetOrderData(orderId)

        Dim html As String = $"
            <!DOCTYPE html>
            <html lang=""en"">
            <head>
                <meta charset=""utf-8"">
                <style>
                    body  {{ font-family: Arial, sans-serif; margin: 40px; color: #333; }}
                    h1   {{ color: #1a56db; }}
                    table {{ width: 100%; border-collapse: collapse; margin-top: 24px; }}
                    th, td {{ padding: 10px 14px; border: 1px solid #d1d5db; text-align: left; }}
                    th   {{ background: #f3f4f6; }}
                    tfoot td {{ font-weight: bold; }}
                </style>
            </head>
            <body>
                <h1>Invoice #{order.InvoiceNumber}</h1>
                <p>Date: {DateTime.UtcNow:yyyy-MM-dd} &nbsp;|&nbsp; Customer: {order.CustomerName}</p>
                <table>
                    <thead><tr><th>Item</th><th>Qty</th><th>Unit Price</th><th>Subtotal</th></tr></thead>
                    <tbody>
                        {String.Join("", order.Items.Select(Function(i) $"<tr><td>{i.Name}</td><td>{i.Quantity}</td>" +
                            $"<td>${i.UnitPrice:F2}</td><td>${i.Quantity * i.UnitPrice:F2}</td></tr>"))}
                    </tbody>
                    <tfoot>
                        <tr><td colspan=""3"">Total</td><td>${order.Items.Sum(Function(i) i.Quantity * i.UnitPrice):F2}</td></tr>
                    </tfoot>
                </table>
            </body>
            </html>
            "

        Dim pdf = _renderer.RenderHtmlAsPdf(html)
        Return File(pdf.BinaryData, "application/pdf", $"invoice-{orderId}.pdf")
    End Function
End Class
$vbLabelText   $csharpLabel

RenderHtmlAsPdf processes the full HTML document -- CSS grid, Flexbox, web fonts, even inline SVG -- using the same Chromium engine that powers Google Chrome. The returned PdfDocument exposes BinaryData (a byte[]) and Stream (a MemoryStream). Passing BinaryData to File() with "application/pdf" and a filename triggers a browser download.

For layouts that need pixel-perfect fidelity, consult the HTML to PDF rendering guide which covers responsive CSS, custom fonts, and JavaScript rendering.

What Does the Generated Invoice PDF Look Like?

Professional PDF invoice showing Invoice #123 dated 2025-11-13 with a styled header section, organized table containing Product A ($10.99) and Product B ($5.49), demonstrating dynamic PDF generation capabilities with custom CSS styling

How Do You Stream a PDF Directly to the Browser Without a Download Dialog?

Serving a PDF inline -- opening it in the browser's built-in viewer rather than downloading it -- requires two small changes: set Content-Disposition to inline and omit the filename from the File() call.

[HttpPost("report/preview")]
public async Task<IActionResult> PreviewReport([FromBody] ReportRequest request)
{
    string html = BuildReportHtml(request);

    var pdfDocument = await _renderer.RenderHtmlAsPdfAsync(html);

    // "inline" tells the browser to display rather than download.
    Response.Headers["Content-Disposition"] = "inline; filename=report.pdf";

    return new FileContentResult(pdfDocument.BinaryData, "application/pdf");
}
[HttpPost("report/preview")]
public async Task<IActionResult> PreviewReport([FromBody] ReportRequest request)
{
    string html = BuildReportHtml(request);

    var pdfDocument = await _renderer.RenderHtmlAsPdfAsync(html);

    // "inline" tells the browser to display rather than download.
    Response.Headers["Content-Disposition"] = "inline; filename=report.pdf";

    return new FileContentResult(pdfDocument.BinaryData, "application/pdf");
}
Imports Microsoft.AspNetCore.Mvc

<HttpPost("report/preview")>
Public Async Function PreviewReport(<FromBody> request As ReportRequest) As Task(Of IActionResult)
    Dim html As String = BuildReportHtml(request)

    Dim pdfDocument = Await _renderer.RenderHtmlAsPdfAsync(html)

    ' "inline" tells the browser to display rather than download.
    Response.Headers("Content-Disposition") = "inline; filename=report.pdf"

    Return New FileContentResult(pdfDocument.BinaryData, "application/pdf")
End Function
$vbLabelText   $csharpLabel

Using the async overload RenderHtmlAsPdfAsync is recommended for ASP.NET Core controllers because it frees the thread-pool thread while Chromium renders, keeping the server responsive under concurrent load.

How Does Memory-Based PDF Generation Work?

Generated PDF report showing 'Monthly Performance Review' title with formatted content demonstrating real-world business document generation including charts, metrics, and detailed analysis sections rendered from HTML template

The pdfDocument.BinaryData byte array lives entirely in managed memory. No intermediate file path is involved. The Content-Disposition header controls whether the PDF is displayed inline or offered as a download -- a browser behaviour defined by the HTTP specification. For a deeper look at the MemoryStream approach, including streaming to Azure Blob Storage, visit the PDF memory stream documentation.

How Do You Generate PDFs from Entity Framework Core Query Results?

Most business applications pull report data from a database rather than constructing it at call time. The pattern below queries Entity Framework Core, builds an HTML table, and returns a PDF -- all within a single controller action.

[HttpGet("report/monthly")]
public async Task<IActionResult> MonthlyReport(int year, int month)
{
    // Pull aggregated transaction data from EF Core.
    var rows = await _dbContext.Transactions
        .Where(t => t.Date.Year == year && t.Date.Month == month)
        .GroupBy(t => t.Category)
        .Select(g => new { Category = g.Key, Count = g.Count(), Total = g.Sum(t => t.Amount) })
        .OrderByDescending(g => g.Total)
        .ToListAsync();

    string tableRows = string.Join("", rows.Select(r =>
        $"<tr><td>{r.Category}</td><td>{r.Count}</td><td>${r.Total:F2}</td></tr>"));

    string html = $"""
        <html><body style="font-family:Arial,sans-serif;padding:32px">
        <h1>Monthly Report -- {month:D2}/{year}</h1>
        <table style="width:100%;border-collapse:collapse">
          <thead>
            <tr style="background:#e5e7eb">
              <th style="padding:8px;border:1px solid #d1d5db">Category</th>
              <th style="padding:8px;border:1px solid #d1d5db">Transactions</th>
              <th style="padding:8px;border:1px solid #d1d5db">Total</th>
            </tr>
          </thead>
          <tbody>{tableRows}</tbody>
        </table>
        </body></html>
        """;

    var pdf = _renderer.RenderHtmlAsPdf(html);
    pdf.MetaData.Title  = $"Monthly Report {month:D2}/{year}";
    pdf.MetaData.Author = "Reporting System";

    return File(pdf.BinaryData, "application/pdf", $"report-{year}-{month:D2}.pdf");
}
[HttpGet("report/monthly")]
public async Task<IActionResult> MonthlyReport(int year, int month)
{
    // Pull aggregated transaction data from EF Core.
    var rows = await _dbContext.Transactions
        .Where(t => t.Date.Year == year && t.Date.Month == month)
        .GroupBy(t => t.Category)
        .Select(g => new { Category = g.Key, Count = g.Count(), Total = g.Sum(t => t.Amount) })
        .OrderByDescending(g => g.Total)
        .ToListAsync();

    string tableRows = string.Join("", rows.Select(r =>
        $"<tr><td>{r.Category}</td><td>{r.Count}</td><td>${r.Total:F2}</td></tr>"));

    string html = $"""
        <html><body style="font-family:Arial,sans-serif;padding:32px">
        <h1>Monthly Report -- {month:D2}/{year}</h1>
        <table style="width:100%;border-collapse:collapse">
          <thead>
            <tr style="background:#e5e7eb">
              <th style="padding:8px;border:1px solid #d1d5db">Category</th>
              <th style="padding:8px;border:1px solid #d1d5db">Transactions</th>
              <th style="padding:8px;border:1px solid #d1d5db">Total</th>
            </tr>
          </thead>
          <tbody>{tableRows}</tbody>
        </table>
        </body></html>
        """;

    var pdf = _renderer.RenderHtmlAsPdf(html);
    pdf.MetaData.Title  = $"Monthly Report {month:D2}/{year}";
    pdf.MetaData.Author = "Reporting System";

    return File(pdf.BinaryData, "application/pdf", $"report-{year}-{month:D2}.pdf");
}
Imports Microsoft.AspNetCore.Mvc
Imports System.Threading.Tasks
Imports System.Linq

<HttpGet("report/monthly")>
Public Async Function MonthlyReport(year As Integer, month As Integer) As Task(Of IActionResult)
    ' Pull aggregated transaction data from EF Core.
    Dim rows = Await _dbContext.Transactions _
        .Where(Function(t) t.Date.Year = year AndAlso t.Date.Month = month) _
        .GroupBy(Function(t) t.Category) _
        .Select(Function(g) New With {Key .Category = g.Key, Key .Count = g.Count(), Key .Total = g.Sum(Function(t) t.Amount)}) _
        .OrderByDescending(Function(g) g.Total) _
        .ToListAsync()

    Dim tableRows As String = String.Join("", rows.Select(Function(r) $"<tr><td>{r.Category}</td><td>{r.Count}</td><td>${r.Total:F2}</td></tr>"))

    Dim html As String = $"
        <html><body style='font-family:Arial,sans-serif;padding:32px'>
        <h1>Monthly Report -- {month:D2}/{year}</h1>
        <table style='width:100%;border-collapse:collapse'>
          <thead>
            <tr style='background:#e5e7eb'>
              <th style='padding:8px;border:1px solid #d1d5db'>Category</th>
              <th style='padding:8px;border:1px solid #d1d5db'>Transactions</th>
              <th style='padding:8px;border:1px solid #d1d5db'>Total</th>
            </tr>
          </thead>
          <tbody>{tableRows}</tbody>
        </table>
        </body></html>
        "

    Dim pdf = _renderer.RenderHtmlAsPdf(html)
    pdf.MetaData.Title = $"Monthly Report {month:D2}/{year}"
    pdf.MetaData.Author = "Reporting System"

    Return File(pdf.BinaryData, "application/pdf", $"report-{year}-{month:D2}.pdf")
End Function
$vbLabelText   $csharpLabel

Setting pdf.MetaData.Title and pdf.MetaData.Author embeds that information into the PDF's document properties, which is useful for compliance tracking and document management systems. For more sophisticated report layouts, consider CSS print styles, explicit page breaks, and embedded chart images.

How Do You Apply Headers, Footers, and Security to Generated PDFs?

Production documents often require running headers with the document title, page-number footers, and access controls that prevent unauthorized printing or copying. IronPDF's ChromePdfRenderOptions covers all of these requirements.

[HttpPost("document/secured")]
public async Task<IActionResult> GenerateSecuredDocument([FromBody] SecuredDocRequest request)
{
    var renderOptions = new ChromePdfRenderOptions
    {
        PaperSize       = PdfPaperSize.A4,
        MarginTop       = 45,
        MarginBottom    = 45,
        MarginLeft      = 25,
        MarginRight     = 25,
        EnableJavaScript = true,
        WaitFor         = new WaitFor { RenderDelay = 500 }
    };

    renderOptions.TextHeader = new TextHeaderFooter
    {
        CenterText       = request.DocumentTitle,
        DrawDividerLine  = true,
        FontSize         = 11
    };

    renderOptions.TextFooter = new TextHeaderFooter
    {
        LeftText  = "{date} {time}",
        RightText = "Page {page} of {total-pages}",
        FontSize  = 9
    };

    _renderer.RenderingOptions = renderOptions;

    var pdf = await _renderer.RenderHtmlAsPdfAsync(request.HtmlContent);

    if (request.RequirePassword)
    {
        pdf.SecuritySettings.OwnerPassword         = request.OwnerPassword;
        pdf.SecuritySettings.UserPassword          = request.UserPassword;
        pdf.SecuritySettings.AllowUserPrinting     = PdfPrintSecurity.NoPrint;
        pdf.SecuritySettings.AllowUserCopyPasteContent = false;
    }

    return File(pdf.BinaryData, "application/pdf", $"{request.FileName}.pdf");
}
[HttpPost("document/secured")]
public async Task<IActionResult> GenerateSecuredDocument([FromBody] SecuredDocRequest request)
{
    var renderOptions = new ChromePdfRenderOptions
    {
        PaperSize       = PdfPaperSize.A4,
        MarginTop       = 45,
        MarginBottom    = 45,
        MarginLeft      = 25,
        MarginRight     = 25,
        EnableJavaScript = true,
        WaitFor         = new WaitFor { RenderDelay = 500 }
    };

    renderOptions.TextHeader = new TextHeaderFooter
    {
        CenterText       = request.DocumentTitle,
        DrawDividerLine  = true,
        FontSize         = 11
    };

    renderOptions.TextFooter = new TextHeaderFooter
    {
        LeftText  = "{date} {time}",
        RightText = "Page {page} of {total-pages}",
        FontSize  = 9
    };

    _renderer.RenderingOptions = renderOptions;

    var pdf = await _renderer.RenderHtmlAsPdfAsync(request.HtmlContent);

    if (request.RequirePassword)
    {
        pdf.SecuritySettings.OwnerPassword         = request.OwnerPassword;
        pdf.SecuritySettings.UserPassword          = request.UserPassword;
        pdf.SecuritySettings.AllowUserPrinting     = PdfPrintSecurity.NoPrint;
        pdf.SecuritySettings.AllowUserCopyPasteContent = false;
    }

    return File(pdf.BinaryData, "application/pdf", $"{request.FileName}.pdf");
}
Imports System.Threading.Tasks
Imports Microsoft.AspNetCore.Mvc

<HttpPost("document/secured")>
Public Async Function GenerateSecuredDocument(<FromBody> request As SecuredDocRequest) As Task(Of IActionResult)
    Dim renderOptions As New ChromePdfRenderOptions With {
        .PaperSize = PdfPaperSize.A4,
        .MarginTop = 45,
        .MarginBottom = 45,
        .MarginLeft = 25,
        .MarginRight = 25,
        .EnableJavaScript = True,
        .WaitFor = New WaitFor With {.RenderDelay = 500}
    }

    renderOptions.TextHeader = New TextHeaderFooter With {
        .CenterText = request.DocumentTitle,
        .DrawDividerLine = True,
        .FontSize = 11
    }

    renderOptions.TextFooter = New TextHeaderFooter With {
        .LeftText = "{date} {time}",
        .RightText = "Page {page} of {total-pages}",
        .FontSize = 9
    }

    _renderer.RenderingOptions = renderOptions

    Dim pdf = Await _renderer.RenderHtmlAsPdfAsync(request.HtmlContent)

    If request.RequirePassword Then
        pdf.SecuritySettings.OwnerPassword = request.OwnerPassword
        pdf.SecuritySettings.UserPassword = request.UserPassword
        pdf.SecuritySettings.AllowUserPrinting = PdfPrintSecurity.NoPrint
        pdf.SecuritySettings.AllowUserCopyPasteContent = False
    End If

    Return File(pdf.BinaryData, "application/pdf", $"{request.FileName}.pdf")
End Function
$vbLabelText   $csharpLabel

The WaitFor.RenderDelay setting is particularly useful when your HTML includes charting libraries such as Chart.js or ApexCharts that finish drawing asynchronously. Setting a delay of 300-500 ms ensures Chromium captures the final rendered state. For documents that need to meet archival standards, combine the approach above with PDF/A compliance and digital signatures.

The {page} and {total-pages} tokens in footer text are resolved automatically by IronPDF at render time. Additional header and footer options include HTML-based headers for logo placement and per-section override capabilities.

What Rendering Options Are Available?

The table below summarises the most useful ChromePdfRenderOptions properties for on-the-fly generation:

Commonly Used ChromePdfRenderOptions Properties
Property Type Purpose
PaperSizePdfPaperSizeSets page dimensions (A4, Letter, Legal, custom)
MarginTop / MarginBottomint (mm)Controls printable area spacing
EnableJavaScriptboolAllows JS execution before capture
WaitFor.RenderDelayint (ms)Delays capture for async rendering
TextHeader / TextFooterTextHeaderFooterRunning page headers and footers
HtmlHeader / HtmlFooterHtmlHeaderFooterHTML-formatted headers/footers with images
GrayScaleboolOutputs monochrome PDF
FitToPaperWidthboolScales wide content to fit page

What Are the Performance Best Practices for High-Volume PDF Generation?

When a single server handles hundreds of concurrent PDF requests, a few architectural decisions have an outsized impact on throughput and latency.

Singleton renderer registration. As shown in the installation section, registering ChromePdfRenderer as a singleton in the DI container avoids the cost of starting a new Chromium subprocess per request. According to Microsoft's ASP.NET Core performance guidance, minimising object allocations and reusing expensive resources are the two most impactful optimisations available.

Always use async. RenderHtmlAsPdfAsync returns a Task<PdfDocument> and suspends the controller thread while Chromium works. This frees the thread pool to handle other incoming requests in parallel, which is why the async documentation recommends this overload for web hosts. The synchronous overload is appropriate only for console tools or background services where thread blocking is acceptable.

Stream directly, skip the intermediate array when possible. For large PDFs, .Stream can be written directly to the response body without materialising the full byte array:

[HttpGet("document/large")]
public IActionResult StreamLargeDocument(int documentId)
{
    string html = BuildLargeDocumentHtml(documentId);
    var pdf = _renderer.RenderHtmlAsPdf(html);

    // Stream.Position is already at 0; no seek needed.
    return File(pdf.Stream, "application/pdf", $"document-{documentId}.pdf");
}
[HttpGet("document/large")]
public IActionResult StreamLargeDocument(int documentId)
{
    string html = BuildLargeDocumentHtml(documentId);
    var pdf = _renderer.RenderHtmlAsPdf(html);

    // Stream.Position is already at 0; no seek needed.
    return File(pdf.Stream, "application/pdf", $"document-{documentId}.pdf");
}
Imports Microsoft.AspNetCore.Mvc

<HttpGet("document/large")>
Public Function StreamLargeDocument(documentId As Integer) As IActionResult
    Dim html As String = BuildLargeDocumentHtml(documentId)
    Dim pdf = _renderer.RenderHtmlAsPdf(html)

    ' Stream.Position is already at 0; no seek needed.
    Return File(pdf.Stream, "application/pdf", $"document-{documentId}.pdf")
End Function
$vbLabelText   $csharpLabel

Dispose after use. PdfDocument implements IDisposable. Wrapping it in a using statement releases the underlying memory buffer promptly, which matters when generating many large PDFs back to back:

using var pdf = _renderer.RenderHtmlAsPdf(html);
byte[] data = pdf.BinaryData;
// pdf is disposed here; data is safely copied to the local array.
return File(data, "application/pdf", "output.pdf");
using var pdf = _renderer.RenderHtmlAsPdf(html);
byte[] data = pdf.BinaryData;
// pdf is disposed here; data is safely copied to the local array.
return File(data, "application/pdf", "output.pdf");
Imports System.IO

Using pdf = _renderer.RenderHtmlAsPdf(html)
    Dim data As Byte() = pdf.BinaryData
    ' pdf is disposed here; data is safely copied to the local array.
    Return File(data, "application/pdf", "output.pdf")
End Using
$vbLabelText   $csharpLabel

For cloud deployment guidance covering Azure, AWS, Docker, and Linux environments, the IronPDF documentation provides environment-specific configuration notes. If the first render after startup is slow, consult the warm-up and caching guide for strategies to pre-initialise the renderer before the first user request arrives.

How Do You Add a Watermark to a Generated PDF?

A text or image watermark can be added to every page of the generated document before streaming:

[HttpGet("document/draft/{id:int}")]
public IActionResult GetDraftDocument(int id)
{
    string html = BuildDocumentHtml(id);
    var pdf = _renderer.RenderHtmlAsPdf(html);

    // Stamp "DRAFT" diagonally across every page.
    pdf.ApplyWatermark(
        "<h1 style='color:rgba(200,0,0,0.25);transform:rotate(-45deg)'>DRAFT</h1>",
        rotation: 45,
        opacity: 30
    );

    return File(pdf.BinaryData, "application/pdf", $"draft-{id}.pdf");
}
[HttpGet("document/draft/{id:int}")]
public IActionResult GetDraftDocument(int id)
{
    string html = BuildDocumentHtml(id);
    var pdf = _renderer.RenderHtmlAsPdf(html);

    // Stamp "DRAFT" diagonally across every page.
    pdf.ApplyWatermark(
        "<h1 style='color:rgba(200,0,0,0.25);transform:rotate(-45deg)'>DRAFT</h1>",
        rotation: 45,
        opacity: 30
    );

    return File(pdf.BinaryData, "application/pdf", $"draft-{id}.pdf");
}
<AttributeUsage(AttributeTargets.Method, Inherited:=True, AllowMultiple:=False)>
Public Class HttpGetAttribute
    Inherits Attribute

    Public Sub New(route As String)
    End Sub
End Class

<HttpGet("document/draft/{id:int}")>
Public Function GetDraftDocument(id As Integer) As IActionResult
    Dim html As String = BuildDocumentHtml(id)
    Dim pdf = _renderer.RenderHtmlAsPdf(html)

    ' Stamp "DRAFT" diagonally across every page.
    pdf.ApplyWatermark(
        "<h1 style='color:rgba(200,0,0,0.25);transform:rotate(-45deg)'>DRAFT</h1>",
        rotation:=45,
        opacity:=30
    )

    Return File(pdf.BinaryData, "application/pdf", $"draft-{id}.pdf")
End Function
$vbLabelText   $csharpLabel

For full watermark configuration options including image watermarks and per-page control, see the watermark documentation.

NuGet Install with NuGet

PM >  Install-Package IronPdf

Check out IronPDF on NuGet for quick installation. With over 10 million downloads, it’s transforming PDF development with C#. You can also download the DLL or Windows installer.

What Are Your Next Steps?

Dynamic PDF generation in ASP.NET Core follows a consistent pattern: build your HTML, call RenderHtmlAsPdf or its async overload, and return the result via a FileResult. IronPDF handles everything in between -- Chromium rendering, CSS application, JavaScript execution -- without requiring disk I/O at any stage.

From here, you can explore several directions depending on your application requirements. If your PDFs need to combine multiple source documents, the merge and split guide covers joining existing PDFs with freshly rendered pages. If you need users to fill and submit forms embedded in a PDF, the interactive forms documentation shows how to create and read field values. For regulated industries, PDF/A compliance and PDF/UA accessibility ensure documents meet archival and accessibility standards.

If you are evaluating IronPDF against alternatives, the iText vs IronPDF comparison provides a side-by-side technical breakdown. When you are ready to move to production, purchase a license to unlock all features and gain access to priority engineering support. The full API reference documents every class and method discussed in this guide.

For questions or issues during implementation, the engineering support team is available to help. For platform-specific notes on Blazor Server or MAUI, dedicated guides cover the configuration differences for each host model.

Frequently Asked Questions

How can I generate PDFs dynamically in ASP.NET Core?

You can use IronPDF to generate PDFs dynamically in ASP.NET Core without saving files to disk. It allows you to stream PDFs directly to browsers.

What are the benefits of using IronPDF for PDF generation?

IronPDF provides a powerful rendering engine that enables dynamic PDF creation directly within your .NET Core projects, ensuring instant PDF generation without the need for server-side storage.

Can IronPDF be used to create invoices and reports?

Yes, IronPDF is suitable for creating various types of documents such as invoices, reports, and certificates, all generated on-the-fly in ASP.NET Core applications.

Is server-side storage necessary when using IronPDF?

No, IronPDF allows you to generate and stream PDFs directly to browsers without the need for server-side storage, making it efficient and fast.

What kind of applications can benefit from on-the-fly PDF generation?

Modern web applications, particularly those requiring real-time document creation like invoicing systems and reporting tools, can greatly benefit from on-the-fly PDF generation provided by IronPDF.

Does IronPDF support .NET Core projects?

Yes, IronPDF is fully compatible with .NET Core projects, allowing developers to integrate PDF generation capabilities seamlessly into their applications.

Curtis Chau
Technical Writer

Curtis Chau holds a Bachelor’s degree in Computer Science (Carleton University) and specializes in front-end development with expertise in Node.js, TypeScript, JavaScript, and React. Passionate about crafting intuitive and aesthetically pleasing user interfaces, Curtis enjoys working with modern frameworks and creating well-structured, visually appealing manuals.

...

Read More

Iron Support Team

We're online 24 hours, 5 days a week.
Chat
Email
Call Me