QuestPDF is a code-first PDF library for .NET: you describe a document with a fluent C# API and it renders a PDF, with no HTML pipeline and no headless browser. This guide goes from install to a real, data-driven report, covers the layout model the whole API is built on, and is honest about where QuestPDF stops. Everything targets QuestPDF 2026.5.0 and runs on current .NET (it ships for net6.0 and netstandard2.0, so it runs on .NET 8 and .NET 10).
How do I install and license QuestPDF?
One NuGet package, no native dependencies to provision:
dotnet add package QuestPDF
Then set the license tier once at startup, before any document is generated. Community is free for individuals and for companies under $1M USD annual gross revenue:
using QuestPDF.Infrastructure;
QuestPDF.Settings.License = LicenseType.Community;
The quick-start guide is the canonical reference if you want the official walkthrough alongside this one.
Write your first PDF end to end
A complete, runnable program. Note the shape: a document holds a page, and the page has a header, content, and footer, each a chained call.
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
QuestPDF.Settings.License = LicenseType.Community;
Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.DefaultTextStyle(x => x.FontSize(12));
page.Header().Text("Monthly Report").FontSize(20).Bold();
page.Content().PaddingVertical(15).Column(column =>
{
column.Spacing(8);
column.Item().Text("Generated from C#, no HTML involved.");
column.Item().Text($"Date: {DateTime.UtcNow:yyyy-MM-dd}");
});
page.Footer().AlignCenter().Text(text =>
{
text.Span("Page ");
text.CurrentPageNumber();
text.Span(" of ");
text.TotalPages();
});
});
})
.GeneratePdf("report.pdf");
This writes report.pdf to disk. The output paginates automatically and the footer page numbers update across pages.
How the container model works
Everything in QuestPDF is a container you configure by chaining methods, and containers nest. page.Content() returns a container; calling .Padding(10).Background(Colors.Grey.Lighten4) wraps it, and .Column(...) fills it with child containers. Once that clicks, the entire API reads the same way: configure a container, then place children inside it. That single idea is what the API reference is built around.
Composing layout with Column, Row, and Text
Three primitives cover most documents:
-
Columnstacks items vertically, withSpacingbetween them. -
Rowplaces items horizontally; size them withRelativeItem(proportional) andConstantItem(points)(fixed). -
Textrenders styled runs, including dynamic spans like page numbers.
page.Content().Column(column =>
{
column.Spacing(10);
column.Item().Row(row =>
{
row.RelativeItem().Text("Invoice #1042").Bold();
row.ConstantItem(140).AlignRight().Text("2026-06-04");
});
column.Item().Text("Thank you for your business.");
});
This produces a two-column header row with the label left-aligned and the date right-aligned, followed by a full-width text line. For tables specifically (line items, statements), QuestPDF has a dedicated table API worth its own read.
Reusing layout with components
For layout you repeat across documents (a letterhead, a signature block), QuestPDF supports components: a class implementing IComponent that drops into any container.
public class Letterhead : IComponent
{
public void Compose(IContainer container)
{
container.Column(column =>
{
column.Item().Text("Acme Corp").Bold().FontSize(16);
column.Item().Text("123 Example Street, Springfield");
});
}
}
// Use it anywhere a container is expected:
page.Header().Component(new Letterhead());
The Letterhead component renders its two-line block wherever you call .Component(new Letterhead()). Components keep shared layout in one place instead of copying the same fluent calls into every document.
Styling text, fonts, and color
Styling is chained onto any text run, and a page-wide default keeps things consistent:
page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Lato"));
column.Item().Text("Heading").FontSize(18).Bold().FontColor(Colors.Blue.Darken2);
column.Item().Text(t =>
{
t.Span("Status: ");
t.Span("PAID").Bold().FontColor(Colors.Green.Darken1);
});
On Linux containers, make sure the font you name is actually installed (or bundle it), or QuestPDF falls back to a default face.
Setting page size, margins, and orientation
page.Size(PageSizes.A4.Landscape());
page.MarginHorizontal(2, Unit.Centimetre);
page.MarginVertical(1.5f, Unit.Centimetre);
PageSizes includes the common presets (A4, Letter, and so on), and .Landscape() flips any of them.
Generate a report from your data
Real documents come from a model. Here a list of rows becomes a styled report section:
public byte[] BuildReport(IReadOnlyList<(string Label, decimal Value)> rows)
{
QuestPDF.Settings.License = LicenseType.Community;
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.Header().Text("Quarterly Summary").FontSize(18).Bold();
page.Content().PaddingTop(10).Column(column =>
{
column.Spacing(6);
foreach (var (label, value) in rows)
{
column.Item().Row(row =>
{
row.RelativeItem().Text(label);
row.ConstantItem(100).AlignRight().Text($"${value:0.00}");
});
}
});
page.Footer().AlignCenter().Text(t => { t.CurrentPageNumber(); t.Span(" / "); t.TotalPages(); });
});
}).GeneratePdf();
}
GeneratePdf() with no argument returns a byte[], which is exactly what an API endpoint returns to the client.
Where QuestPDF stops: HTML, reading, and converting
Worth stating plainly, because it decides whether QuestPDF fits: it is generation-only. It does not render HTML, CSS, or Razor, and it cannot open, read, extract text from, edit, merge, or convert an existing PDF. Those are deliberate scope choices, not bugs, and for code-first document generation they do not matter. They matter when your input is HTML or an existing PDF.
When your source of truth is HTML, not C
If the document already lives as an HTML or Razor template (a designer-owned invoice, an existing web view), reproducing it in fluent code is real work, and QuestPDF cannot consume the markup directly.
IronPDF is a commercial library that renders HTML or Razor markup to PDF through a Chromium engine, so the template you already maintain stays the source of truth. It renders a Razor view straight to PDF and takes a raw HTML string just as readily. A workflow using it looks like this:
using IronPdf;
IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";
var renderer = new ChromePdfRenderer();
// Render an HTML string; a rendered Razor view is passed the same way
var pdf = renderer.RenderHtmlAsPdf("<h1>Invoice #1042</h1><p>Total: $1,200.00</p>");
pdf.SaveAs("invoice.pdf");
This produces a PDF from the HTML string using Chromium's rendering engine. The trade-offs compared to QuestPDF:
- Existing HTML and Razor templates work as inputs, which QuestPDF does not support.
- It requires a paid commercial license; there is no free tier for production use.
- The Chromium engine adds over 100 MB to the deployment footprint.
- On Linux, native dependencies (
libgdiplus, system fonts) must be installed on the host or container.
For documents you define in code, QuestPDF carries none of those costs: pure managed .NET, no native binary, no commercial license required under the community revenue threshold.
Running QuestPDF on Linux, Docker, and in ASP.NET
QuestPDF is managed code with no native binary to ship, so deployment is the same as the rest of your app. In ASP.NET Core, return the bytes directly:
app.MapGet("/report", () =>
{
var pdf = BuildReport(GetRows());
return Results.File(pdf, "application/pdf", "report.pdf");
});
No headless browser to install, no Chromium layer, no extra container configuration. It deploys cleanly to Linux, Docker, and serverless hosts.
Common pitfalls
-
License set too late. Set
QuestPDF.Settings.Licenseat startup, before the firstDocument.Create, or generation throws. - Naming a font that is not installed. On slim Linux images, reference a font that exists on the host or bundle it.
- Expecting HTML. Passing an HTML string to QuestPDF does nothing useful; it is not an HTML renderer.
- Over-nesting cells and items. Deep per-item layouts slow large documents; keep item content shallow.
FAQ
Does QuestPDF need a purchased license key in code?
You set a LicenseType (Community, Professional, or Enterprise). Community needs no purchased key under the $1M revenue threshold.
Can QuestPDF render HTML or a Razor view?
No. It is code-first by design. For HTML input, rebuild it with the fluent API or use an HTML-rendering library.
Does it run on Linux and in containers?
Yes. Managed .NET, no native binary, so it deploys like any other dependency.
Which .NET versions are supported?
It targets net6.0 and netstandard2.0, so it runs on .NET 8 and .NET 10.
How do I preview a document while developing?
Use the QuestPDF Companion app for live preview and hot reload, covered in a separate guide.
What kind of documents are you building?
Are you generating reports from structured data, converting existing HTML invoices, or something else? The answer changes which library fits, and I'm curious what real-world use cases people are hitting with .NET PDF generation in 2026.
Takeaways
QuestPDF is the strongest free option for code-first PDF generation in .NET: one package, no native runtime, no commercial license under the community threshold. Learn the container model and the three primitives (Column, Row, Text) and you can build most business documents, with tables and images layered on top. Reach for an HTML renderer when your content genuinely starts life as HTML, or when you need to read or edit existing PDFs, neither of which QuestPDF handles.
If you're in the HTML-to-PDF camp and want to test against your own templates, IronPDF offers a 30-day trial.
Top comments (0)