Creating PDF in .NET is not that straight forward as one could imagine. There are many libraries, some of them paid, but with introduction of Playwright.NET a new way have shown up.
Playwright was primarily designed to serve as a reliable end-to-end testing framework. It utilizes headless browser capabilities. I have many use cases for it, starting from testing, through automating reservation of doctor appointment, to creating PDF using HTML template. Playwright enables HTML injection and printing it like you would do that on your desktop’s browser.
Dig in!
Let us start with creating an empty ASP.NET Core Web API project and adding Playwright.NET package:
dotnet new webapi --name HTML2PDF
cd HTML2PDF
dotnet add package Microsoft.Playwright --version 1.27.0
Now create a Service
folder within the project and add PdfService
class:
using Microsoft.Playwright;
public interface IPdfService
{
Task<byte[]> CreateAsync(string html);
}
public class PdfService : IPdfService
{
public async Task<byte[]> CreateAsync(string html)
{
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = true });
var page = await browser.NewPageAsync();
await page.EmulateMediaAsync(new PageEmulateMediaOptions { Media = Media.Screen });
await page.SetContentAsync(html, new PageSetContentOptions() { WaitUntil = WaitUntilState.Load });
return await page.PdfAsync(new PagePdfOptions { Format = "A4" });
}
}
That part of code is will be responsible for creating PDF. It initializes Playwright driver instance and opens browser in headless mode. Next it takes html
, injects it into browser’s page context and creates PDF out of it. The PdfAsync
method has an options to pass path for produced file, but I prefer to return array of bytes and later decide what I will do with it. For the purpose of this post I will just save it in project root directory, but in real life I would prefer to send it into some kind of storage, eg. Azure Blob Storage.
Next add a controller with one HTTP POST method Create
and run the application. Remember to also register our PdfService
in Program.cs
or your Startup
class.
builder.Services.AddSingleton<IPdfService, PdfService>();
[Route("api/[controller]")]
[ApiController]
public class PdfController : ControllerBase
{
private readonly IPdfService _pdfService;
public const string FileDirectory = "test";
public PdfController(IPdfService pdfService)
{
_pdfService = pdfService;
}
[HttpPost]
public async Task<IActionResult> Create()
{
byte[] bytes = await _pdfService.CreateAsync("<h1>Hello World</h1>");
if (!Directory.Exists(FileDirectory))
{
Directory.CreateDirectory(FileDirectory);
}
await System.IO.File.WriteAllBytesAsync($"{FileDirectory}/hello.pdf", bytes);
return Ok();
}
}
If you haven’t installed Playwright before, or have a different version, instead of new PDF file you will probably get the following exception:
Microsoft.Playwright.PlaywrightException: Executable doesn't exist at C:\Users\Dell\AppData\Local\ms-playwright\chromium-1028\chrome-win\chrome.exe
╔════════════════════════════════════════════════════════════╗
║ Looks like Playwright was just installed or updated. ║
║ Please run the following command to download new browsers: ║
║ ║
║ pwsh bin/Debug/netX/playwright.ps1 install ║
║ ║
║ <3 Playwright Team ║
╚════════════════════════════════════════════════════════════╝
The issue is you are missing browser(s) Playwright wants to use. To fix this you can follow instructions from exception or if you have npm
installed you could use:
npx playwright install
Now everything should be installed and application should be ready for tests. Run it, head to Swagger
and send that Create
HTTP POST request. If you look closely in project root directory you will notice new folder named test
. It will contain hello.pdf
file.
Say no to dependencies
I could finish that post here, but I wouldn’t be myself if I would not pack this app into docker image. As we have seen Playwright requires couple steps to set it up. I believe it is better to handle it once and later use it in any environment without the need to pre-install any dependencies. Just docker run
and fun!
I always start with Visual Studio inbuilt Dockerfile. Right click on your project and then Add -> Docker Support
to create ready-to-use template. We only have to change one line there - replace base
ASP.NET runtime image with Playwright focal
image (as of the day of writing only focal image has .NET SDK preinstalled). In this project I’m using .NET 6 and already checked that Playwright v1.27.0 image have the same version pre-installed. You can check how Playwright team builds their image here.
#FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base #instead of this
FROM mcr.microsoft.com/playwright/dotnet:v1.27.0-focal AS base #paste this
The second thing is to add one more property into .csproj
file. You can add it in first PropertyGroup
in the file:
<PlaywrightPlatform>Linux</PlaywrightPlatform>
Now try running the app in Docker
mode and send that POST request again. It should go without errors and when you bash into the container you will find new pdf file in /app/pdf
directory.
#To bash into container first find it name with
docker ps
#And then exec into it
docker exec -it <container-name-or-id> /bin/bash
Wrap up
In this post I showed you how to easily create PDFs. HTML gives you enormous flexibility, you can use CSS or Javascript to modify how content looks like. At the end we packed our sample application into docker image, so you can use it anywhere with just one command.
In the next articles I am planning to show how HTML templates could be created with Razor Engine
(and I don’t mean Razor Pages
you could know from MVC
) and how document creation can be combined with messaging system of choice. Playwright basically launches new Chromium tab for each PDF so creating new documents with API requests might not be the best idea. Most of you probably joked about Chrome memory usage more than once.
You can find complete source code in the repository. If you found that post useful please share it and if you have any question feel free to reach me out. Till the next one!