In this article, I will show you an easy way to create a PDF document and style it properly with Razor engine and the DinkToPdf library.
The source code of this project can be found on GitHub.
Setting up the project
For simplicity, we will create a default Web API project with default options that will have one endpoint that will be responsible for creating a PDF document and returning the created document to the user.
To start, let’s delete the existing controller and create our own controller that will be responsible for generating the PDF document. Let’s call it PDFController with one Action called CreatePdf. We will later modify this controller so it returns the PDF document.
namespace PDFSampleProject.API.Controllers { [Route("api/[controller]")] [ApiController] public class PDFController : ControllerBase { [HttpGet("Create")] public async Task<IActionResult> CreatePdf() { return Ok("Test"); } } }
Now let’s add the library that we will need for creating the PDF and manipulation of data.
Configuration of project
DinkToPDF
Add the DinkToPdf library. DinkToPDF is an awesome library that is a .NET wrapper around the popular wkhtmltopdf library and we will use it to convert an HTML template to a PDF document. Things that make this library great is that it can use styles as well, so we can have an HTML template and convert it to PDFDocument.
DinkToPDF on GitHub.
Let’s add this library through Package Manager Console ( we could easily add it through the NuGet package window, but I prefer PMC 😊 )
In order for DinkToPDF to work correctly, we will need to add some native PDF dlls libwkhtmltox and load them at start-up of the application. Those can be found on the source code of this project.
Razor Light
We will need to add Razor Light. Razor is a powerful library for templating HTML documents and has lots of features for rendering HTML content. Instead of pulling a big razor library we will use the light version of the Razor library since we will need only the rendering feature to convert that generated HTML content to a PDF document.
Data preparation
For simplicity, we will add simple DTO, let’s name it CardDTO with properties.
using System; namespace PDFSampleProject.API.Data { public class CarDTO { public string NameOfCar { get; set; } public int NumberOfDoors { get; set; } public DateTime FirstRegistration { get; set; } public double MaxSpeed { get; set; } } }
Next, let us add the static class that will mock our repository where will we pull entities and map them to DTOs to serve us data. We will call it CarRepository.cs
using System; using System.Collections.Generic; namespace PDFSampleProject.API.Data { public static class CarRepository { public static IList<CarDTO> GetCars() { return new List<CarDTO>() { new CarDTO{NameOfCar="Audi Q7",FirstRegistration = DateTime.UtcNow.AddYears(-3),MaxSpeed = 200,NumberOfDoors = 4}, new CarDTO{NameOfCar="Audi A5",FirstRegistration = DateTime.UtcNow,MaxSpeed = 180,NumberOfDoors = 4}, new CarDTO{NameOfCar="Audi Q3",FirstRegistration = DateTime.UtcNow,MaxSpeed = 245,NumberOfDoors = 2}, new CarDTO{NameOfCar="Mercedes SLI",FirstRegistration = DateTime.UtcNow,MaxSpeed = 150,NumberOfDoors = 4}, new CarDTO{NameOfCar="Chevrolet",FirstRegistration = DateTime.UtcNow,MaxSpeed = 220,NumberOfDoors = 4}, new CarDTO{NameOfCar="BMW",FirstRegistration = DateTime.UtcNow,MaxSpeed = 200,NumberOfDoors = 4}, }; } } }
Razor HTML template
If you work with Razor you will know how powerful the Razor engine is, as it lets us write C# in HTML and performs server-side rendering of HTML. We will create a PDFTemplate.cshtml file under the Template folder. Our template will have a ViewModel CarDTO and it will have a simple way of displaying data.
@model List<PDFSampleProject.API.Data.CarDTO> <html> <head> <style> body{ font-family: "Arial"; font-size: 14px;} .car-name { color: darkgray; } .car-max-speed { color: tomato; } .car-registration { color: brown; } </style> </head> <body> @foreach (var car in Model) { <div> <span>Car name:</span> <span class="car-name">@car.NameOfCar</span> </div> <div> <span>Cars first registration:</span> <span class="car-registration">@car.FirstRegistration.ToShortDateString()</span> </div> <div> <span>Cars max speed:</span> <span class="car-max-speed">@car.MaxSpeed</span> </div> <div> <span> Number of doors:</span> <span> @car.NumberOfDoors</span> </div> <br /> } </body> </html>
With this, our configuration and preparation are done. Now we will wire everything up.
Wiring everything up
To wire everything up we will need to do a few things.
To load native PDF dlls we will need the CustomAssemblyLoadContext class that will be used to load those dlls (note: depending on the process you will require the 64bit or 32bit dlls)
using System; using System.Reflection; using System.Runtime.Loader; namespace PDFSampleProject.API.Abstraction { public class CustomAssemblyLoadContext : AssemblyLoadContext { public IntPtr LoadUnmanagedLibrary(string absolutePath) { return LoadUnmanagedDll(absolutePath); } protected override IntPtr LoadUnmanagedDll(String unmanagedDllName) { return LoadUnmanagedDllFromPath(unmanagedDllName); } protected override Assembly Load(AssemblyName assemblyName) { throw new NotImplementedException(); } } }
And then in our Startup.cs in the method ConfigureServices add the following code for loading assemblies into memory
.... var processSufix = "32bit"; if (Environment.Is64BitProcess && IntPtr.Size == 8) { processSufix = "64bit"; } var context = new CustomAssemblyLoadContext(); context.LoadUnmanagedLibrary(Path.Combine(Directory.GetCurrentDirectory(), $"PDFNative\\{processSufix}\\libwkhtmltox.dll")); ....
After this let’s add the Razor service
... services.AddScoped<IRazorLightEngine>(sp => { var engine = new RazorLightEngineBuilder() .UseFilesystemProject(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)) .UseMemoryCachingProvider() .Build(); return engine; }); ...
Additionally, let’s add a service that will be responsible for creating the PDF. Let’s call it IPDFService and it will have a single method Create
using System.Threading.Tasks; namespace PDFSampleProject.API.Abstraction { internal interface IPDFService { Task<byte[]> Create(); } }
Now comes the fun part, creating the concrete implementation that will be used for heavy lifting with a proper injection of services that we prepared before.
using System.IO; using System.Reflection; using System.Threading.Tasks; using DinkToPdf; using DinkToPdf.Contracts; using RazorLight; namespace PDFSampleProject.API.Abstraction { public class PDFService : IPDFService { private readonly IRazorLightEngine _razorEngine; private readonly IConverter _pdfConverter; public PDFService(IRazorLightEngine razorEngine,IConverter pdfConverter) { _razorEngine = razorEngine; _pdfConverter = pdfConverter; } public async Task<byte[]> Create() { var model = Data.CarRepository.GetCars(); var templatePath = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"Templates/PDFTemplate.cshtml"); string template = await _razorEngine.CompileRenderAsync(templatePath,model); var globalSettings = new GlobalSettings { ColorMode = ColorMode.Color, Orientation = Orientation.Portrait, PaperSize = PaperKind.A4, Margins = new MarginSettings() { Top = 10, Bottom = 10, Left = 10, Right = 10 }, DocumentTitle = "Simple PDF document", }; var objectSettings = new ObjectSettings { PagesCount = true, HtmlContent = template, WebSettings = { DefaultEncoding = "utf-8" }, HeaderSettings = { FontName = "Arial", FontSize = 12,Line = true, Center = "Fun pdf document"}, FooterSettings = { FontName = "Arial", FontSize = 12, Line = true, Right = "Page [page] of [toPage]" } }; var pdf = new HtmlToPdfDocument() { GlobalSettings = globalSettings, Objects = { objectSettings } }; byte[] file = _pdfConverter.Convert(pdf); return file; } } }
As we can see the Create() method is where everything is happening. Let’s explain it a bit, we are getting data for the model from our mock of the repository class. This model will be used in our template for rendering using the Razor engine. Next, we are getting the template from our templates folder. Keep in mind we can have multiple templates and depending on the context we can load them independently. After we load the template we will pass the template with a model to razor engine. And then we get the raw HTML content that we will pass to the DinkToPDF library to convert it to an array of bytes or PDF document. The one thing left to do is to wire this PDF service to our controller.
We will need to add this service to our Startup class so we have access to it across the application
... services.AddScoped<IPDFService, PDFService>(); ...
So now in our PDFController, we will have something like this.
namespace PDFSampleProject.API.Controllers { [Route("api/[controller]")] [ApiController] public class PDFController : ControllerBase { private readonly IPDFService _pdfService; public PDFController(IPDFService pdfService) { _pdfService = pdfService; } [HttpGet("Create")] public async Task<IActionResult> CreatePdf() { var file = await _pdfService.Create(); return File(file, "application/pdf"); } } }
Now it is time to test it. If we start the application and go to the URL that is on the image, we should get a PDF document.
Wrapping up
Here I showed you a simple but yet powerful way of creating a dynamic PDF document. This solution can be used in many scenarios with a little adjustment. But with this simple example, you can see how Razor can be used as a templating engine. Happy coding! 🙂