Creating PDF Documents with Razor in ASP.NET Core 2.2 Web API


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! 🙂
 

Related Blog Posts

We hope you’ve found this to be helpful and are walking away with some new, useful insights. If you want to learn more, here are a couple of related articles that others also usually find to be interesting:

Our Gear Is Packed and We're Excited to Explore With You

Ready to come with us? 

Together, we can map your company’s software journey and start down the right trails. If you’re set to take the first step, simply fill out our contact form. We’ll be in touch quickly – and you’ll have a partner who is ready to help your company take the next step on its software journey. 

We can’t wait to hear from you! 

Main Contact

This field is for validation purposes and should be left unchanged.

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the form below. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Montage Portal

Montage Furniture Services provides furniture protection plans and claims processing services to a wide selection of furniture retailers and consumers.

Project Background

Montage was looking to build a new web portal for both Retailers and Consumers, which would integrate with Dynamics CRM and other legacy systems. The portal needed to be multi tenant and support branding and configuration for different Retailers. Trailhead architected the new Montage Platform, including the Portal and all of it’s back end integrations, did the UI/UX and then delivered the new system, along with enhancements to DevOps and processes.

Logistics

We’ve logged countless miles exploring the tech world. In doing so, we gained the experience that enables us to deliver your unique software and systems architecture needs. Our team of seasoned tech vets can provide you with:

Custom App and Software Development

We collaborate with you throughout the entire process because your customized tech should fit your needs, not just those of other clients.

Cloud and Mobile Applications

The modern world demands versatile technology, and this is exactly what your mobile and cloud-based apps will give you.

User Experience and Interface (UX/UI) Design

We want your end users to have optimal experiences with tech that is highly intuitive and responsive.

DevOps

This combination of Agile software development and IT operations provides you with high-quality software at reduced cost, time, and risk.

Trailhead stepped into a challenging project – building our new web architecture and redeveloping our portals at the same time the business was migrating from a legacy system to our new CRM solution. They were able to not only significantly improve our web development architecture but our development and deployment processes as well as the functionality and performance of our portals. The feedback from customers has been overwhelmingly positive. Trailhead has proven themselves to be a valuable partner.

– BOB DOERKSEN, Vice President of Technology Services
at Montage Furniture Services

Technologies Used

When you hit the trails, it is essential to bring appropriate gear. The same holds true for your digital technology needs. That’s why Trailhead builds custom solutions on trusted platforms like .NET, Angular, React, and Xamarin.

Expertise

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

  • Project Management
  • Architecture
  • Web App Development
  • Cloud Development
  • DevOps
  • Process Improvements
  • Legacy System Integration
  • UI Design
  • Manual QA
  • Back end/API/Database development

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

Our Gear Is Packed and We're Excited to Explore with You

Ready to come with us? 

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the contact form. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Thank you for reaching out.

You’ll be getting an email from our team shortly. If you need immediate assistance, please call (616) 371-1037.