Generate a multi-page PDF table in C# with OnePdf.Layout: column widths, striped rows, header and footer rows on every page, spans and page numbers.
dotnet add package RecursiveAI.OnePdf.Layout --version 1.11.0using System.Globalization; using OnePdf.Layout; var us = CultureInfo.GetCultureInfo("en-US"); var rows = Enumerable.Range(1, 120) .Select(i => (Date: new DateOnly(2026, 1, 1).AddDays(i * 2), Crew: $"Crew {(i % 6) + 1}", Kg: 40m + (i * 37 % 90))) .ToList(); LayoutDocument.Compose(doc => { doc.Title("Catch log, 2026").Language("en-US"); doc.Page(page => { page.Size(PageSize.Letter); page.Margin(54); page.Content().Column(col => { col.Spacing(12); col.Item().Heading(1).Text("Catch log, 2026").FontSize(20).Bold(); col.Item().Table(table => { table.Columns(c => { c.Constant(90); // a fixed width in points c.Relative(2); // shares the rest of the width by weight c.Relative(1); }); table.CellPadding(4); table.CellBorder(0.5, Colors.LightGrey); table.RowBackground(i => i % 2 == 1 ? LayoutColor.FromHex("#F2F2F2") : null); // striped body rows table.HeaderRow(r => // repeated at the top of every page { r.Cell().Text("Date").Bold(); r.Cell().Text("Crew").Bold(); r.Cell().AlignEnd().Text("Catch (kg)").Bold(); }); foreach (var row in rows) { table.Row(r => { r.Cell().Text(row.Date.ToString("MMM d", us)); r.Cell().Text(row.Crew); r.Cell().AlignEnd().Text(row.Kg.ToString("N1", us)); }); } table.FooterRow(r => // repeated at the bottom of every page { r.Cell().Span(2).Text("Total, all crews").Bold(); // one cell over two columns r.Cell().AlignEnd().Text(rows.Sum(x => x.Kg).ToString("N1", us)).Bold(); }); }); }); page.Footer().AlignCenter().Text(t => { t.FontSize(9); t.Span("Page "); t.CurrentPageNumber(); t.Span(" of "); t.TotalPages().ReservedDigits(2); }); }); }).Save("catch-log.pdf");
The 120 rows fill five US Letter pages. Each page starts with the “Date, Crew, Catch (kg)” header row and ends with the total row, above the page number.
Columns
Constant(points)keeps a fixed width.Relative(weight)shares the width left over by weight, and never gets narrower than its longest word.Auto()takes the width of its widest content when there is room. AnAutocolumn makes every row count for the width, so preferConstantandRelativefor long tables.
Rows and cells
HeaderRowandFooterRowrepeat on every page the table reaches and never split. Body rows split between lines of text;r.KeepTogether()moves a row to the next page instead.Span(columns, rows)merges cells.CellBackground,CellBorder,CellPaddingandCellAlignset one cell.RowHeader()makes a cell a row header for screen readers.- Borders are collapsed: where two cells meet they share one line.
- A cell holds any content: text, an image, a nested column, another table.
Good to know
ReservedDigitssets the room left for the total page count, which isn’t known until the end. Reserve as many digits as the count can have; a centered footer shows the unused room as a gap.- For a running subtotal per page (“Carried forward”), use
Accumulateon the amount cells andDynamicTextin the footer, as shown in Layout. - The output is tagged, with
THheader cells, so the same document can be saved as PDF/UA: see Create PDF/UA. Save(path)writes page by page, so memory stays small for long tables, and layout time grows in proportion to the number of rows.- Text is set in Liberation Sans unless you register other fonts on
doc.Fonts.
Every table option and the column-width rules are in Layout.
For a complete multi-page invoice with an ASP.NET Core download, see Create PDF invoices and reports in C#.
Coming from MigraDoc tables: Migrate from PDFsharp and MigraDoc.