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.
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:
<!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:
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:
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 singletonRazorRendererover 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 fromwwwrootand 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 inMissingAssets. - Fragments. A component that renders only a fragment, with no
<html>element, is wrapped in a document usingRazorRenderOptions.Language(enby default),TitleandStylesheets. Set those inAddOneRender(o => ...)so a tagged PDF still gets a title and language. - Options pass through.
HtmlPdfOptionssets the page size and output, such as a PDF/UA profile. ItsBaseUrlandResourcesare replaced by the web root, which is asked first.
Images and HTML
RenderImageAsync<T> returns a RazorImageResult with ToPng(), taking HtmlRenderOptions for the viewport:
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.