Generate PDFs in ASP.NET Core

Add a PDF download to an existing ASP.NET Core page, or generate a PDF from a template. The examples use PDFCrowd's HTML to PDF API and work for invoices, reports, receipts, and other documents built from application data.

Set up the client

Install the .NET client in your application project:

dotnet add package Pdfcrowd.Official

You can run the examples below with the included demo credentials, or replace them with your PDFCrowd username and API key.

Add a PDF download to an existing page

An existing controller can display its usual HTML page and return a PDF when the user clicks a download button. Reuse the page's view, data preparation, and access checks.

Choose what to send to PDFCrowd:

Convert rendered HTML

This example extends a document page in an ASP.NET Core MVC application using Razor views. GetDocumentForCurrentUser(id, User) represents your application's document lookup and permission checks; substitute your own logic here. The view at Views/Documents/Details.cshtml receives the document as its model.

Keep your application's authentication setup and access restrictions. [Authorize] requires a signed-in user, while the lookup checks access to the requested document.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
// Standard C# and ASP.NET Core HTTP imports are omitted.

[Authorize]
[AutoValidateAntiforgeryToken]
public class DocumentsController(RazorRenderer renderer) : Controller
{
    [AcceptVerbs("GET", "POST")]
    [Route("documents/{id:int}")]
    public async Task<IActionResult> Details(int id)
    {
        var document = GetDocumentForCurrentUser(id, User);

        if (HttpMethods.IsPost(Request.Method) &&
            Request.HasFormContentType &&
            (await Request.ReadFormAsync()).ContainsKey("download_pdf"))
        {
            var html = await renderer.RenderAsync(
                "/Views/Documents/Details.cshtml", document, ControllerContext);

            var client = new pdfcrowd.HtmlToPdfClient("demo", "demo");
            client.setContentViewportWidth("balanced");
            byte[] pdf = client.convertString(html);

            return File(pdf, "application/pdf", $"document-{id}.pdf");
        }

        return View("Details", document);
    }
}

RazorRenderer renders the view to an HTML string before conversion. Add this helper to your application; it uses the configured Razor view engine, including the view's layout and partials:

using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
// Standard C# and other ASP.NET Core imports are omitted.

public sealed class RazorRenderer(
    IRazorViewEngine views,
    IModelMetadataProvider metadata,
    ITempDataProvider tempData,
    IServiceProvider services,
    IConfiguration configuration)
{
    public async Task<string> RenderAsync<TModel>(
        string viewPath, TModel model, ActionContext? context = null)
    {
        context ??= new ActionContext(
            new DefaultHttpContext { RequestServices = services },
            new RouteData(), new ActionDescriptor());

        var result = views.GetView(null, viewPath, isMainPage: true);
        if (!result.Success)
            throw new InvalidOperationException($"Razor view not found: {viewPath}");

        var data = new ViewDataDictionary<TModel>(metadata, new ModelStateDictionary())
        {
            Model = model,
            ["PdfBaseUrl"] = configuration["Pdf:BaseUrl"]
        };
        using var output = new StringWriter();
        var viewContext = new ViewContext(
            context, result.View, data,
            new TempDataDictionary(context.HttpContext, tempData),
            output, new HtmlHelperOptions());

        await result.View.RenderAsync(viewContext);
        return output.ToString();
    }
}

Register the helper in Program.cs before builder.Build(), alongside your existing AddControllersWithViews() setup:

builder.Services.AddScoped<RazorRenderer>();

Pass the data your view and layout need through the model. The helper creates fresh view data for each render; values assigned only to the controller's ViewData are not copied. The current ControllerContext supplies request information for Razor helpers. PdfBaseUrl supports the asset configuration below.

Add this form to Details.cshtml, outside any existing form. It assumes MVC's Form Tag Helper is enabled in your existing _ViewImports.cshtml:

<form method="post" asp-antiforgery="true" class="pdfcrowd-remove">
    <button type="submit" name="download_pdf" value="1">Download PDF</button>
</form>

The form posts to the current URL and includes an antiforgery token. AutoValidateAntiforgeryToken validates POST requests; GET requests continue to display HTML. A POST containing download_pdf renders the same view, converts it, and returns a PDF. The pdfcrowd-remove class hides the form in the PDF while leaving it visible on the webpage.

Conversion finishes before the PDF response is created, with the result held in memory as byte[]. View-rendering and conversion exceptions propagate to your application's normal error handling. Return a generic error to the browser; see SDK error handling for details. Razor rendering is asynchronous, but the SDK's conversion call is synchronous.

To display the PDF in the browser, use File(pdf, "application/pdf") without the download filename. Set page size, margins, or other PDF options before convertString().

Make CSS and images available

PDFCrowd needs reachable URLs for stylesheets, images, and fonts referenced by the rendered HTML. Absolute asset URLs, such as https://your-site.example/css/document.css, need no additional base URL.

For relative paths, add the public site or directory URL to your application configuration, for example in appsettings.json:

{
    "Pdf": {
        "BaseUrl": "https://your-site.example/"
    }
}

Replace https://your-site.example/ with your website's public URL. Then add this to the Razor view's layout, in <head> before stylesheet links:

@if (ViewData["PdfBaseUrl"] is string baseUrl && !string.IsNullOrWhiteSpace(baseUrl))
{
    <base href="@baseUrl">
}

The helper supplies PdfBaseUrl only when rendering HTML for a PDF. Ordinary page rendering does not add the <base>. Razor escapes its value; do not use Html.Raw here.

For example, /css/document.css resolves to https://your-site.example/css/document.css. A path without a leading slash, such as images/logo.png, resolves relative to the base directory: https://your-site.example/reports/ gives https://your-site.example/reports/images/logo.png. Keep the trailing slash when the base names a directory. This also affects hyperlinks; avoid adding a second <base> if the layout already supplies one.

PDFCrowd cannot reach assets on your development machine through localhost or 127.0.0.1. For local assets, you can instead package the HTML and assets in a ZIP archive. Keep paths relative to the archive's folders and omit the website <base> for that approach.

Convert the page URL instead

Instead of sending rendered HTML, you can ask PDFCrowd to open the page through its URL. Keep the lookup, POST check, client setup, and response code above. Replace the renderer.RenderAsync(...) and convertString() calls with:

byte[] pdf = client.convertUrl(Request.GetEncodedUrl());

Add using Microsoft.AspNetCore.Http.Extensions; for GetEncodedUrl(). It preserves the encoded path and query parameters, including an application's path base.

PDFCrowd makes a separate GET request, so the controller returns HTML; only the original POST triggers conversion. The server must be able to handle that GET while the POST waits. The URL must be reachable from PDFCrowd; localhost cannot reach your development machine. Ensure it uses your public address, with the application's trusted-host and proxy settings configured appropriately.

The separate request does not inherit the user's ASP.NET Core authentication cookie. For protected pages, configure cookies or HTTP authentication as appropriate, or use rendered HTML from the original request.

Generate a PDF from a template

You can also generate a PDF directly from a Razor view and application data. This helper reuses RazorRenderer and the same asset configuration, then returns PDF bytes to its caller:

public sealed class PdfRenderer(RazorRenderer renderer)
{
    public async Task<byte[]> RenderAsync<TModel>(string viewPath, TModel model)
    {
        var html = await renderer.RenderAsync(viewPath, model);
        var client = new pdfcrowd.HtmlToPdfClient("demo", "demo");
        client.setContentViewportWidth("balanced");
        return client.convertString(html);
    }
}

Register it alongside RazorRenderer:

builder.Services.AddScoped<PdfRenderer>();

For example, inject PdfRenderer into a controller and pass an already-authorized document:

byte[] pdf = await pdfRenderer.RenderAsync("/Views/Documents/Pdf.cshtml", document);

Use a view with the matching @model type and a layout suitable for the document. Alternatively, set Layout = null in a view that contains a complete HTML document. The resulting bytes can be used for a download, storage, or email; errors propagate to the caller.

This helper renders without the current request. Its minimal rendering context supplies application services, but not the logged-in user, session, existing TempData values, or route data. Pass all document data through the model, use explicit asset URLs, and keep antiforgery forms and request-dependent helpers out of the view and its layout. Outside an HTTP request, resolve PdfRenderer within an application service scope and keep that scope alive until the awaited call finishes.

For more conversion inputs and PDF settings, see the .NET examples and API reference.