Blog/Guides

Generate PDFs in C# and ASP.NET Core

Build an ASP.NET Core PDF download endpoint with the official C# SDK, HTML templates, cancellation, and server-side error handling.

Guides··4 min read

An ASP.NET Core application can generate a PDF by rendering an HTML document on its server, sending that HTML to RelayPDF, and returning the PDF bytes as a file response. The API key stays in the application environment; the browser receives a PDF download.

The example implements GET /invoices/1042/pdf for one fixed synthetic invoice. It includes a complete .NET 8 project, an HTML template, cancellation handling, and an SDK dependency pinned to the version used by the example. Download the example projects, fixtures, and results.

Run the complete project

Install the .NET 8 SDK, authenticate using npx @relaypdf/cli setup --env .env, and load the generated environment file before starting the application. From the archive root:

set -a
. ./.env
set +a
dotnet restore dotnet/InvoiceDemo.csproj
dotnet run --project dotnet/InvoiceDemo.csproj --urls http://127.0.0.1:5080

In another terminal, download the fixed fixture:

curl --fail http://127.0.0.1:5080/invoices/1042/pdf --output invoice-1042.pdf

The example uses the official RelayPDF NuGet package. It does not require a local browser installation: HTML rendering takes place in the API's browser service. Local Chrome is needed only if you also run the separate rendering comparison included in the archive.

Keep template rendering on the server

The template defines A4 paper, margins, a simple table, and a system-font stack. In this small example, a single customer placeholder is replaced with HTML-encoded text. Encoding matters even for business data: a customer name containing < or & must remain text when inserted into HTML.

For a larger application, use your existing view or template engine with automatic escaping. Avoid building an entire invoice through string concatenation, especially when it contains optional sections or nested line items. Do not accept arbitrary HTML from an unauthenticated download endpoint.

The complete endpoint is:

using System.Net;
using RelayPDF;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(_ => new RelayPDF.RelayPDF(
    Environment.GetEnvironmentVariable("RELAYPDF_API_KEY") ?? throw new InvalidOperationException("Run relaypdf setup first"),
    Environment.GetEnvironmentVariable("RELAYPDF_BASE_URL")));
var app = builder.Build();
var template = File.ReadAllText(Path.Combine(app.Environment.ContentRootPath, "invoice.html"));
// Fixed synthetic record. In a real app, authorize access to the invoice before rendering it.
app.MapGet("/invoices/1042/pdf", async (RelayPDF.RelayPDF client, HttpContext context) =>
{
    using var deadline = CancellationTokenSource.CreateLinkedTokenSource(context.RequestAborted);
    deadline.CancelAfter(TimeSpan.FromSeconds(60));
    var html = template.Replace("{{customer}}", WebUtility.HtmlEncode("Example Workshop"));
    try
    {
        var result = await client.Pdf.FromHtmlAsync(html,
            extra: new() { ["response"] = "binary", ["filename"] = "invoice-1042.pdf" },
            ct: deadline.Token);
        if (result is not BinaryResult pdf) return Results.Problem("Unexpected PDF response", statusCode: 502);
        context.Response.Headers.CacheControl = "private, no-store";
        return Results.File(pdf.Bytes, "application/pdf", "invoice-1042.pdf");
    }
    catch (RelayPDFException error)
    {
        if (error.RetryAfter is int seconds) context.Response.Headers.RetryAfter = seconds.ToString();
        return Results.Problem(title: "PDF generation failed", detail: error.Code,
            statusCode: error.Status == 429 ? 429 : 502);
    }
    catch (OperationCanceledException) when (!context.RequestAborted.IsCancellationRequested)
    {
        return Results.Problem("PDF generation timed out", statusCode: 504);
    }
});
app.Run();

Return the file with explicit download behavior

Results.File sets the content type and download filename. The response uses private, no-store because this example demonstrates a business document. Decide caching deliberately when adding your own authentication and invoice authorization.

The sample has only a fixed demonstration record. In a real application, resolve the requested invoice within the authenticated customer's account before rendering. A valid login alone does not establish access to an arbitrary invoice ID.

Microsoft documents the byte-array and stream forms of file responses in its Minimal API response guide. This example uses the byte-array form because the SDK's binary result already holds the PDF bytes.

Bound the request lifetime

The linked cancellation token combines client disconnects with a 60-second application deadline. A deadline expiring produces a gateway timeout when the client is still connected. Cancellation stops waiting locally; it does not prove that remote work was canceled or that no charge occurred.

For short invoices, a direct download is convenient. For a large statement run or an Office conversion that may outlast your hosting timeout, submit an async job, persist its ID, and deliver the result after completion. See bulk PDF generation for a durable example.

The SDK exposes error status and a retry interval where supplied. The endpoint passes through rate-limit responses and returns a generic upstream error for other failures. Do not expose raw provider errors or document contents to the browser. A production application may map additional known errors to more specific user-facing messages.

Check the output, not just the HTTP status

The fixture should contain “Invoice 1042,” “Example Workshop,” and “Total USD 125.00.” Check the PDF's page count and searchable text, then inspect the first rendered page. A successful HTTP response cannot tell you that the last table row fits or that the intended font loaded.

Start with a short invoice, then add a long customer name, a multi-line address, enough line items to cross a page, and a missing optional field. Those cases expose template weaknesses earlier than another one-line smoke test. The PDF regression guide shows how to automate the checks.

For fonts and pagination, use the existing font guide and page-break guide. The C# SDK reference documents the remaining methods, while HTML-to-PDF describes the conversion surface.

The supplied project was compiled and its download endpoint exercised against staging on September 7, 2026. It returned HTTP 200 and passed three invoice-text checks. Inspect the generated PDF and application check results.

Ready to generate?

One API for HTML, Markdown, URLs, and Office. REST, SDK, CLI, or MCP.