Streamlining .NET Invoice PDFs

Automating PDF invoice creation in .NET shouldn’t feel like wrestling with command-line tools or juggling raw HTML. InvoiceGenerator brings together Razor templating, IronPdf rendering, and idiomatic dependency-injection wiring into one cohesive library. Below, we unpack each layer so you can plug it into your application with confidence.
1. Overall Architecture
sequenceDiagram
participant App as Your .NET App
participant InvGen as IInvoiceGenerator
participant PdfGen as InvoicePdfGenerator
participant Razor as IRazorViewToStringRenderer
participant Engine as IPdfGenerator
participant View as Views/InvoiceReport.cshtml
participant IronPdf as IronPdf.HtmlToPdf
App->>InvGen: GenerateAsync(model)
InvGen->>PdfGen: GenerateAsync(model)
PdfGen->>Razor: RenderViewToStringAsync("InvoiceReport", model)
Razor->>View: compile & render with model
View-->>Razor: HTML string
Razor-->>PdfGen: HTML string
PdfGen->>Engine: GeneratePdf(html)
Engine-->>PdfGen: PDF bytes
PdfGen-->>App: byte[]
This sequence diagram emphasises the call-flow:
App calls
IInvoiceGenerator.GenerateAsync
InvoicePdfGenerator first invokes the Razor renderer
Razor compiles & renders the
.cshtml
into HTMLInvoicePdfGenerator passes HTML to the PDF engine
IronPdf produces the final PDF bytes, returned all the way back to your app.
2. Contract Definitions
public interface IInvoiceGenerator
{
Task<byte[]> GenerateAsync(Invoice model);
}
public interface IRazorViewToStringRenderer
{
Task<string> RenderViewToStringAsync(string viewName, object model);
}
public interface IPdfGenerator
{
byte[] GeneratePdf(string html);
}
Why interfaces?
Swap out Razor for another templating engine
Replace IronPdf with DinkToPdf, wkhtmltopdf, etc.
Write unit tests against mocks without spinning up actual rendering
3. Dependency-Injection Setup
Extension method in InvoiceGenerator.Core.Extensions.ServiceCollectionExtensions
:
public static IServiceCollection AddInvoiceGenerator(this IServiceCollection services)
{
// 1. Add Razor templating support
services.AddRazorTemplating();
// 2. PDF generator using IronPdf
services.AddSingleton<IPdfGenerator, PdfGenerator>();
// 3. Orchestrator
services.AddSingleton<IInvoiceGenerator, InvoicePdfGenerator>();
return services;
}
Usage in Program.cs
:
var services = new ServiceCollection()
.AddInvoiceGenerator()
.BuildServiceProvider();
var invoiceGenerator = services.GetRequiredService<IInvoiceGenerator>();
4. Razor Template Internals
Location: Views/InvoiceReport.cshtml
Key sections:
Model declaration
@model InvoiceGenerator.Core.Contracts.Invoice
Header: invoice number, issue/due dates
Address blocks: seller vs. customer
Line-item table: loops through
Model.LineItems
Custom fields: optional dictionary entries
Example snippet:
<table>
<thead>
<tr>
<th>Item</th><th>Qty</th><th>Unit Price</th><th>Total</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.LineItems)
{
<tr>
<td>@item.Name</td>
<td>@item.Quantity</td>
<td>@item.Price:C</td>
<td>@(item.Quantity * item.Price):C</td>
</tr>
}
</tbody>
</table>
You can extend, override layouts, inject CSS, or use partials—Razor gives you full view-engine power.
5. PDF Conversion with IronPdf
Implementation in PdfGenerator
:
public class PdfGenerator : IPdfGenerator
{
public byte[] GeneratePdf(string html)
{
var renderer = new IronPdf.HtmlToPdf();
// Optional: tweak renderer.Options for margins, headers/footers
var pdfDoc = renderer.RenderHtmlAsPdf(html);
return pdfDoc.BinaryData;
}
}
Tips:
Reuse a single
HtmlToPdf
instance for batch jobs.Configure headers/footers for page numbers or watermarks via
renderer.PrintOptions
.Ensure any external CSS or images are embedded or accessible.
6. Orchestration Logic
InvoicePdfGenerator
ties it all together:
public class InvoicePdfGenerator : IInvoiceGenerator
{
private readonly IRazorViewToStringRenderer _renderer;
private readonly IPdfGenerator _pdfGenerator;
public InvoicePdfGenerator(
IRazorViewToStringRenderer renderer,
IPdfGenerator pdfGenerator)
{
_renderer = renderer;
_pdfGenerator = pdfGenerator;
}
public async Task<byte[]> GenerateAsync(Invoice model)
{
// 1. Render Razor view into HTML string
string html = await _renderer.RenderViewToStringAsync("InvoiceReport", model);
// 2. Convert HTML to PDF
return _pdfGenerator.GeneratePdf(html);
}
}
This single‐method orchestration keeps your calling code focused on building the Invoice
model and handling the resulting byte[]
.
7. Best Practices & Performance
Async Rendering
UseRenderViewToStringAsync
so .NET threads aren’t blocked.Precompile Views
For high-volume scenarios, consider precompiling Razor templates to eliminate first-request JIT.Dependency Reuse
KeepHtmlToPdf
instances alive for batch operations to reduce initialization overhead.Error Handling
Wrap rendering and PDF steps with try/catch to capture template errors or conversion failures, then log with context (invoice number, customer ID).
8. Extending & Customizing
Alternative PDF Engines
public class MyPdfGenerator : IPdfGenerator { public byte[] GeneratePdf(string html) { /* use DinkToPdf */ } } // Then in ConfigureServices: services.AddSingleton<IPdfGenerator, MyPdfGenerator>();
Multiple Templates
Render different.cshtml
files (e.g.ProformaInvoice.cshtml
,CreditNote.cshtml
) by passing the view name toGenerateAsync
.Advanced Model Data
EnrichInvoice
with tax breakdowns, discounts, payment links—either as strong properties or viaCustomFields
for key/value flexibility.
Conclusion
InvoiceGenerator.Core delivers a clear separation of concerns:
Contracts define “what” happens.
Services implement “how” it happens.
Views supply “what it looks like.”
With minimal DI setup and pluggable components, you can automate invoice PDFs in any .NET 8+ project predictably, and with enough hooks to adapt as your billing requirements evolve.
Get started:
dotnet add package InvoiceGenerator.Core
Explore the source and contribute on GitHub
Subscribe to my newsletter
Read articles from Sagar HS directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Sagar HS
Sagar HS
Software engineer with 4 + years delivering high-performance .NET APIs, polished React front-ends, and hands-off CI/CD pipelines. Hackathon quests include AgroCropProtocol, a crop-insurance DApp recognised with a World coin pool prize and ZK Memory Organ, a zk-SNARK privacy prototype highlighted by Torus at ETH-Oxford. Recent experiments like Fracture Log keep me exploring AI observability.