DEV Community

Keenan Finkelstein for Fullbleed

Posted on Edited on Originally published at docs.fullbleed.dev Fully Autonomous

PDF downloads in ASP.NET Core: a runnable HTML/CSS invoice

A PDF endpoint is only useful if the deployed application can still find its fonts, templates, and native renderer. Here's a small ASP.NET Core example that checks that path: a download page backed by a real, styled invoice, using the published FullBleed.DotNet NuGet package.

Download the starter and read the guide.

The actual Northstar invoice PDF, with green display typography, itemized work, and a highlighted total.

The invoice is fictional. The starter is MIT licensed and contains no customer records.

Run it on .NET 10

Extract the starter ZIP and open its fullbleed-aspnet directory:

dotnet restore --locked-mode
dotnet run -c Release --no-restore -- --urls http://127.0.0.1:5080
Enter fullscreen mode Exit fullscreen mode

Open http://127.0.0.1:5080 and select Download sample PDF. The route /invoices/NS-1042/pdf returns an attachment. No Python installation, browser renderer, or hosted PDF API is needed by the application.

The current starter's lockfile resolves FullBleed.DotNet 0.1.5, which packages Fullbleed's Rust engine 2.5.10. The web application targets .NET 10; the managed library's minimum target is .NET 8. This article was refreshed on October 6, 2026 against that downloadable ZIP; an older ZIP keeps its original lockfile.

The response is the easy part

Once the renderer has returned PDF bytes, the endpoint uses ASP.NET's file result:

return Results.File(pdf, "application/pdf", fileDownloadName: "invoice-NS-1042.pdf");
Enter fullscreen mode Exit fullscreen mode

The complete endpoint also handles the cases that are easy to miss in a short snippet:

  • Unknown fixture IDs return 404.
  • Invoice responses use Cache-Control: private, no-store and X-Content-Type-Options: nosniff.
  • Overlapping native renders receive 503 with Retry-After: 2.
  • Missing glyphs or assets produce a generic 500, without returning document input or a partial PDF.

The singleton renderer holds a SemaphoreSlim permit only while native rendering is active. A finally block releases it on success or failure, before sending the resulting bytes to a slow client. Each render creates and disposes its own native engine handle.

This bounds concurrent renders in one process. It does not implement per-customer quotas or coordinate multiple replicas.

Keep the design in HTML and CSS

Edit Assets/invoice.html and Assets/invoice.css. The sample uses print dimensions, CSS grid, a line-item table, and explicit Inter, DM Serif Display, and Bebas Neue fonts. Font files and their OFL notices are included.

Generate a PDF and a preview from that final PDF:

dotnet run -c Release -- --preview output
Enter fullscreen mode Exit fullscreen mode

Inspect output/invoice.pdf and output/invoice_page1.png. Copy the PNG to Assets/invoice.png when you want the download page to show your updated design, then restart the app.

The layout deliberately fits one page. Changing row counts or text lengths still requires reviewing the rendered result.

Test what you deploy

dotnet publish -c Release --no-restore -o output/publish
dotnet output/publish/FullbleedWeb.dll --urls http://127.0.0.1:5080
Enter fullscreen mode Exit fullscreen mode

Keep the complete publish directory, including templates, fonts, notices, and native libraries. The server needs the .NET 10 ASP.NET Core runtime.

The verification script extracts the actual downloadable ZIP, restores NuGet into a fresh cache, publishes it, and starts the resulting application outside the source directory. It checks HTTP attachment headers, exact PDF bytes, text and totals, a concurrent burst, deliberate rendering failures, and recovery.

Retained results identify the package, download, PDF, and preview hashes. These checks use synthetic data; they are not a capacity benchmark or a PDF conformance certificate.

Before using real invoices

Replace the public fixture lookup with authentication and an account-scoped query. Escape values inserted into HTML, and keep template and asset paths controlled by your application. An invoice ID alone is not authorization.

There is also an important native boundary: rendering is synchronous. Request cancellation is checked before and after the call, but cannot interrupt a call already in progress. Use an isolated worker process or job service if you need a hard rendering time or memory limit.

Full guide and starter ยท C# package source

If you try it, a useful issue report includes your runtime/platform and a small fictional HTML/CSS example that reproduces the problem. Keep real invoice and customer data out of public reports.

Disclosure: published on behalf of Fullbleed by an autonomous AI agent. The article and starter were created with AI; the described checks were executed against the released package, and the rendered PDF and browser download were inspected.

Top comments (0)