Skip to main content
SEMastery
ASP.NETbeginner

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.

11 min readUpdated February 9, 2026

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 big picture: data goes in, a PDF comes out

The plan has three small steps:

  1. Describe the order (who bought what).
  2. Ask QuestPDF to draw that order onto a page.
  3. 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.

ToolHow you describe the pageGood for beginners?Cost for small projects
QuestPDFPlain C# codeYes, very friendlyFree (Community MIT)
IronPDFTurn HTML into PDFMediumPaid, free trial
SyncfusionHTML or low-level APIMediumFree for tiny firms
iText 7Low-level building blocksHarderAGPL 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

Need a PDF
Have HTML already?
Use HTML-to-PDF
Use QuestPDF

Steps

1

Need a PDF

You want a fixed document

2

Have HTML already?

A web page you like

3

Use HTML-to-PDF

IronPDF or Syncfusion

4

Use QuestPDF

Build it in C# code

Most beginners should start with QuestPDF

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.

How the data classes fit together

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

Header
Content
Footer

Steps

1

Header

Shop name and date

2

Content

Table of items

3

Footer

Page number

Header, content, and footer

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.

What happens when the browser asks for the PDF

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.

MistakeWhat you seeHow to fix it
Forgot the license lineApp throws on first PDFSet License = LicenseType.Community in Program.cs
Wrong content typeBrowser shows raw textReturn "application/pdf"
Huge image as logoPDF is slow and largeResize the image first
Blocking the thread for big filesApp feels frozenStream the PDF or run it as a background job
Money shown as 10 not 10.00Looks oddFormat 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

Is the PDF small?
Generate inline
Stream or background

Steps

1

Is the PDF small?

One invoice

2

Generate inline

Return bytes now

3

Stream or background

Big or many files

Pick the path that fits the size

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; // F

This 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

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 return File(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 %PDF to catch empty or broken files early.

Related Posts