Streamlining .NET Invoice PDFs

Sagar HSSagar HS
4 min read

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:

  1. App calls IInvoiceGenerator.GenerateAsync

  2. InvoicePdfGenerator first invokes the Razor renderer

  3. Razor compiles & renders the .cshtml into HTML

  4. InvoicePdfGenerator passes HTML to the PDF engine

  5. 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
    Use RenderViewToStringAsync so .NET threads aren’t blocked.

  • Precompile Views
    For high-volume scenarios, consider precompiling Razor templates to eliminate first-request JIT.

  • Dependency Reuse
    Keep HtmlToPdf 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 to GenerateAsync.

  • Advanced Model Data
    Enrich Invoice with tax breakdowns, discounts, payment links—either as strong properties or via CustomFields 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

0
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.