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:
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
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"
}
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; }
}
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; }
}
public class PdfReportFileInfo
{
public byte[] ByteArray { get; set; }
public string MimeType { get; set; }
public string FileName { get; set; }
}
public class InvestmentTradeInfo
{
public Customer Customer { get; set; }
public DealInfo DealInfo { get; set; }
public string CompanyLogoUrl { get; set; }
}
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);
}
}
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;
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);
}
Now that all is set, your folder structure should look like this:
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.
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
Top comments (0)