"QuestPDF is the first PDF library that feels like it was designed by someone who actually uses it."
When a manager approves a quotation, the system auto-generates a professional A4 PDF and streams it to the browser. QuestPDF's fluent C# API produces the entire document as a byte array — no COM interop, no Word Automation, no license files to manage.
Part of the Green Concrete ERP project. View full project page →
Why QuestPDF Over Alternatives
iTextSharp: AGPL license (commercial use requires a paid license). PDFSharp: no table support built-in. Syncfusion PDF: paid. Telerik Reporting: overkill for document generation. QuestPDF: MIT license since v2023.2, pure C#, built-in table/grid support, hot reload support in development. For open-source projects and training, QuestPDF wins.
Quotation PDF — Full Structure
public byte[] Generate(Quotation q)
{
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.DefaultTextStyle(s => s.FontSize(10).FontFamily("Arial"));
page.Header().Row(row =>
{
row.RelativeItem().Column(col =>
{
col.Item().Text("GREEN CONCRETE LTD.")
.FontSize(16).Bold().FontColor("#16a34a");
col.Item().Text("REQUEST FOR QUOTATION")
.FontSize(11).FontColor("#6b7280");
col.Item().Text("No: " + q.QuotationNumber);
});
});
page.Content().Column(col =>
{
col.Item().Table(table =>
{
table.ColumnsDefinition(cols =>
{
cols.RelativeColumn(3); // Material
cols.RelativeColumn(1); // Qty
cols.RelativeColumn(1); // Unit Price
cols.RelativeColumn(1); // Total
});
foreach (var row in q.Rows)
{
table.Cell().Text(row.Material);
table.Cell().Text(row.QuantityTon + " T");
table.Cell().Text("SAR " + row.UnitPrice.ToString("N2"));
table.Cell().Text("SAR " + row.Total.ToString("N2"));
}
});
col.Item().AlignRight().Text("Total: SAR " + q.TotalAmount.ToString("N2"))
.FontSize(12).Bold();
if (q.Status == QuotationStatus.Approved)
col.Item().Text("APPROVED by " + q.ApprovedBy + " on "
+ q.ApprovedAt.ToString("dd MMM yyyy")).FontColor("#16a34a");
});
});
}).GeneratePdf();
}Streaming to Browser + Email
// WebApi controller — stream PDF to browser
[HttpGet("quotations/{id}/pdf")]
[Authorize(Roles = "Manager,Admin")]
public async Task<IActionResult> GetPdf(Guid id)
{
var pdfBytes = await _pdfService.GenerateAsync(id);
return File(pdfBytes, "application/pdf",
"GCL-RFQ-" + id.ToString("N")[..8].ToUpper() + ".pdf");
}
// MailKit — email on approval
await _emailService.SendWithAttachmentAsync(
to: quotation.ClientEmail,
subject: "Quotation Approved — " + quotation.QuotationNumber,
pdfBytes: pdfBytes,
fileName: "GCL-RFQ.pdf");🎯 Quick Check
Q1: Why does the PDF generation service return byte[] instead of a file path?
Show Answer
byte[] is the most flexible format — you can stream it to a browser (File() result), save it to Azure Blob Storage, attach it to an email, or cache it in Redis. If the service returned a file path, it would require disk access and create cleanup concerns. byte[] is stateless and infrastructure-agnostic, matching the principle that the Infrastructure layer handles storage decisions.
Q2: What does GeneratePdf() return in QuestPDF?
Show Answer
A byte[] containing the PDF file. The document is generated entirely in memory — no temp files, no disk I/O. This makes it safe for serverless and containerised deployments where the filesystem may be read-only.
Q3: When should the PDF be generated — on approval or on demand?
Show Answer
Both strategies are valid. Generate on approval: the PDF is created once, stored in blob storage, fast to retrieve. Generate on demand: always reflects current data if quotation details change. For this ERP, we generate on approval (immutable document — reflects the approved state). Storing in blob and serving from blob is more scalable than regenerating on every download.
Key Takeaways
- QuestPDF is MIT-licensed — no COM interop, no license files, pure C# fluent API
- GeneratePdf() returns byte[] — stream to browser, save to blob, or email — all from Infrastructure
- Auto-triggered on manager approval via the ApproveQuotationHandler calling the PDF service
- Table layout with ColumnsDefinition gives pixel-accurate column widths on A4
- Approval stamp is added only when Status == Approved — same service generates draft previews too