Skip to content

Dynamic & data-driven content

Most documents that matter are generated from data: an invoice from line items, a report from a database query, a catalog from a product list. Because the Rivoli PDF document model is plain C# objects, you build it with ordinary loops, conditionals, and LINQ: there’s no template language to learn.

The core move: iterate your data and Add a block (or row, or item) per element.

using Rivoli.Pdf;
using Rivoli.Pdf.Model;
public Document BuildCatalog(IEnumerable<Product> products)
{
var doc = Document.Create("Product Catalog");
var section = doc.AddSection();
foreach (var product in products)
{
var para = new Paragraph();
para.Inlines.Add(new Run(product.Name) { Style = new Style().Bold() });
para.Inlines.Add(new Run($", ${product.Price:0.00}"));
section.Add(para);
}
return doc;
}

Tables are the most common data-driven shape. Build the header once, then loop the rows:

public Block BuildLineItems(IReadOnlyList<LineItem> items)
{
var table = new Table().Header("Item", "Qty", "Unit price", "Total");
foreach (var item in items)
{
table.AddDataRow(
item.Description,
item.Quantity.ToString(),
item.UnitPrice.ToString("C"),
(item.Quantity * item.UnitPrice).ToString("C"));
}
table.AddDataRow("", "", "Total", items.Sum(i => i.Quantity * i.UnitPrice).ToString("C"));
return table;
}

Header(...), AddDataRow(...), and Sum(...) are ordinary calls: the table is just a C# object you populate.

var list = List.Unordered();
foreach (var finding in report.Findings)
list.Add(finding.Summary);
section.Add(list);
// Or, more fluently, with the Items extension:
section.Add(List.Ordered().Items(steps.Select(s => s.Title)));

Conditionals decide whether a block appears. Plain if statements work because the model is built imperatively:

var section = doc.AddSection();
if (report.HasExecutiveSummary)
section.AddParagraph(report.Summary);
section.AddParagraph(report.Body);
if (report.IsConfidential)
section.AddParagraph("CONFIDENTIAL",
s => s.Bold().WithForegroundColor(Color.Red));

For optional content that’s expensive to compute, guard the construction, not just the Add: there’s no point building a block you won’t use.

Use LINQ to shape the data, then map each group to a section or a heading:

var byCategory = products
.GroupBy(p => p.Category)
.OrderBy(g => g.Key);
foreach (var group in byCategory)
{
var section = doc.AddSection();
section.H2(group.Key);
foreach (var product in group.OrderBy(p => p.Name))
section.AddParagraph($"{product.Name}, ${product.Price:0.00}");
}

Data-driven code stays readable when each element maps to a component or helper, so the loop body is one line:

foreach (var order in orders)
section.Add(OrderCard(order)); // OrderCard returns a Block
// where OrderCard is a component:
static Block OrderCard(Order order) =>
new ContainerBlock()
.WithBorder(Color.Gray, 1)
.WithPadding(10)
.Add(new Paragraph($"Order #{order.Id}") { Style = new Style().Bold() })
.Add(new Paragraph($"Placed {order.Date:d}, {order.Total:C}"));

The loop describes the structure (“a card per order”); the component describes the content (“what a card looks like”). That separation is what keeps generated documents maintainable as both the data and the design evolve.

  • Loop to repeat, if to include: content is just imperative C#.
  • Shape data with LINQ first (group, order, filter), then map to document structure.
  • Push per-element rendering into a component so loop bodies stay one line.
  • Format values at the edge (ToString("C"), :0.00) where you add them, close to the data.