Skip to content

Styling via extension methods

C# extension methods let you add fluent, discoverable shortcuts to types you don’t own, including Rivoli PDF’s Section, Style, Paragraph, and the rest. This is how you give your codebase a vocabulary that reads like part of the library: section.H2(...), para.Branded(), style.AsHeading().

Rivoli PDF ships several of these itself, which is the clearest illustration of the pattern.

The fluent helpers you’ve already used are extension methods on the model types, grouped by the type they extend:

  • SectionExtensions: H1(...)H6(...), AddParagraph(...), AddUnorderedList(...), AddOrderedList(...), AddTable(...), AddImage(...), AddSpace(...), AddPageBreak().
  • ListExtensions: Item(...), Items(...).
  • TableExtensions: Header(...), Row(...), Rows(...).
  • DocumentExtensions: WithSection(...), WithTitle(...), and friends.

They follow one shape: take the receiver as this, do the work, return the receiver so calls chain.

// from the library, paraphrased
public static Section H2(this Section section, string text) =>
AddHeading(section, text, 2);
public static Section AddParagraph(this Section section, string text, Action<Style> configure)
{
var style = new Style();
configure(style);
section.Add(new Paragraph(text) { Style = style });
return section;
}

Because they return Section, they chain with everything else:

section
.H1("Quarterly Report")
.AddParagraph("Prepared by the finance team.")
.AddParagraph("Confidential", s => s.Italic().WithForegroundColor(Color.Gray))
.AddPageBreak();

The highest-value extensions in your own code are usually on Style: they capture your brand as named, reusable formatting:

using Rivoli.Pdf.Styling;
public static class BrandStyles
{
private static readonly Color Accent = Color.FromRgb(0, 90, 160);
public static Style Branded(this Style style) =>
style.WithFontFamily("Helvetica").WithForegroundColor(Accent);
public static Style AsHeading(this Style style, double size) =>
style.Branded().WithFontSize(size).Bold();
public static Style AsCaption(this Style style) =>
style.WithFontSize(9).Italic().WithForegroundColor(Color.Gray);
}

Now styling reads as intent, and the brand lives in one file:

var heading = new Style().AsHeading(20);
var caption = new Style().AsCaption();

You can also extend the content types to encode house conventions: a branded paragraph, a standard signature line:

public static class DocExtensions
{
public static Section Branded(this Section section, string text) =>
section.AddParagraph(text, s => s.Branded());
public static Section SignatureLine(this Section section, string name)
{
section.AddParagraph("_____________________________");
section.AddParagraph(name, s => s.AsCaption());
return section;
}
}
// usage
section
.Branded("Acme Corporation")
.SignatureLine("Authorized signatory");

Both reduce repetition, but they answer different questions:

  • An extension method adds a verb: a fluent shortcut that mutates or configures something in place (section.Branded(...), style.AsHeading(...)).
  • A component produces a noun: a self-contained Block you place wherever you want (section.Add(Callout(...))).

Use extensions for fluent styling and small content shortcuts; use components for reusable structural pieces. They compose well together: a component’s internals are often built with your own style extensions.

  • Group by receiver type in a static class (SectionExtensions, StyleExtensions): mirrors how the library organizes its own.
  • Always return the receiver so your extensions chain with the built-in fluent API.
  • Keep one namespace for your extensions so a single using brings the whole vocabulary into scope.
  • Don’t shadow the built-in helpers; pick distinct, intent-revealing names.