Generating PDF: .Net Core and Azure Web Application
Using NReco library to generate PDF files on Azure Web App running .Net Core.
Generating a PDF is one of those features that come along in a while and gets me thinking.
How do I do this now?
Previously I had written about dynamically generating a large PDF from website contents . The PDF library I used that then, did have the limitation of not being able to run on Azure Web App. It was because of Azure sandbox restrictions .
In this post we will look at how we can generate PDF in an Azure Web App and running .Net Core, what the limitations are and some tips and tricks to help with the development. I am using the NReco HTML-to-PDF Generator for .Net , which is a C# wrapper over WkHtmlToPdf .
To use NReco HTML-To-PDF Generator with .Net Core, you need a license .
Generating the PDF
Generate HTML
To generate the PDF, we first need to generate HTML. RazorLight is a template engine based on Razor for .Net Core. It is available as a NuGet package . I am using the latest available pre-release version - 2.0.0-beta4 . Razor light supports templates from Files / EmbeddedResources / Strings / Database or Custom Source. The source is configured when setting the RazorLightEngine to be used in the application. For .Net Core, we can inject an instance of IRazorLightEngine for use in the application. The ContentRootPath is from the IWebHostEnvironment that can be injected to the Startup class
var engine = new RazorLightEngineBuilder().UseFileSystemProject($"{ContentRootPath}/PdfTemplates").UseMemoryCachingProvider().Build();services.AddSingleton<IRazorLightEngine>(engine);
An instance of the engine is used to generate HTML from a razor view. By using UseFileSystemProject function above, RazorLight picks up the templates from the provided file path. I have all the template files under a folder 'PdfTemplates'. Make sure to set the Template files ( *.cshtml ), and any associated resource files (CSS and images) to ' Copy to Output Directory '. RazorLight adds the templates in the path specified and makes them available against a template key. The template key format is different based on the source . e.g., When using filesystem template key is the relative path to the template file from the RootPath
The HtmlGenerationService below takes in a data object and generates the HTML string using the RazorLightEngine. By convention, it expects a template file (*.cshtml) within a folder. E.g., For data type 'Quote', it expects a template with key 'Quote/Quote.cshtml'.
public class HtmlGenerationService : IHtmlGenerationService{private readonly IRazorLightEngine _razorLightEngine;public HtmlGenerationService(IRazorLightEngine razorLightEngine){_razorLightEngine = razorLightEngine;}public async Task<string> Generate<T>(T data){var template = typeof(T).Name;return await _razorLightEngine.CompileRenderAsync($"{template}/{template}.cshtml", data);}}
I got the following error - InvalidOperationException: Cannot find reference assembly 'Microsoft.AspNetCore.Antiforgery.dll' file for package Microsoft.AspNetCore.Antiforgery and had to set PreserveCompilationReferences and PreserveCompilationContext in the csproj as mentioned here . Make sure to check the FAQ's if you are facing any error using the library.
Generate PDF
With the HTML generated, we can use the HtmlToPdfConverter, the NReco wrapper class, to convert it to PDF format. The library is free for .Net but needs a paid license for .Net Core. It is available as a NuGet package and does work fine with .Net Core 3.1 as well.
The wkhtmltopdf binaries must be deployed for your target platform(s) (Windows, Linux, or OS X) with your .NET Core app.
With .Net Core, the wkhtmltopdf executable does not get bundled as part of the NuGet package. It is because the executable differs based on the hosting OS environment. Make sure to include the executable and set to be copied to the bin folder. By default, the converter looks for the executable ( wkhtmltopdf.exe ) under the folder wkhtmltopdf . The path is configurable.
public class PdfGeneratorService : IPdfGeneratorService{...public PdfGeneratorService(IHtmlGenerationService htmlGenerationService, NRecoConfig config) {...}public async Task<byte[]> Generate<T>(T data){var htmlContent = await HtmlGenerationService.Generate(data);return ToPdf(htmlContent);}private byte[] ToPdf (string htmlContent){var htmlToPdf = new HtmlToPdfConverter();htmlToPdf.License.SetLicenseKey(_config.UserName,_config.License);if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)){htmlToPdf.WkHtmlToPdfExeName = "wkhtmltopdf";}return htmlToPdf.GeneratePdf(htmlContent);}}
Calling the GeneratePdf function with the HTML string returns the Pdf byte array. The Pdf byte array can be returned as a File or saved for later reference.
public async Task<IActionResult> Get(string id){...var result = await PdfGenerationService.Generate(model);return File(result, "application/pdf", $"Quote - {model.Number}.pdf");}
Limitations
Before using any PDF generation library, make sure you read the associated docs and FAQ's as most of them have one limitation or the other. It's about finding the library that fits the purpose and budget.
Must run on a dedicated VM backed plan : NReco does work fine in Azure Web App as long as it in on a dedicated VM-based plan (Basic, Standard, Premium). If you are running on a Free or Shared plan, NReco will not work.
Custom fonts are not supported : On Azure Web App, there is a limitation on the font's . Custom fonts are ignored, and system-installed fonts are used.
Not all Browser features available : wkhtmltopdf uses Qt WebKit rendering engine to render the HTML into PDF. You will need to play around and see what works and what doesn't. I have seen this mostly affecting with CSS (as Flexbox and CSS Grid support was unavailalbe in the version I was using).
Development Tips & Tricks
Here are a few things that helped speed up the development of the Razor file.
Render Razor View While Development
Once I had the PDF generation pipeline set up, the challenge was to get the formatting with real-time feedback. I didn't want to download the PDF and verify every time I made a change.
To see the output of the razor template as and when you make changes, return the HTML content as ContentResult back on the API endpoint. When calling this from a browser, it will automatically render it.
[HttpGet][Route("{id}")]public async Task<IActionResult> Get(string id, [FromQuery]bool? html){ ...if (html.GetValueOrDefault()){var htmlResult = await HtmlGenerationService.Generate(model);return new ContentResult() {Content = htmlResult,ContentType = "text/html",StatusCode = 200 };}