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:
- Rendered HTML uses content your application has already prepared.
- The page URL loads the page through its normal address.
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.