How to Easily Create PDF Documents in ASP.NET Core
A simple, friendly guide to creating PDF documents in ASP.NET Core with QuestPDF, with clear code, diagrams, tables, and tips for invoices and reports.
A receipt from the chai stall
Imagine you stop at a chai stall near your school. You buy two cups of chai and a packet of biscuits. The uncle at the stall quickly writes a small bill on a paper pad: the date, what you bought, the price of each item, and the total at the bottom.
That little paper bill is special. It looks the same to everyone. If your friend looks at it, the numbers do not move. If you keep it in your bag for a week, it still reads the same. The shop name stays at the top, and the total stays at the bottom.
A PDF (Portable Document Format) is the computer version of that paper bill. Once you make a PDF, it looks the same on every phone, laptop, and printer in the world. That is why companies use PDFs for invoices, salary slips, report cards, tickets, and certificates.
In this guide, you will learn how to create a PDF from scratch in ASP.NET Core using a friendly library called QuestPDF. We will keep the code small. By the end, your web app will be able to hand a real PDF bill back to the browser, just like the chai uncle hands you that paper.
What we will build
We will build a tiny web app that makes an invoice PDF. Think of an invoice as a polished version of the chai bill: a header with the shop name, a table of items, and a total at the bottom.
The plan has three small steps:
- Describe the order (who bought what).
- Ask QuestPDF to draw that order onto a page.
- Send the finished PDF back to the user.
Let us walk through each step slowly.
Why QuestPDF?
There are many PDF tools for .NET. We pick QuestPDF because it is the kindest one for beginners. Here is a quick compare.
| Tool | How you describe the page | Good for beginners? | Cost for small projects |
|---|---|---|---|
| QuestPDF | Plain C# code | Yes, very friendly | Free (Community MIT) |
| IronPDF | Turn HTML into PDF | Medium | Paid, free trial |
| Syncfusion | HTML or low-level API | Medium | Free for tiny firms |
| iText 7 | Low-level building blocks | Harder | AGPL or paid |
With QuestPDF, you do not learn a new template language. You write the same C# you already know. You say "put a header here, a table there, a total at the bottom," and the library does the spacing and page breaks for you.
Choosing your PDF path
Steps
Need a PDF
You want a fixed document
Have HTML already?
A web page you like
Use HTML-to-PDF
IronPDF or Syncfusion
Use QuestPDF
Build it in C# code
A quick word on the license
QuestPDF is free for most learners and small teams, but it is fair to be clear about the rules so nobody is surprised later.
- Free under the Community MIT License if your company earns less than $1M USD a year.
- Free for students, hobby projects, open-source, and charity work.
- Paid (Professional or Enterprise) only when a larger company uses it.
You tell QuestPDF which license you are using one time, when your app starts. We will do that in the next section. This is a one-line setting, not a hard task.
Step 1 — Install and set the license
First, create a new ASP.NET Core Web API project and add the QuestPDF package.
// In your terminal, inside the project folder:
// dotnet add package QuestPDF
// Then, in Program.cs, near the top, set the license once:
using QuestPDF.Infrastructure;
// This line MUST run before you generate any PDF.
QuestPDF.Settings.License = LicenseType.Community;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();That single license line is important. If you forget it, QuestPDF will stop and remind you. So set it once and move on.
Step 2 — Describe the order
Before we draw anything, we need some data. Let us make a small class that holds an order. Think of this as writing down what the customer bought.
public class OrderLine
{
public string Item { get; set; } = "";
public int Quantity { get; set; }
public decimal Price { get; set; } // price for one item
public decimal Total => Quantity * Price;
}
public class Order
{
public string CustomerName { get; set; } = "";
public DateTime Date { get; set; } = DateTime.Today;
public List<OrderLine> Lines { get; set; } = new();
public decimal GrandTotal => Lines.Sum(l => l.Total);
}Nothing fancy here. An Order has a customer name, a date, and a list of lines. Each line knows its own total, and the order knows the grand total. This is just data sitting in memory, waiting to be drawn.
Step 3 — Draw the PDF
Now the fun part. We tell QuestPDF how the page should look. In QuestPDF, a document has three areas you care about most: the header (top), the content (middle), and the footer (bottom). It is just like the chai bill: shop name on top, items in the middle, a thank-you line at the bottom.
The three parts of a page
Steps
Header
Shop name and date
Content
Table of items
Footer
Page number
Here is the code that builds the invoice. Read it slowly. Each .Text(...) or .Table(...) is one piece you are placing on the page.
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
public class InvoiceDocument : IDocument
{
private readonly Order _order;
public InvoiceDocument(Order order) => _order = order;
public void Compose(IDocumentContainer container)
{
container.Page(page =>
{
page.Margin(40);
page.Size(PageSizes.A4);
// 1) Header: shop name and date
page.Header().Column(col =>
{
col.Item().Text("Chai Stall Invoice").FontSize(22).Bold();
col.Item().Text($"Customer: {_order.CustomerName}");
col.Item().Text($"Date: {_order.Date:dd MMM yyyy}");
});
// 2) Content: the table of items
page.Content().PaddingVertical(15).Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn(3); // Item
columns.RelativeColumn(); // Qty
columns.RelativeColumn(); // Price
columns.RelativeColumn(); // Total
});
// table header row
table.Header(header =>
{
header.Cell().Text("Item").Bold();
header.Cell().Text("Qty").Bold();
header.Cell().Text("Price").Bold();
header.Cell().Text("Total").Bold();
});
// one row per item
foreach (var line in _order.Lines)
{
table.Cell().Text(line.Item);
table.Cell().Text(line.Quantity.ToString());
table.Cell().Text($"{line.Price:0.00}");
table.Cell().Text($"{line.Total:0.00}");
}
});
// 3) Footer: grand total and page number
page.Footer().Column(col =>
{
col.Item().AlignRight()
.Text($"Grand Total: {_order.GrandTotal:0.00}").Bold();
col.Item().AlignCenter()
.Text(t => t.CurrentPageNumber());
});
});
}
}Look at how readable that is. You can almost speak it out loud: "Header has the shop name. Content has a table with four columns. For each line, add a row. Footer shows the grand total and the page number." QuestPDF handles the hard parts, like deciding when to start a new page if the table gets too long.
Step 4 — Send the PDF back to the user
Now we wire it into a controller. The job here is small: build the order, turn the document into bytes, and return those bytes as a file.
using Microsoft.AspNetCore.Mvc;
using QuestPDF.Fluent;
[ApiController]
[Route("invoices")]
public class InvoiceController : ControllerBase
{
[HttpGet("sample")]
public IActionResult GetSample()
{
var order = new Order
{
CustomerName = "Aarav",
Lines =
{
new OrderLine { Item = "Chai", Quantity = 2, Price = 10 },
new OrderLine { Item = "Biscuits", Quantity = 1, Price = 20 }
}
};
// Turn the document into PDF bytes.
byte[] pdf = new InvoiceDocument(order).GeneratePdf();
// Hand the bytes back as a downloadable file.
return File(pdf, "application/pdf", "invoice.pdf");
}
}That is the whole journey. Run the app, open /invoices/sample in your browser, and you will get a real PDF invoice with a header, a table, and a total. The chai uncle would be proud.
Adding a logo
A bill feels official with a logo on top. QuestPDF can draw an image from bytes. You load your logo file once and place it in the header.
// Load the image once (for example, when the app starts).
byte[] logoBytes = File.ReadAllBytes("Assets/logo.png");
// Inside the header column:
col.Item().Width(120).Image(logoBytes);Keep logos small (a PNG under 100 KB is plenty). A huge image makes the PDF heavy and slow to open on a phone.
Common beginner mistakes
When you are new, a few small things trip people up. Here is a friendly table to keep you safe.
| Mistake | What you see | How to fix it |
|---|---|---|
| Forgot the license line | App throws on first PDF | Set License = LicenseType.Community in Program.cs |
| Wrong content type | Browser shows raw text | Return "application/pdf" |
| Huge image as logo | PDF is slow and large | Resize the image first |
| Blocking the thread for big files | App feels frozen | Stream the PDF or run it as a background job |
Money shown as 10 not 10.00 | Looks odd | Format with "0.00" |
Most of these are quick one-line fixes. Do not worry, everyone hits them once.
Making it faster for big reports
Small invoices are quick. But what if you must build a 500-page report? Holding the whole PDF in memory as a byte array can be wasteful. QuestPDF lets you stream the PDF straight into the response, so memory stays low.
[HttpGet("big-report")]
public IActionResult BigReport()
{
var order = BuildHugeOrder(); // imagine many lines
var document = new InvoiceDocument(order);
// Stream directly to the HTTP response body.
Response.ContentType = "application/pdf";
document.GeneratePdf(Response.Body);
return new EmptyResult();
}For very heavy jobs, like generating monthly slips for thousands of staff, do not make the user wait. Generate the PDFs in a background worker and email a link when they are ready. The user clicks once, then gets on with their day.
Small vs big PDF jobs
Steps
Is the PDF small?
One invoice
Generate inline
Return bytes now
Stream or background
Big or many files
A note on testing your PDFs
You cannot eyeball every PDF by hand forever. A simple trick is to save the bytes to a file in a test and check the length is greater than zero, and that the first bytes start with %PDF. Every real PDF begins with those four characters, like a secret handshake. If that handshake is there, you know QuestPDF produced a valid file.
byte[] pdf = new InvoiceDocument(order).GeneratePdf();
// A real PDF starts with the bytes for "%PDF".
bool looksValid = pdf.Length > 4
&& pdf[0] == 0x25 // %
&& pdf[1] == 0x50 // P
&& pdf[2] == 0x44 // D
&& pdf[3] == 0x46; // FThis tiny check catches the big mistakes, like accidentally returning an empty array. For real apps you might also open the file and read it once by hand to make sure the layout looks right.
When QuestPDF is not the right fit
QuestPDF is wonderful, but it is not the only answer. If your team already has beautiful HTML pages with lots of CSS, and you want the PDF to match them exactly, an HTML-to-PDF tool (like IronPDF or Syncfusion) may save you time, because it reuses your existing pages. The trade-off is that those tools cost money for larger companies and can be heavier to run, since some of them start a hidden browser to render the page.
So the simple rule is this: if you are building the document fresh, reach for QuestPDF. If you are reprinting a web page you already love, reach for an HTML-to-PDF tool.
References and further reading
- QuestPDF — Official Documentation
- QuestPDF License and Pricing
- QuestPDF on GitHub
- QuestPDF on NuGet
- PDF Generation using QuestPDF in ASP.NET Core (community guide)
Quick recap
- A PDF is like a paper bill: it looks the same everywhere.
- QuestPDF lets you build a PDF using plain C# code, which is great for beginners.
- Set the license once in
Program.cs(Community MIT is free for small teams and learners). - Make a small data class for your order, then describe the header, content, and footer.
- Turn the document into bytes with
GeneratePdf()and returnFile(bytes, "application/pdf", "name.pdf"). - Add a logo with
Image(bytes), and keep images small. - For big reports, stream the PDF or build it in the background so the user never waits.
- Test that the bytes start with
%PDFto catch empty or broken files early.
Related Posts
How to Create and Convert PDF Documents in ASP.NET Core
Learn to create and convert PDF documents in ASP.NET Core using QuestPDF and HTML-to-PDF tools, with simple code, diagrams, and clear advice.
Building Async APIs in ASP.NET Core the Right Way
Learn to build fast, safe async APIs in ASP.NET Core: async/await, CancellationToken, avoiding .Result deadlocks, and thread pool tips.
The 3 C# PDF Libraries Every Developer Must Know
A friendly guide to QuestPDF, PDFsharp, and iText for C#. Learn what each does, their licensing, code examples, and how to pick the right one.
Getting Started with Hot Chocolate GraphQL in ASP.NET Core
A friendly beginner guide to building a GraphQL API in ASP.NET Core with Hot Chocolate. Learn queries, mutations, resolvers, and DataLoaders with simple examples.
The Best Way to Validate Objects in .NET (2024 Guide)
A friendly guide to validating objects in .NET: Data Annotations, FluentValidation, IValidatableObject, and the new built-in validation in .NET 10.
Top 15 Mistakes Developers Make When Creating Web APIs
A warm, beginner-friendly tour of the 15 most common Web API mistakes in ASP.NET Core, with simple fixes, diagrams, tables, and clear C# examples.