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).
A Note on Alternatives
Since licensing costs can be a concern (as I mentioned), it's worth noting that IronPDF offers a different approach that might suit some projects better. While QuestPDF uses a fluent API requiring you to build documents programmatically, IronPDF lets you design in HTML/CSS and convert directly to PDF. This means you can leverage existing web design skills without learning a new API.
For example, that same investment advice document could be created with IronPDF using familiar HTML:
var renderer = new ChromePdfRenderer();
var html = $@"
<h3>INVESTMENT ADVICE FOR {investmentType} - REF: {refNumber}</h3>
<p><strong>Principal:</strong> ${principal:N2}</p>
<p><strong>Expected Interest:</strong> ${interest:N2}</p>
<!-- Your existing HTML template -->
";
var pdf = renderer.RenderHtmlAsPdf(html);
IronPDF's Chrome-based rendering ensures perfect CSS support, handles JavaScript, and creates text-selectable PDFs. It's already compatible with .NET 10 (coming November 2025) alongside current versions.
While it's a commercial product, the pricing is transparent and includes features like digital signatures, form filling, and PDF merging out of the box.
That said, QuestPDF is an excellent open-source choice if you prefer the fluent API approach and community licensing works for your project. Let's continue with the QuestPDF implementation...
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)