Flexible PDF Reporting in .NET Using Razor Views

Milan Jovanović - Jun 30 - - Dev Community

I'll never forget when I was working on a project that required generating weekly sales reports for a client. The initial solution involved a clunky process of exporting data, manipulating it in spreadsheets, and manually creating PDFs. It was tedious, error-prone, and it sucked up way too much of my time. I knew there had to be a better way.

That's when I discovered the power of combining Razor views with HTML-to-PDF conversion. You have more control over formatting the document. You can use modern CSS to style the HTML markup, which will be applied when exporting to a PDF document. It's also simple to implement in ASP.NET Core.

Here's what we'll cover:

  • Understanding Razor views
  • Converting Razor views to HTML
  • HTML to PDF conversion in .NET
  • Putting it all together with Minimal APIs

Let's dive in!

Razor Views

Razor viewsare an HTML template with embedded Razor markup. Razor allows you to write and execute .NET code inside a web page. Views have a special .cshtml file extension. They're commonly used in ASP.NET Core MVC,Razor Pages, and Blazor.

However, you can define Razor views in a class library or ASP.NET Core Web API project.

You can use the Model object to pass in data to a Razor view. Inside the .cshtml file, you'll specify the model type with the @model keyword. In the example below, I'm specifying that the Invoice class is the model for this view. You can access the model instance with the Model property in the view.

This is the InvoiceReport.cshtml view we'll use to generate a PDF invoice.

You can write CSS in the Razor view inline or reference a stylesheet. I'm using the Tailwind CSS utility framework, which uses inline CSS. I usually delegate this to a front-end engineer on my team so they can stylize the report as needed.

@using System.Globalization
@using HtmlToPdf.Contracts

@model HtmlToPdf.Contracts.Invoice

@{
    IFormatProvider cultureInfo = CultureInfo.CreateSpecificCulture("en-US");
    var subtotal = Model.LineItems.Sum(li => li.Price * li.Quantity).ToString("C", cultureInfo);
    var total = Model.LineItems.Sum(li => li.Price * li.Quantity).ToString("C", cultureInfo);
}

<script src="https://cdn.tailwindcss.com"></script>

<div class="min-w-7xl flex flex-col bg-gray-200 space-y-4 p-10">
    <h1 class="text-2xl font-semibold">Invoice #@Model.Number</h1>

    <p>Issued date: @Model.IssuedDate.ToString("dd/MM/yyyy")</p>
    <p>Due date: @Model.DueDate.ToString("dd/MM/yyyy")</p>

    <div class="flex justify-between space-x-4">
        <div class="bg-gray-100 rounded-lg flex flex-col space-y-1 p-4 w-1/2">
            <p class="font-medium">Seller:</p>
            <p>@Model.SellerAddress.CompanyName</p>
            <p>@Model.SellerAddress.Street</p>
            <p>@Model.SellerAddress.City</p>
            <p>@Model.SellerAddress.State</p>
            <p>@Model.SellerAddress.Email</p>
        </div>
        <div class="bg-gray-100 rounded-lg flex flex-col space-y-1 p-4 w-1/2">
            <p class="font-medium">Bill to:</p>
            <p>@Model.CustomerAddress.CompanyName</p>
            <p>@Model.CustomerAddress.Street</p>
            <p>@Model.CustomerAddress.City</p>
            <p>@Model.CustomerAddress.State</p>
            <p>@Model.CustomerAddress.Email</p>
        </div>
    </div>

    <div class="flex flex-col bg-white rounded-lg p-4 space-y-2">
        <h2 class="text-xl font-medium">Items:</h2>
        <div class="">
            <div class="flex space-x-4 font-medium">
                <p class="w-10">#</p>
                <p class="w-52">Name</p>
                <p class="w-20">Price</p>
                <p class="w-20">Quantity</p>
            </div>

            @foreach ((int index, LineItem item) in Model.LineItems.Select((li, i) => (i + 1, li)))
            {
                <div class="flex space-x-4">
                    <p class="w-10">@index</p>
                    <p class="w-52">@item.Name</p>
                    <p class="w-20">@item.Price.ToString("C", cultureInfo)</p>
                    <p class="w-20">@item.Quantity.ToString("N2")</p>
                </div>
            }
        </div>
    </div>

    <div class="flex flex-col items-end bg-gray-50 space-y-2 p-4 rounded-lg">
        <p>Subtotal: @subtotal</p>
        <p>Total: <span class="font-semibold">@total</span></p>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Converting Razor Views to HTML

The next thing we'll need is a way to convert the Razor view into HTML. We can do this with the Razor.Templating.Core library. It provides a simple API to render a .cshtml file into a string.

Install-Package Razor.Templating.Core
Enter fullscreen mode Exit fullscreen mode

You can use the RazorTemplateEngine static class to call the RenderAsync method. It accepts the path to the Razor view and the model instance that will be passed to the view.

Here's what that will look like:

Invoice invoice = invoiceFactory.Create();

string html = await RazorTemplateEngine.RenderAsync(
    "Views/InvoiceReport.cshtml",
    invoice);
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can use the IRazorTemplateEngine instead of the static class. In that case, you must call AddRazorTemplating to register the required services with DI. This is also required if you want to use dependency injection inside the Razor views with @inject. It's recommended that you call AddRazorTemplating after registering all dependencies.

services.AddRazorTemplating();
Enter fullscreen mode Exit fullscreen mode

HTML to PDF conversion

Now that we've converted our Razor view into HTML, we can use it to generate a PDF report. Many libraries offer this functionality. The library I've used most often is IronPDF. It's a paid library (and well worth it), but I know developers also want free options, so I'll list some alternatives at the end.

We can use IronPDF's ChromePdfRenderer, which uses an embedded Chrome browser. The renderer exposes the RenderHtmlAsPdf method, which generates a PdfDocument. Once you have the document, you can store it on the file system or export it as binary data.

var renderer = new ChromePdfRenderer();

using var pdfDocument = renderer.RenderHtmlAsPdf(html);

pdfDocument.SaveAs($"invoice-{invoice.Number}.pdf");
Enter fullscreen mode Exit fullscreen mode

If you're looking for free options, check out Puppeteer Sharp. It's a .NET port of the Puppeteer library, which allows you to run a headless Chrome browser.

Another (conditionally) free option to consider is NReco.PdfGenerator. However, it's only free for single-server deployments.

Putting It All Together

Let's use everything we discussed to create a Minimal API endpoint to generate an invoice PDF report and return it as a file response. Here's the code snippet:

app.MapGet("invoice-report", async (InvoiceFactory invoiceFactory) =>
{
    Invoice invoice = invoiceFactory.Create();

    var html = await RazorTemplateEngine.RenderAsync(
        "Views/InvoiceReport.cshtml",
        invoice);

    var renderer = new ChromePdfRenderer();

    using var pdfDocument = renderer.RenderHtmlAsPdf(html);

    return Results.File(
        pdfDocument.BinaryData,
        "application/pdf",
        $"invoice-{invoice.Number}.pdf");
});
Enter fullscreen mode Exit fullscreen mode

This is what the generated PDF report looks like:

Image description

You can grab the source code for this sample here. Feel free to try out a different library for HTML to PDF conversion.

Summary

In this article, we've explored the power of using Razor views for flexible PDF reporting in .NET. We've seen how to create report templates with Razor views, convert them to HTML, and then transform that HTML into beautifully formatted PDF documents.

Whether you need to generate invoices, sales reports, or any other kind of structured document, this approach offers a simple and customizable solution.

Here's what you can explore next:

That's all for this week. Stay awesome!


P.S. Whenever you're ready, there are 3 ways I can help you:

  1. Modular Monolith Architecture (NEW): Join 650+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.

  2. Pragmatic Clean Architecture: Join 2,800+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.

  3. Patreon Community: Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .