---
title: "Render a Razor component to PDF"
url: https://recursiveai.org/onerender/cookbook/razor-to-pdf/
description: "Render a Razor component to a tagged PDF from an ASP.NET Core endpoint in one call, with its style sheet served from wwwroot, using…"
version: "1.0.0"
---

# Render a Razor component to PDF

Render a Razor component to a tagged PDF from an ASP.NET Core endpoint in one call, with its style sheet served from `wwwroot`, using `RecursiveAI.OneRender.Razor`.

```shell
dotnet new web -o InvoiceWeb
cd InvoiceWeb
dotnet add package RecursiveAI.OneRender.Razor --version 1.0.0
```

The component, `Components/Invoice.razor`, is a whole HTML document with parameters:

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Invoice @Number</title>
  <link rel="stylesheet" href="css/invoice.css">
</head>
<body>
  <h1>Invoice @Number</h1>
  <p class="to">Billed to @Customer</p>
  <table>
    <thead><tr><th>Item</th><th>Qty</th><th>Amount</th></tr></thead>
    <tbody>
      @foreach (var line in Lines)
      {
        <tr><td>@line.Item</td><td>@line.Qty</td><td>@line.Amount.ToString("C", System.Globalization.CultureInfo.GetCultureInfo("en-US"))</td></tr>
      }
    </tbody>
  </table>
  <p class="total">Total @Lines.Sum(l => l.Amount).ToString("C", System.Globalization.CultureInfo.GetCultureInfo("en-US"))</p>
</body>
</html>

@code {
    [Parameter] public string Number { get; set; } = "";
    [Parameter] public string Customer { get; set; } = "";
    [Parameter] public IReadOnlyList<InvoiceLine> Lines { get; set; } = [];

    public record InvoiceLine(string Item, int Qty, decimal Amount);
}
```

`Components/_Imports.razor` holds `@using Microsoft.AspNetCore.Components`, and the style sheet is an ordinary file, `wwwroot/css/invoice.css`:

```css
body { font: 15px/1.5 Arial, sans-serif; color: #222 }
h1 { font-family: Georgia, serif; font-weight: normal; margin: 0 }
.to { color: #555 }
table { width: 100%; border-collapse: collapse }
th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid #ddd }
.total { text-align: right; font-weight: bold }
```

`Program.cs` registers the renderer and maps the endpoint:

```csharp
using InvoiceWeb.Components;
using OneRender.Pdf;
using OneRender.Razor;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOneRender();
var app = builder.Build();

app.MapGet("/invoices/{number}.pdf", async (string number, RazorRenderer renderer, CancellationToken ct) =>
{
    var parameters = new Dictionary<string, object?>
    {
        ["Number"] = number,
        ["Customer"] = "Harbor Street Books",
        ["Lines"] = new Invoice.InvoiceLine[] { new("Shelf labels", 400, 120m), new("Reading lamps", 6, 354m) },
    };
    RazorPdfResult pdf = await renderer.RenderPdfAsync<Invoice>(parameters,
        new HtmlPdfOptions { Page = PdfPageSetup.Letter }, ct);
    foreach (string missing in pdf.MissingAssets)
        app.Logger.LogWarning("Missing asset {Path}", missing);
    return Results.File(pdf.Bytes!, "application/pdf", $"invoice-{number}.pdf");
});

app.Run();
```

Run it and request `/invoices/INV-1042.pdf`: the response is a one-page Letter PDF titled “Invoice INV-1042”, tagged, styled by `invoice.css`, with no missing assets logged.

## How it works

- **`AddOneRender()`** registers a singleton `RazorRenderer` over the app’s web root. Each call renders the component in a fresh service scope, so scoped services behave as they do in a request, and components may await.
- **Assets** (`<link>`, `<img>`, `@font-face`) are served from `wwwroot` and the static web assets of referenced libraries (`_content/<Library>/...`). `data:` addresses work, and `..` segments are refused. An asset that isn’t found never throws: it is listed in `MissingAssets`.
- **Fragments.** A component that renders only a fragment, with no `<html>` element, is wrapped in a document using `RazorRenderOptions.Language` (`en` by default), `Title` and `Stylesheets`. Set those in `AddOneRender(o => ...)` so a tagged PDF still gets a title and language.
- **Options pass through.** `HtmlPdfOptions` sets the page size and output, such as a [PDF/UA profile](https://recursiveai.org/onerender/cookbook/accessible-pdf/). Its `BaseUrl` and `Resources` are replaced by the web root, which is asked first.

## Images and HTML

`RenderImageAsync<T>` returns a `RazorImageResult` with `ToPng()`, taking `HtmlRenderOptions` for the viewport:

```csharp
RazorImageResult img = await renderer.RenderImageAsync<Invoice>(parameters, new HtmlRenderOptions { Width = 800, Height = 600 }, ct);
byte[] png = img.ToPng();
```

`RenderHtmlAsync<T>` returns the markup alone, and `RenderPdfAsync<T>(stream, ...)` writes to your stream, such as the response body, and returns the page count with `Bytes` null.

The package needs the ASP.NET Core shared framework. Everything it does goes through the public `HtmlPdf` and `HtmlRender` APIs. The details are in [Razor components](https://recursiveai.org/onerender/docs/razor/).
