Return a PDF from an ASP.NET controller
What you’ll do
Return a PDF from a web request. The caller asks for a company and a month, your own code loads the data, and Paperwork.Core makes the document and sends it back.
Before you start
- A .NET web application, such as ASP.NET Core MVC or Razor Pages.
- The
Paperwork.Corepackage. - A template exported from Paperwork.Design. Open the template and choose Export. That gives one
.configfile holding the layout, styles, data and fields.
This recipe uses the exported invoice template, InvoiceAcme.config. It has the fields name, companyCode and invoiceMonth, and a data source named model. Put the file in a Templates folder in your project.
Steps
1. Register Paperwork once, at startup.
using Paperwork.Generation;
builder.Services.AddPaperwork(builder.Configuration);
This registers IPaperworkFactory so you can inject it. Passing the configuration also applies the Paperwork section of appsettings.json (fonts, imaging, parsing, output and tracing) to every document.
2. Add the controller.
using Microsoft.AspNetCore.Mvc;
using Paperwork; // NewDocument / FromConfig extensions
using Paperwork.Generation; // IPaperworkFactory
public class InvoiceController : Controller
{
private readonly IPaperworkFactory _paperwork;
private readonly IWebHostEnvironment _env;
public InvoiceController(IPaperworkFactory paperwork, IWebHostEnvironment env)
{
_paperwork = paperwork;
_env = env;
}
// GET /invoice/summary?company=Acme&month=3
[HttpGet]
public async Task<IActionResult> Summary(string company, int month, CancellationToken cancel)
{
if (!TryValidate(company, month, out var error))
return BadRequest(error);
var model = await LoadInvoiceDataAsync(company, month, cancel);
if (model is null)
return NotFound($"No invoices for {company} in month {month}.");
var templatePath = Path.Combine(_env.ContentRootPath, "Templates", "InvoiceAcme.config");
await using var template = System.IO.File.OpenRead(templatePath);
var pdf = await _paperwork.FromConfig(template)
.WithData("model", model) // the name the template binds to
.WithField("name", company)
.WithField("companyCode", model.CompanyCode)
.WithField("invoiceMonth", month.ToString())
.BuildBytesAsync();
return File(pdf, "application/pdf", $"{company}-{month:00}.pdf");
}
private static bool TryValidate(string company, int month, out string error)
{
if (string.IsNullOrWhiteSpace(company)) { error = "A company is required."; return false; }
if (month < 1 || month > 12) { error = "Month must be 1 to 12."; return false; }
error = string.Empty;
return true;
}
private Task<InvoiceModel?> LoadInvoiceDataAsync(string company, int month, CancellationToken cancel)
{
// your repository or API call - anything serialisable
throw new NotImplementedException();
}
}
The data model, which is your own type. Any object works:
public class InvoiceModel
{
public string CompanyCode { get; set; } = "";
// the rest of what the invoice template reads
}
How it works
- Validate first. Nothing reaches the template until the request makes sense. A field the template requires, left out here, would fail later and less clearly.
- Load your own data. Paperwork does not reach into your database.
WithDatatakes any object, so your own model type works without serialising it yourself. - Fields are strings. They match the ids in the template:
name,companyCodeandinvoiceMonth. In the template they read as$fields.name, and the data asmodel. BuildBytesAsyncreturns the PDF.File(...)sends it to the caller.
Check it worked
Run the application and open /invoice/summary?company=Acme&month=3. The browser downloads Acme-03.pdf. A missing company or a month outside 1 to 12 returns a 400 message instead.
Options
- Show it in the browser instead of downloading it: leave out the file name,
File(pdf, "application/pdf"). - Handle failures without exceptions: use
BuildAsync(). It returns a result withSuccess,Error,MessageandGenerationDuration, and you map a failure to a 500 yourself. - Protect the document:
.WithUserPassword("...")asks for a password to open it, and.WithSecurity(...)sets what a reader may still do. See Password-protect a PDF. - Other sources:
WithDataFileandWithDataUrlfor data, andWithLayoutFileandWithStyleFileto build a document without an exported template, starting fromNewDocument()instead ofFromConfig.
Things to watch
- Read the template once if it does not change. This example opens the file on every request. Read it into a
byte[]at startup, and give the builder a newMemoryStreamfor each request. - A template that reads a shared asset or a remote source fetches it over the network while the document is made. The server needs outbound access, and it adds to the time.
Next
- Paperwork.Core, for the library.
- Paperwork.CLI, if a script or a scheduled job makes the document rather than a request.