Skip to main content
SEMastery

How to Build a Production-Ready Invoice Builder in .NET Using IronPDF

A simple, beginner-friendly guide to building a real invoice PDF generator in .NET with IronPDF, from HTML template to a clean download in your API.

12 min readUpdated January 25, 2026

Think about the bill you get at a sweet shop during Diwali. The shopkeeper does not draw a fresh bill format every time. There is one printed pad with boxes for the item names, the quantity, the rate, and the total. He just fills in the blanks for your order and tears off the page. Same shape, different numbers.

A invoice builder in .NET works the same way. You design the invoice once, as a template. Then for each customer you pour in fresh data, the items they bought, the prices, the GST, the total, and print a clean PDF. In this guide we will build exactly that using a library called IronPDF.

We will keep the words plain and the steps small. By the end you will have a real, working invoice generator that you could put into an ASP.NET Core API and ship.

Why a PDF, and why IronPDF?

A PDF looks the same everywhere. On your phone, on the customer's laptop, on the accountant's printer. An invoice has to look exactly right and never shift around, because money and tax are involved. That is the job a PDF does very well.

Now, the hard part is turning your data into a nice-looking PDF. You could draw every line and box by hand in code, but that is slow and painful. A much friendlier way is to write the invoice as an HTML page with CSS, the same way you would build a small web page, and then let a tool print that page to a PDF.

That is what IronPDF does. It carries its own copy of the Chrome rendering engine inside it. You hand it some HTML, and it prints a pixel-perfect PDF, the same way Chrome's "Print to PDF" works, but fully controlled from your C# code.

The big idea: your data becomes HTML, then IronPDF prints it to a PDF.

One honest note before we start. IronPDF is a commercial, paid library. It is not free for production. You get a 30-day free trial and a free development key to test everything, but for a real deployed app you need a license. We will talk about this clearly near the end so there are no surprises.

What we are building

We will build a small invoice service with three clear jobs:

  1. Hold the invoice data in plain C# objects.
  2. Turn that data into an HTML page that looks like a proper invoice.
  3. Use IronPDF to print that HTML into a PDF and return it.

Here is the shape of the whole thing.

Invoice builder pipeline

Data
Template
Render
Deliver

Steps

1

Data

Build Invoice object

2

Template

Fill HTML with values

3

Render

IronPDF makes PDF

4

Deliver

Save or download

Each stage has one job, so the code stays easy to read and test.

Step 1: Install IronPDF

Add the NuGet package to your project. Use the .NET CLI:

dotnet add package IronPdf

IronPDF works on .NET 10, 9, 8, and older versions too, on Windows, Linux, and macOS. For this guide we assume a normal ASP.NET Core Web API on .NET 10, which is the current LTS release.

Next, set your license key once when the app starts. During development you can use a trial key. In Program.cs:

using IronPdf;
 
// Set this once, at startup, before you render anything.
// Keep the real key in configuration or a secret, not in code.
License.LicenseKey = builder.Configuration["IronPdf:LicenseKey"];

Notice we read the key from configuration. Never paste a real license key straight into your source code. Put it in appsettings.json, environment variables, or a secret store, so it does not leak into your Git history.

Step 2: Model the invoice data

Before any HTML, we need clean data. Let us describe an invoice with small C# records. Records are great here because an invoice, once made, should not change.

public record InvoiceLine(string Description, int Quantity, decimal UnitPrice)
{
    public decimal LineTotal => Quantity * UnitPrice;
}
 
public record Invoice(
    string Number,
    string CustomerName,
    DateOnly IssuedOn,
    IReadOnlyList<InvoiceLine> Lines)
{
    public decimal SubTotal => Lines.Sum(l => l.LineTotal);
    public decimal Tax => Math.Round(SubTotal * 0.18m, 2); // 18% GST example
    public decimal GrandTotal => SubTotal + Tax;
}

A few things to notice. The LineTotal, SubTotal, Tax, and GrandTotal are computed properties. We never store the totals by hand. They are always worked out from the parts. This avoids a classic bug where the stored total and the line items disagree.

The 18% here is just an example GST rate. In a real app you would pass the rate in, because different items have different tax slabs.

Step 3: Turn data into HTML

Now we build the HTML. Think of it as filling the blanks on that sweet-shop bill pad. We will keep the layout in simple CSS so it prints cleanly.

public static class InvoiceHtmlBuilder
{
    public static string Build(Invoice invoice)
    {
        var rows = string.Join("", invoice.Lines.Select(line => $"""
            <tr>
              <td>{line.Description}</td>
              <td class="num">{line.Quantity}</td>
              <td class="num">{line.UnitPrice:C}</td>
              <td class="num">{line.LineTotal:C}</td>
            </tr>
            """));
 
        return $$"""
            <html>
            <head>
              <style>
                body { font-family: Arial, sans-serif; color: #222; }
                h1 { color: #1a4f8b; }
                table { width: 100%; border-collapse: collapse; margin-top: 16px; }
                th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
                .num { text-align: right; }
                .total { font-weight: bold; font-size: 1.1em; }
              </style>
            </head>
            <body>
              <h1>Invoice {{invoice.Number}}</h1>
              <p>Bill to: <strong>{{invoice.CustomerName}}</strong></p>
              <p>Date: {{invoice.IssuedOn:dd MMM yyyy}}</p>
              <table>
                <tr><th>Item</th><th>Qty</th><th>Rate</th><th>Amount</th></tr>
                {{rows}}
                <tr class="total"><td colspan="3" class="num">Subtotal</td>
                  <td class="num">{{invoice.SubTotal:C}}</td></tr>
                <tr><td colspan="3" class="num">GST (18%)</td>
                  <td class="num">{{invoice.Tax:C}}</td></tr>
                <tr class="total"><td colspan="3" class="num">Grand Total</td>
                  <td class="num">{{invoice.GrandTotal:C}}</td></tr>
              </table>
            </body>
            </html>
            """;
    }
}

This is plain HTML and CSS. If you have ever made a small web page, this will feel familiar. The format shows the number as currency. The table grows or shrinks based on how many lines the invoice has.

For very large or fancy templates, many teams use Razor views instead of building strings by hand. That is a fine choice too. The idea is the same: data in, HTML out.

Step 4: Render the PDF with IronPDF

Here is the heart of the whole thing. We take the HTML and ask IronPDF to print it.

using IronPdf;
 
public class InvoicePdfService
{
    private readonly ChromePdfRenderer _renderer;
 
    public InvoicePdfService()
    {
        _renderer = new ChromePdfRenderer();
        // Give the page a little breathing room around the edges.
        _renderer.RenderingOptions.MarginTop = 20;
        _renderer.RenderingOptions.MarginBottom = 20;
        // Add a simple page number footer.
        _renderer.RenderingOptions.TextFooter = new TextHeaderFooter
        {
            CenterText = "Page {page} of {total-pages}"
        };
    }
 
    public byte[] Generate(Invoice invoice)
    {
        string html = InvoiceHtmlBuilder.Build(invoice);
        PdfDocument pdf = _renderer.RenderHtmlAsPdf(html);
        return pdf.BinaryData; // ready to save, stream, or download
    }
}

The key class is ChromePdfRenderer. The key method is RenderHtmlAsPdf. You pass HTML, you get back a PdfDocument. From there you can save it, stream it, merge it, or grab the raw bytes with BinaryData.

Notice we created the renderer once in the constructor and reused it. Starting up the Chrome engine takes a moment, so you do not want to do it on every single call. Build it once, use it many times.

What happens inside the service when you call Generate.

Step 5: Wire it into an API endpoint

Now let us expose this so a browser or app can ask for an invoice and download it. Using a minimal API endpoint:

app.MapGet("/invoices/{number}/pdf", (string number, InvoicePdfService pdfService) =>
{
    // In a real app you would load this from your database.
    var invoice = new Invoice(
        Number: number,
        CustomerName: "Asha Traders",
        IssuedOn: DateOnly.FromDateTime(DateTime.Today),
        Lines: new[]
        {
            new InvoiceLine("Cotton T-Shirt", 3, 499m),
            new InvoiceLine("Denim Jeans", 2, 1299m)
        });
 
    byte[] pdf = pdfService.Generate(invoice);
    return Results.File(pdf, "application/pdf", $"invoice-{number}.pdf");
});

Do not forget to register the service so it can be injected:

builder.Services.AddSingleton<InvoicePdfService>();

We register it as a singleton because it holds a reusable renderer. One instance for the whole app is exactly what we want.

When someone visits /invoices/INV-1001/pdf, the API builds the data, renders the HTML, prints the PDF, and sends it back as a download. That is the full loop.

A picture of the request

Here is how a single request flows through the system, from click to download.

One PDF request

Browser
Endpoint
Service
IronPDF
Download

Steps

1

Browser

GET /pdf

2

Endpoint

Load invoice

3

Service

Build HTML

4

IronPDF

Render PDF

5

Download

Return file

The browser asks, the API builds, IronPDF prints, the file comes back.

Making it production-ready

A demo and a real service are not the same thing. Here are the things that matter once real customers are involved.

Reuse the renderer

We said this already, but it is worth repeating because it is the single biggest performance win. Do not create a new ChromePdfRenderer per request. Create it once and share it. Starting Chrome over and over will make your server slow and hungry for memory.

Handle bulk runs as background jobs

Generating one invoice inside a web request is fine. Generating 5000 invoices at the end of the month inside a single request is not. The request will time out and the server will struggle.

For bulk work, push each invoice job onto a queue and let a background worker chew through them. The web request returns instantly, and the heavy work happens quietly in the background.

Single invoice goes inline, bulk invoices go through a queue.

Watch your memory

Each PDF render uses memory, and the Chrome engine is not tiny. If you generate a flood of PDFs at once, you can run out of memory. Limit how many run at the same time, for example with a SemaphoreSlim, so you never have a hundred renders fighting for RAM at the same moment.

Run it in containers carefully

On Linux and in Docker, the Chrome engine needs a few system libraries to be present. IronPDF can download what it needs, but the official Docker and Linux guides list the exact packages. Test your container early, not on launch day.

Common methods at a glance

IronPDF gives you a few rendering entry points. Pick the one that fits your source.

MethodWhat you give itBest for
RenderHtmlAsPdfAn HTML stringTemplates built in code, like our invoice
RenderHtmlFileAsPdfA path to an HTML fileDesigns saved as static files
RenderUrlAsPdfA web URLPrinting an existing live page

And here are the handful of options you will reach for most often.

OptionWhat it controls
MarginTop / MarginBottomWhite space around the page
TextFooter / TextHeaderPage numbers, dates, company name
PaperSizeA4, Letter, and other sizes
PrintHtmlBackgroundsWhether CSS colors and images print

Free trial and licensing, told honestly

IronPDF is a commercial product. Here is the plain truth so you can plan.

  • You get a 30-day free trial and a free development key. You can test the full library, no features locked.
  • For production you need a paid license.
  • A perpetual license starts at around 749 US dollars at the low end, with higher tiers for more developers and projects.
  • There is also a monthly subscription option if you prefer to pay over time.

Prices change, so always confirm on the official licensing page before you buy. If your project cannot pay for a library, look at free options like PuppeteerSharp or Playwright, which also print HTML to PDF using a headless browser. IronPDF's selling point is convenience, support, and a smooth single-package experience.

Quick way to decide if IronPDF fits your project.

A common mistake to avoid

A very common slip is mixing up where the totals come from. Some developers store GrandTotal as a saved field and also store the line items. Then someone edits a line item, the total does not update, and the printed invoice shows numbers that do not add up. The customer notices, and it looks unprofessional.

The fix is the one we used above. Compute totals from the line items every time. Never store a number you can calculate. The invoice always tells the truth because the math happens at the last moment.

Quick recap

  • An invoice is like a fill-in-the-blanks bill. Design once, pour in data, print.
  • IronPDF prints HTML to PDF using a built-in Chrome engine, so you design with familiar HTML and CSS.
  • Model your data with records and compute the totals, never store them by hand.
  • Build the HTML, then call ChromePdfRenderer.RenderHtmlAsPdf to get a PdfDocument.
  • Create the renderer once and reuse it. This is the biggest speed win.
  • For bulk invoices, move the work to a background job so the web request stays fast.
  • Watch memory, limit how many run at once, and test your Docker setup early.
  • IronPDF is paid, with a 30-day trial. Confirm pricing on the official site before you ship.

References and further reading

Related Posts