Today, I want to show you how to build a production-ready invoice builder application.
We will build an app that helps clients create, manage, and download invoices from business data.
It turns orders and customer records into branded documents with accurate taxes, totals, and payment terms, and provides users with clear actions, such as previews and PDF downloads.
In production, it must be reliable and consistent, and produce PDFs that are easy to store and easy to read on any device.
Invoice Builder requirements
Business requirements:
- Create Invoice Customers and Senders
- Create Invoices: Invoice & Date details, existing Customer and Sender data, line items
- Generate and download Invoice PDF files
PDF requirements:
- Consistent layout at any length: a single item or hundreds of rows should paginate cleanly.
- Accurate rendering and stable formatting on any device.
- High-quality rendering with crisp text, logos, and tables suitable for printing or archiving.
- PDF/UA compliance for long-term storage and audit needs.
- Digital signatures to prove integrity and origin, with an optional visible signature appearance.
- Fully managed, cross-platform runtime with fast generation and small operational overhead.
UI Features:
-
Home Screen:
- Tabs: Invoices, Customers, Senders
-
Invoices:
- List paged invoices in a table view
- Create new invoices
- Table actions: View, Edit, Delete (with confirmation), Download PDF
-
Customers:
- List paged customers in a table view
- Create new customers
- Table actions: View, Edit, Delete (with confirmation)
-
Senders:
- List paged senders in a table view
- Create new senders
- Table actions: View, Edit, Delete (with confirmation)
Implementation Tech Stack:
Backend:
- ASP .NET Core 10, EF Core, IronPDF for generating PDFs
Frontend:
- React, TypeScript, TailwindCSS
Architecture:
- Modular Monolith
- Vertical Slice Architecture + Clean Architecture
Database:
- PostgreSQL
Deployment:
- Docker Compose
Let's dive into the implementation details.
Invoice Builder UI Overview
To better understand how the Invoice Builder app works, let's take a look at the UI screens.
Here is the Home Screen with Invoices, Customers, and Senders tabs:
The Invoices tab shows a list of invoices with actions such as View, Edit, Delete, and Download PDF.
To create a new invoice, we need to create a Customer and a Sender first.
Now we can create an invoice. We need to fill the following fields:
- Invoice Details (Number, Dates, Tax Rate, Notes, etc.)
- Sender Info
- Customer Info
- Line Items
We need to select a Customer and Sender from the dropdowns.
After we click "Create Invoice", the invoice is created and stored in the database.
You can download the PDF file by clicking the "Download PDF" button on the invoice details page or the Invoices list page.
Now, let's explore the options we have for generating the PDF file.
Selecting C# PDF Library for Invoice Generation
There are multiple options for generating PDFs in C# and ASP.NET Core:
- IronPDF
- QuestPDF
- Aspose.PDF
- PuppeteerSharp
Let's briefly explore each of them and compare their pros and cons.
IronPDF
IronPDF is a powerful .NET library for creating, editing, signing, and rendering PDFs.
It is designed to be developer-friendly: many complex tasks in raw PDF APIs or lower-level tools are easier with IronPDF via a more fluent, higher-level API.
IronPDF pros:
- HTML, DOCX, RTF, XML, MD, Image to PDF Conversion
- PDF FROM URL, PDF to HTML
- Compliant to PDF/A, PDF/UA, and PDF/X standards
- Fully managed and cross-platform
- High-performance rendering
- Digital Signatures & Security
- Rich PDF Manipulation (edit, merge, split PDFs)
- Good documentation
IronPDF cons:
- Requires a paid license for commercial use.
QuestPDF
QuestPDF is a popular open-source PDF generation library for .NET developers.
Its primary goal is to simplify PDF creation through a C# fluent API.
QuestPDF pros:
- Fluent C# API — build documents directly in code using a layout-based approach.
- Consistent layouts — predictable, repeatable output for structured documents.
- Good documentation with many examples and patterns for reports.
- Free for commercial use for small companies.
QuestPDF cons:
- Missing HTML-to-PDF conversion — unable to convert HTML content directly into PDFs, limiting versatility in web applications.
- Missing Compliance standards — no built-in support for PDF/A (archival) or PDF/UA (accessibility).
- Missing Digital signatures or security — no native features for signing, password protection, or permissions.
- Missing Interactive forms or JavaScript rendering — limited scope to static layouts only.
- Requires a paid license for commercial use for larger companies.
Aspose.PDF
Aspose.PDF provides advanced capabilities for programmatic PDF creation.
It gives developers extensive control over document elements, including paragraphs, tables, images, forms, and annotations.
Aspose.PDF pros:
- Extensive programmatic control over PDF elements (paragraphs, tables, images, forms, annotations).
- Advanced PDF manipulation features for complex document workflows.
- Support for PDF/A standards and digital signatures.
- Mature library with a long track record in enterprise environments.
Aspose.PDF cons:
- Verbose API requiring detailed coding even for simple tasks, leading to a steeper learning curve.
- Limited HTML-to-PDF conversion — lacks native Chromium engine, resulting in rendering limitations.
- No JavaScript execution support for dynamic content rendering.
- Slower performance and higher memory usage compared to Chromium-based alternatives (like IronPDF).
- Expensive licensing with higher pricing tiers compared to competitors.
PuppeteerSharp
Puppeteer Sharp is a .NET port of the official Node.js Puppeteer API.
It lets you drive Chromium/Chrome from C# over the DevTools Protocol:
open pages, run JavaScript, wait for selectors, take screenshots, and print pages to PDF.
It runs headless by default and can also connect to a remote browser.
PuppeteerSharp pros:
- Easy API to create PDFs using Chrome Engine
- Open-source and free for commercial use
PuppeteerSharp cons:
- Puppeteer-Sharp is a browser automation library, and not a .NET-native PDF SDK. While it excels at rendering HTML into PDFs, it is not a full-featured .NET PDF SDK.
- Missing Compliance standards — no built-in support for PDF/A (archival) or PDF/UA (accessibility).
- Missing Digital signatures or security — no native features for signing, password protection, or permissions.
- Missing PDF editing — no tools for merging, splitting, or modifying existing PDFs.
Why IronPDF is the Best Choice for Invoice Builder Application
I have personally tested all the libraries, and my preferred choice for commercial projects is IronPDF.
For an invoice builder application, it's a perfect choice because it meets all our requirements:
- Consistent layout at any length: a single item or hundreds of rows should paginate cleanly.
- Accurate rendering and stable formatting on any device.
- High-quality rendering with crisp text, logos, and tables suitable for printing or archiving.
- PDF/A-3b compliance for long-term storage and audit needs.
- Digital signatures to prove integrity and origin, with an optional visible signature appearance.
- Fully managed, cross-platform runtime with fast generation and small operational overhead.
QuestPDF and PuppeteerSharp don't meet these requirements.
Aspose can be an alternative, but IronPDF is better in HTML-to-PDF conversion, is faster and uses less memory.
Also, IronPDF offers a license at a much more affordable price.
Now let's explore how we can generate the invoice PDF file on the backend, using IronPDF.
Generating Invoice PDFs with IronPDF
IronPDF has a quick learning curve, just 5 minutes from getting started to generating the first PDF.
First, you need to install the following NuGet package:
dotnet add package IronPdf
With IronPDF, you can create a PDF document from HTML content in just three lines of code:
using IronPdf;
var htmlContent = @"
<html>
<head>
<style>
h1 { color: blue; }
p { font-size: 16px; }
</style>
</head>
<body>
<h1>Invoice 1000471</h1>
<p>Test invoice</p>
</body>
</html>";
// Create a PDF from HTML content
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf(htmlContent);
pdf.SaveAs("Invoice.pdf");
Creating PDF documents from HTML content is pretty easy.
But how can we quickly generate a rich, professional-looking HTML invoice document in ASP.NET Core?
For this purpose, we can use Razor views.
They are used in ASP.NET Core MVC, Razor Pages and Blazor applications.
Razor views have a .cshtml extension and allow you to use C# inside the HTML markup.
Let's build the Invoice PDF document using Razor views.
You need to install the additional NuGet package:
dotnet add package Razor.Templating.Core
Razor.Templating.Core package allows you to use Razor views in any assembly, even in a class library, so you don't need to create an ASP.NET Core MVC application.
Here is our set of Invoice models:
public sealed record InvoiceResponse(
Guid Id,
string InvoiceNumber,
DateTime InvoiceDate,
DateTime DueDate,
string Currency,
string Notes,
CustomerResponse Customer,
SenderResponse Sender,
List<InvoiceLineItemResponse> LineItems,
decimal Subtotal,
decimal TaxRate,
decimal TotalAmount
);
public sealed record CustomerResponse(
Guid Id,
string CompanyName,
string CustomerName,
string CustomerAddress,
string PostalCode,
string CustomerEmail,
string CustomerTaxVatId
);
public sealed record SenderResponse(
Guid Id,
string SenderCompanyName,
string SenderFullName,
string SenderAddress,
string SenderTaxVatId,
string BankDetails
);
public sealed record InvoiceLineItemResponse(
Guid Id,
string ItemName,
decimal Quantity,
decimal UnitPrice,
decimal Total
);
For styling, we can use Tailwind CSS, which simplifies CSS and makes it easier to create responsive layouts.
Let's create an InvoiceTemplate.cshtml file in the Views folder:
@model Modules.Invoices.Features.Features.Shared.Responses.InvoiceResponse
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice @Model.InvoiceNumber</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-white">
<div class="max-w-4xl mx-auto p-8">
<!-- Header -->
<div class="flex justify-between items-start mb-8">
<div>
<h1 class="text-4xl font-bold text-gray-900 mb-2">INVOICE</h1>
<p class="text-lg text-gray-600">@Model.InvoiceNumber</p>
</div>
<div class="text-right">
<p class="text-sm text-gray-600">Invoice Date</p>
<p class="text-lg font-semibold text-gray-900">@Model.InvoiceDate.ToString("MMM dd, yyyy", System.Globalization.CultureInfo.InvariantCulture)</p>
<p class="text-sm text-gray-600 mt-2">Due Date</p>
<p class="text-lg font-semibold text-red-600">@Model.DueDate.ToString("MMM dd, yyyy", System.Globalization.CultureInfo.InvariantCulture)</p>
</div>
</div>
<!-- Sender and Customer Information -->
<div class="grid grid-cols-2 gap-8 mb-8">
<!-- From (Sender) -->
<div class="bg-gray-50 p-6 rounded-lg">
<h2 class="text-sm font-semibold text-gray-500 uppercase mb-3">From</h2>
<div class="space-y-1">
<p class="text-lg font-bold text-gray-900">@Model.Sender.SenderCompanyName</p>
<p class="text-gray-700">@Model.Sender.SenderFullName</p>
<p class="text-gray-600 text-sm">@Model.Sender.SenderAddress</p>
<p class="text-gray-600 text-sm mt-2">
<span class="font-semibold">VAT/Tax ID:</span> @Model.Sender.SenderTaxVatId
</p>
<div class="mt-3 pt-3 border-t border-gray-300">
<p class="text-xs text-gray-500 uppercase font-semibold mb-1">Bank Details</p>
<p class="text-sm text-gray-700">@Model.Sender.BankDetails</p>
</div>
</div>
</div>
<!-- To (Customer) -->
<div class="bg-blue-50 p-6 rounded-lg">
<h2 class="text-sm font-semibold text-blue-700 uppercase mb-3">Bill To</h2>
<div class="space-y-1">
<p class="text-lg font-bold text-gray-900">@Model.Customer.CompanyName</p>
<p class="text-gray-700">@Model.Customer.CustomerName</p>
<p class="text-gray-600 text-sm">@Model.Customer.CustomerAddress</p>
<p class="text-gray-600 text-sm">@Model.Customer.PostalCode</p>
<p class="text-gray-600 text-sm mt-2">
<span class="font-semibold">Email:</span> @Model.Customer.CustomerEmail
</p>
<p class="text-gray-600 text-sm">
<span class="font-semibold">VAT/Tax ID:</span> @Model.Customer.CustomerTaxVatId
</p>
</div>
</div>
</div>
<!-- Line Items Table -->
<div class="mb-8">
<table class="w-full border-collapse">
<thead>
<tr class="bg-gray-800 text-white">
<th class="text-left py-3 px-4 font-semibold text-sm">Item Description</th>
<th class="text-right py-3 px-4 font-semibold text-sm">Quantity</th>
<th class="text-right py-3 px-4 font-semibold text-sm">Unit Price</th>
<th class="text-right py-3 px-4 font-semibold text-sm">Total</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.LineItems)
{
<tr class="border-b border-gray-200 hover:bg-gray-50">
<td class="py-3 px-4 text-gray-900">@item.ItemName</td>
<td class="text-right py-3 px-4 text-gray-700">@item.Quantity.ToString("N2")</td>
<td class="text-right py-3 px-4 text-gray-700">@item.UnitPrice.ToString("N2") @Model.Currency</td>
<td class="text-right py-3 px-4 font-semibold text-gray-900">@item.Total.ToString("N2") @Model.Currency</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Totals Section -->
<div class="flex justify-end mb-8">
<div class="w-80">
<div class="bg-gray-50 p-6 rounded-lg space-y-3">
<div class="flex justify-between items-center">
<span class="text-gray-700">Subtotal</span>
<span class="text-lg font-semibold text-gray-900">@Model.Subtotal.ToString("N2") @Model.Currency</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-700">Tax (@(Model.TaxRate.ToString("N2"))%)</span>
<span class="text-lg font-semibold text-gray-900">@((Model.TotalAmount - Model.Subtotal).ToString("N2")) @Model.Currency</span>
</div>
<div class="border-t-2 border-gray-300 pt-3">
<div class="flex justify-between items-center">
<span class="text-xl font-bold text-gray-900">Total Amount</span>
<span class="text-2xl font-bold text-blue-600">@Model.TotalAmount.ToString("N2") @Model.Currency</span>
</div>
</div>
</div>
</div>
</div>
<!-- Notes Section -->
@if (!string.IsNullOrWhiteSpace(Model.Notes))
{
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-8">
<h3 class="text-sm font-semibold text-gray-700 uppercase mb-2">Notes</h3>
<p class="text-gray-700 text-sm">@Model.Notes</p>
</div>
}
<!-- Footer -->
<div class="border-t-2 border-gray-300 pt-6 text-center">
<p class="text-sm text-gray-600">Thank you for your business!</p>
<p class="text-xs text-gray-500 mt-2">This is a computer-generated invoice and does not require a signature.</p>
</div>
</div>
</body>
</html>
Then we call RazorTemplateEngine.RenderAsync to dynamically render the HTML content from a Razor view:
public sealed class InvoicePdfGenerator : IInvoicePdfGenerator
{
private const string InvoiceTemplatePath = "~/Views/Invoice/InvoiceTemplate.cshtml";
private const int DefaultMargin = 10;
public async Task<byte[]> GeneratePdfAsync(InvoiceResponse invoice)
{
var html = await RazorTemplateEngine.RenderAsync(InvoiceTemplatePath, invoice);
var renderer = CreatePdfRenderer();
var pdf = await renderer.RenderHtmlAsPdfAsync(html);
return pdf.BinaryData;
}
}
Here we use the ChromePdfRenderer class from IronPDF to generate a PDF document:
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf(html);
The InvoicePdfGenerator is used in the DownloadInvoiceHandler, which reads the invoice from the database and generates the PDF document:
internal sealed class DownloadInvoiceHandler(
InvoicesDbContext context,
IInvoicePdfGenerator pdfGenerator)
: IDownloadInvoiceHandler
{
public async Task<Result<InvoicePdfResponse>> HandleAsync(
Guid id,
CancellationToken cancellationToken)
{
var invoice = await context.Invoices
.Include(x => x.Customer)
.Include(x => x.Sender)
.Include(x => x.Items)
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (invoice is null)
{
return InvoiceErrors.NotFound(id);
}
var invoiceResponse = invoice.MapToResponse();
var pdfBytes = await pdfGenerator.GeneratePdfAsync(invoiceResponse);
var fileName = $"Invoice_{invoice.InvoiceNumber}_{DateTime.UtcNow:yyyyMMdd}.pdf";
return new InvoicePdfResponse(pdfBytes, fileName);
}
}
We can use the Results.File with contentType: "application/pdf" to download the file from the ASP .NET Core Minimal API endpoint:
public class DownloadInvoiceApiEndpoint : IApiEndpoint
{
public void MapEndpoint(WebApplication app)
{
app.MapGet(RouteConsts.Download, Handle);
}
private static async Task<IResult> Handle(
Guid id,
IDownloadInvoiceHandler handler,
CancellationToken cancellationToken)
{
var response = await handler.HandleAsync(id, cancellationToken);
if (response.IsError)
{
return response.Errors.ToProblem();
}
var pdfData = response.Value!;
return Results.File(
fileContents: pdfData.FileBytes,
contentType: "application/pdf",
fileDownloadName: pdfData.FileName);
}
}
Here is what the downloaded report looks like:
This invoice looks good and professional.
Now let's explore how we can add PDF/UA Compliance to the generated PDF.
Generating Compliant PDFs with IronPDF
For invoices, regulations and accessibility standards often require that documents follow strict formats.
Two of the most important standards are PDF/A and PDF/UA.
PDF/A (PDF for Archiving) ensures that a document can be stored long-term without losing fonts, colors, or layout.
It is often required for contracts, invoices, and government filings.
PDF/UA (Universal Accessibility) ensures that a document is accessible to everyone, including individuals who rely on screen readers or other assistive technologies.
This is important for accessibility compliance.
With IronPDF, you can create both PDF/A and PDF/UA documents in just a few lines of code.
It uses the Chromium engine to render HTML and CSS, then exports the result into the required compliant format.
You can convert the rendered HTML into PDF/A or PDF/UA using the ConvertToPdfA or ConvertToPdfUA methods:
using IronPdf;
var renderer = CreatePdfRenderer();
var pdf = await renderer.RenderHtmlAsPdfAsync(html);
pdf = pdf.ConvertToPdfUA();
And let the user download the compliant PDF file.
Now let's explore the final piece of the puzzle: adding a digital signature to the generated PDF.
Adding a Digital Signature to PDF with IronPDF
Invoices often need a signature to prove integrity and origin.
There are two parts you can combine:
- A visual mark inside the PDF (a stamp or signature block) so the human reader sees a signed area.
- A cryptographic digital signature that a PDF reader can verify.
Let's explore how we can add a visual signature with a sender name to the generated PDF:
using IronPdf;
var renderer = CreatePdfRenderer();
var pdf = await renderer.RenderHtmlAsPdfAsync(html);
AddSignatureToPdf(pdf, invoice.Sender.SenderFullName);
private void AddSignatureToPdf(PdfDocument pdf, string senderFullName)
{
var signatureText = $"Digitally signed by: {senderFullName}";
var stamp = new IronPdf.Editing.TextStamper
{
Text = signatureText,
FontFamily = "Arial",
FontSize = 20,
IsBold = false,
IsItalic = true,
VerticalAlignment = IronPdf.Editing.VerticalAlignment.Bottom,
HorizontalAlignment = IronPdf.Editing.HorizontalAlignment.Right,
Opacity = 100,
Rotation = 0
};
pdf.ApplyStamp(stamp);
logger.LogDebug("Added signature to PDF: {SignatureText}", signatureText);
}
This code adds a stamp with the sender's name to the bottom right corner of the PDF:
To add a real cryptographic signature, sign the document with a certificate (PFX).
This feature ensures your PDFs are secure for business, legal, and compliance purposes.
Digital signatures prove the authenticity of a document.
With IronPDF, you can sign using a certificate (.pfx or .p12) and add details like the signer's name, location, or even an image:
var renderer = CreatePdfRenderer();
var pdf = await renderer.RenderHtmlAsPdfAsync(html);
pdf = pdf.ConvertToPdfUA();
var signature = new PdfSignature("IronSoftware.pfx", "123456")
{
SignatureDate = DateTime.Now,
SigningContact = "cert@company.com",
SigningLocation = "Chicago, USA",
SigningReason = "Contractual Agreement",
TimeStampUrl = "[http://timestamp.digicert.com](http://timestamp.digicert.com)",
TimestampHashAlgorithm = TimestampHashAlgorithms.SHA256,
SignatureImage = new PdfSignatureImage("assets/visual-signature.png", 0,
new Rectangle(350, 750, 200, 100))
};
pdf.Sign(signature);
This ensures that the PDF cannot be changed without invalidating the signature.
For more information, refer to the official documentation.
Licensing
IronPDF follows a commercial licensing model.
- Free trial available: You can start with a trial license to explore features.
- Commercial licenses required for production: Businesses and organizations must obtain a license before deploying IronPDF in production.
- Support and updates included: A license comes with access to updates, bug fixes, and professional technical support.
IronPDF provides enterprise-grade features, including PDF/A and PDF/UA compliance, digital signatures, and document security, which are often required in professional and regulated environments.
IronPDF offers a license at a much more affordable price and offers more benefits than Aspose, which is honestly very overpriced.
IronPDF offers the best support, helping developers quickly resolve issues — a feature that other PDF libraries often lack.







Top comments (0)