DEV Community

Olufemi Oyedepo
Olufemi Oyedepo

Posted on

PDF Generation using QuestPDF in ASP.NET Core — Part 1

Introduction
Report generation in an enterprise setting provides insights into key performance indicators, tracks progress toward goals, and helps stakeholders make informed decisions. The most common report formats available are in PDF, MS-Word, PPTX, Excel, etc.

Of course, there are a bunch of PDF generation tools/libraries for the .NET platform out there, each with its pros & cons (the most notable for me has always been the licensing cost, as they are usually on the high side).

By the end of this article, you should be able to generate a dummy investment advice PDF document shown in the image below:

PDF Report sample questpdf

What is QuestPDF?
QuestPDF is an open-source .NET library for PDF document generation. It uses a fluent API approach to compose together many simple elements to create complex documents.

Pros of QuestPDF:

  • Fluent API
  • Very easy to use: Decent knowledge of C# is all that’s required.
  • Intuive documentation

Cons of QuestPDF:

  • Getting used to the fluent API specifications & helper methods.

Let’s get started
1. Create an asp.net core web API project and install the QuestPDF NuGet package.



Install-Package QuestPDF


Enter fullscreen mode Exit fullscreen mode

2. Create a folder in the API project and name it “Assets”, create a JSON file inside the Assets folder, and call it “customer-investment-data.json”. Paste the following snippet into the JSON file.



{
  "Customer": {
    "FullName": "John Doe",
    "Address": "23 Aaron Hawkins Avenue, Charleston, Texas"
  },
  "DealInfo": {
    "Type": "Fixed Deposit",
    "Issuer": "Morgan Stanley",
    "Currency": "USD",
    "ReferenceNumber": "REF-1234567890",
    "TransactionDate": "Thursday, 2 April 2024",
    "StartDate": "Thursday, 2 May 2024",
    "EndDate": "Monday, 27 January 2025",
    "Duration": "270",
    "NetRate": "17.88",
    "Principal": 450000,
    "ExpectedInterest": 70000,
    "NetMaturityValue": 850000
  },
  "CompanyLogoUrl": "idBGtJQnXa/idINtDZZob.jpeg"
}


Enter fullscreen mode Exit fullscreen mode

3. Create model classes to deserialize the content of the JSON file: Create a folder and call it “Models” inside the API project, and create 4 model classes with the properties as stated below:



public class Customer
{
    public string FullName { get; set; }
    public string Address { get; set; }
}


Enter fullscreen mode Exit fullscreen mode


public class DealInfo
{
    public string Type { get; set; }
    public string Issuer { get; set; }
    public string Currency { get; set; }
    public string ReferenceNumber { get; set; }
    public string TransactionDate { get; set; }
    public string StartDate { get; set; }
    public string EndDate { get; set; }
    public string Duration { get; set; }
    public string NetRate { get; set; }
    public int Principal { get; set; }
    public int ExpectedInterest { get; set; }
    public int NetMaturityValue { get; set; }
}


Enter fullscreen mode Exit fullscreen mode


public class PdfReportFileInfo
{
    public byte[] ByteArray { get; set; }
    public string MimeType { get; set; }
    public string FileName { get; set; }
}


Enter fullscreen mode Exit fullscreen mode


public class InvestmentTradeInfo
{
    public Customer Customer { get; set; }
    public DealInfo DealInfo { get; set; }
    public string CompanyLogoUrl { get; set; }
}


Enter fullscreen mode Exit fullscreen mode

4. Create a service class (Services/QuestPdfService.cs) to handle the PDF generation business logic with the content below:



using Microsoft.AspNetCore.Mvc;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF_Demo.Models;
using System.Net;
using System.Text.Json;

public static class QuestPdfService
{
    public static async Task<PdfReportFileInfo> GenerateSample1Pdf()
    {
        // this is just a proof of concept, please feel free to refactor / make the code asynchronous if db/http calls are involved

        try
        {
            // read investment advice data from json file & deserialize into a C# object
            string customerInvestmentData = File.ReadAllText(@"./Assets/customer-investment-data.json");
            var investmentTradeInfo = JsonSerializer.Deserialize<InvestmentTradeInfo>(customerInvestmentData);
            var imageStream = await GetCompanyLogoUrlImage2(investmentTradeInfo.CompanyLogoUrl);

            Document document = Document.Create(container =>
            {
                container.Page(page =>
                {
                    page.Size(PageSizes.A4);
                    page.Margin(2, Unit.Centimetre);
                    page.PageColor(Colors.White);
                    page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Arial", "Calibri", "Tahoma"));
                    page.Header().Width(1, Unit.Inch).Image(imageStream);

                    page.Content().Column(x =>
                    {
                        x.Item().PaddingVertical((float)0.5, Unit.Centimetre).Text(investmentTradeInfo.DealInfo.TransactionDate);
                        x.Item().Text(investmentTradeInfo.Customer.FullName).FontSize(15).Bold();
                        x.Item().PaddingBottom(1, Unit.Centimetre).Text(investmentTradeInfo.Customer.Address).FontSize(13);

                        x.Item().PaddingBottom((float)0.3, Unit.Centimetre).Text("Dear Sir/Ma,");

                        x.Item().AlignCenter()
                            .Text($"INVESTMENT ADVICE FOR {investmentTradeInfo.DealInfo.Type} - {investmentTradeInfo.DealInfo.Issuer} - REF NO: {investmentTradeInfo.DealInfo.ReferenceNumber}".ToUpper())
                            .FontSize(13)
                            .SemiBold()
                            .Underline();

                        x.Item().PaddingTop((float)0.5, Unit.Centimetre).Text("Please refer to the details of the investment below: ");

                        x.Item().PaddingTop((float)0.5, Unit.Centimetre).Row(row =>
                        {
                            row.RelativeItem().Text("Issuer: ").SemiBold();
                            row.RelativeItem().AlignRight().Text($"{investmentTradeInfo.DealInfo.Issuer}".ToUpper());
                        });

                        x.Item().PaddingTop((float)0.3, Unit.Centimetre).Row(row =>
                        {
                            row.RelativeItem().Text("Investment Type: ").SemiBold();
                            row.RelativeItem().AlignRight().Text($"{investmentTradeInfo.DealInfo.Type}".ToUpper());
                        });

                        x.Item().PaddingTop((float)0.3, Unit.Centimetre).Row(row =>
                        {
                            row.RelativeItem().Text("Currency: ").SemiBold();
                            row.RelativeItem().AlignRight().Text($"{investmentTradeInfo.DealInfo.Currency}".ToUpper());
                        });

                        x.Item().PaddingTop((float)0.3, Unit.Centimetre).Row(row =>
                        {
                            row.RelativeItem().Text("Start Date: ").SemiBold();
                            row.RelativeItem().AlignRight().Text(investmentTradeInfo.DealInfo.StartDate);
                        });

                        x.Item().PaddingTop((float)0.3, Unit.Centimetre).Row(row =>
                        {
                            row.RelativeItem().Text("End Date: ").SemiBold();
                            row.RelativeItem().AlignRight().Text(investmentTradeInfo.DealInfo.EndDate);
                        });

                        x.Item().PaddingTop((float)0.3, Unit.Centimetre).Row(row =>
                        {
                            row.RelativeItem().Text("Duration (days): ").SemiBold();
                            row.RelativeItem().AlignRight().Text(investmentTradeInfo.DealInfo.Duration);
                        });

                        x.Item().PaddingTop((float)0.3, Unit.Centimetre).Row(row =>
                        {
                            row.RelativeItem().Text("Net Rate (% per annum): ").SemiBold();
                            row.RelativeItem().AlignRight().Text(investmentTradeInfo.DealInfo.NetRate);
                        });

                        x.Item().PaddingTop((float)0.3, Unit.Centimetre).Row(row =>
                        {
                            row.RelativeItem().Text("Principal: ").SemiBold();
                            row.RelativeItem().AlignRight().Text($"${investmentTradeInfo.DealInfo.Principal:N2}");
                        });

                        x.Item().PaddingTop((float)0.3, Unit.Centimetre).Row(row =>
                        {
                            row.RelativeItem().Text("Expected Interest: ").SemiBold();
                            row.RelativeItem().AlignRight().Text($"${investmentTradeInfo.DealInfo.ExpectedInterest:N2}");
                        });

                        x.Item().PaddingTop((float)0.3, Unit.Centimetre).Row(row =>
                        {
                            row.RelativeItem().Text("Net Maturity Value: ").SemiBold();
                            row.RelativeItem().AlignRight().Text($"${investmentTradeInfo.DealInfo.NetMaturityValue:N2}");
                        });


                        x.Item().PaddingTop((float)0.8, Unit.Centimetre).Text("Terms & conditions: ").SemiBold();

                        x.Item().PaddingTop((float)0.3, Unit.Centimetre).Row(row =>
                        {
                            row.Spacing(5);
                            row.AutoItem().PaddingLeft(10).Text("•");
                            row.RelativeItem().Text("This investment is offered under the Jane Doe Investment Securities Management Service.");
                        });

                        x.Item().PaddingTop((float)0.5, Unit.Centimetre).Text("We thank you for your valued patronage and continued interest in Jane Doe Investment Securities Limited.");


                        x.Item().PaddingTop(1, Unit.Centimetre).Text("Warm regards,");
                        x.Item().PaddingTop((float)0.3, Unit.Centimetre).Row(row =>
                        {
                            row.Spacing(5);
                            row.AutoItem().Text("For: ").NormalWeight();
                            row.RelativeItem().Text("Jane Doe Investment Securities Limited");
                        });

                    });



                    page.Footer()
                        .AlignCenter()
                        .Text(x =>
                        {
                            x.Span("THIS IS A SYSTEM GENERATED MAIL, PLEASE DO NOT REPLY TO THIS EMAIL.").FontSize(9);
                        });
                });
            });
            byte[] pdfBytes = document.GeneratePdf();

            return new PdfReportFileInfo() { 
                ByteArray = pdfBytes, 
                FileName = $"Investment_Advice_{investmentTradeInfo.Customer.FullName}_{investmentTradeInfo.DealInfo.ReferenceNumber}.pdf",
                MimeType = "application/pdf"
            };
        }
        catch (Exception ex)
        {
            throw ex;
        }

    }

    private static async Task<Image> GetCompanyLogoUrlImage2(string imagePath)
    {
        using var client = new HttpClient();
        client.BaseAddress = new Uri("https://asset.brandfetch.io/");
        client.DefaultRequestHeaders.Accept.Clear();
        var imageStream = await client.GetStreamAsync(imagePath);
        return Image.FromStream(imageStream);
    }
}


Enter fullscreen mode Exit fullscreen mode

5. Set the license type: Insert the snippet below into the Progam.cs file. You may want to read more about licensing here



QuestPDF.Settings.License = LicenseType.Community;


Enter fullscreen mode Exit fullscreen mode

6. Create a controller, name it QuestPdfController, and add the following content:



[Route("api/[controller]")]
[ApiController]
public class QuestPdfController : ControllerBase
{
    [HttpGet]
    [Route("sample1")]
    public async Task<ActionResult> GenerateSample1Pdf()
    {
        var pdfReportInfo = await QuestPdfService.GenerateSample1Pdf();
        return File(pdfReportInfo.ByteArray, pdfReportInfo.MimeType, pdfReportInfo.FileName);
    }


Enter fullscreen mode Exit fullscreen mode

Now that all is set, your folder structure should look like this:
QuestPdf sample report 1

7. Run the web API project and you should see the image below, call the /sample1 endpoint by clicking on the “Execute” button, that action should return a file that can be downloaded. Click on “Download file” as indicated below and you should have the dummy investment advice report in PDF.

QuestPDF-Sample-report-api-endpoint

And that’s it for the first part of this series, kindly drop me a follow so as not to miss subsequent posts.
The source code for this example can be found here

.netcore #pdf #report #questpdf #ironpdf #csharp

Top comments (0)