🛠 Series: ERP RFQ ModulePart 7 of 8

"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