# Generate Reports in C# Like Crystal Reports (.NET 10)
**HTML-to-PDF report generation** in **C# .NET** with IronPDF replaces Crystal Reports' proprietary `.rpt` designer with standard **HTML, CSS, and Razor templates**, enabling .NET developers to build data-driven business reports using the web development skills they already have. That includes full support for dynamic tables, **JavaScript-powered charts**, conditional formatting, **multi-document batch processing**, and cross-platform deployment across any environment that runs .NET.
*as-heading:2(TL;DR: Quickstart Guide)*
This tutorial covers replacing Crystal Reports with *HTML-to-PDF* report generation in C# .NET, from basic templates to batch processing and scheduled generation.
- **Who this is for:** .NET developers replacing Crystal Reports or building new reporting systems from scratch.
- **What you'll build:** Three complete report implementations (sales invoice, employee directory, inventory report), plus Chart.js visualizations, branded headers/footers, table of contents generation, sub-report merging, and parallel batch processing.
- **Where it runs:** .NET 10, .NET 8 LTS, .NET Framework 4.6.2+, and .NET Standard 2.0. No Windows-only COM dependencies.
- **When to use this approach:** When Crystal Reports' lack of .NET Core support, Windows lock-in, or complex licensing becomes a bottleneck.
- **Why it matters technically:** HTML/CSS renders identically across platforms, integrates with CI/CD, and executes JavaScript for charts, all without a proprietary designer or per-document fees.
To follow along with the code examples, install `IronPdf` via NuGet (`Install-Package IronPdf`). Generate your first report with just a few lines of code:
```cs
:title=Convert an HTML report template to PDF in one line!
// Install-Package IronPdf
var pdf = new IronPdf.ChromePdfRenderer()
.RenderHtmlAsPdf("<h1>Sales Report</h1><table><tr><td>Q1</td><td>$50,000</td></tr></table>")
.SaveAs("sales-report.pdf");
```
After you've purchased or signed up for a 30-day trial of IronPDF, add your license key at the start of your application.
```cs
IronPdf.License.LicenseKey = "KEY";
```
!!!--LIBRARY_START_TRIAL_BLOCK--!!!
!!!--LIBRARY_NUGET_INSTALL_BLOCK--!!!
*as-heading:2(Table of Contents)*
- **TL;DR: Quickstart Guide**
- [Quick Overview](#anchor-tldr-quickstart-guide)
- **HTML Templates to PDF Architecture**
- [C# Report Generator: HTML Templates to PDF](#anchor-c-report-generator-html-templates-to-pdf)
- **Why Replace Crystal Reports in .NET Applications**
- [No .NET 8 or .NET Core Support](#anchor-no-net-8-or-net-core-support)
- [Complex Licensing and Hidden Costs](#anchor-complex-licensing-and-hidden-costs)
- [Windows-Only Platform Lock-In](#anchor-windows-only-platform-lock-in)
- **Set Up a C# Report Generator in .NET 10**
- [Choose a Template Approach: Razor, HTML, or Hybrid](#anchor-choose-a-template-approach-razor-html-or-hybrid)
- **Build a Data-Driven PDF Report in C#**
- [Create the HTML/CSS Report Template](#anchor-create-the-htmlcss-report-template)
- [Bind Data to the Template](#anchor-bind-data-to-the-template)
- [Add Headers, Footers, and Page Numbers](#anchor-add-headers-footers-and-page-numbers)
- [Create Dynamic Tables and Repeating Sections](#anchor-create-dynamic-tables-and-repeating-sections)
- **Advanced C# Report Generation With IronPDF**
- [Add Charts and Graphs to PDF Reports](#anchor-add-charts-and-graphs-to-pdf-reports)
- [Apply Conditional Formatting and Business Logic](#anchor-apply-conditional-formatting-and-business-logic)
- [Create Sub-Reports and Section Breaks](#anchor-create-sub-reports-and-section-breaks)
- [Generate a Table of Contents](#anchor-generate-a-table-of-contents)
- **Migrate From Crystal Reports to IronPDF**
- [Map Crystal Reports Concepts to IronPDF](#anchor-map-crystal-reports-concepts-to-ironpdf)
- [Best Strategy for Converting .rpt Templates](#anchor-best-strategy-for-converting-rpt-templates)
- **Batch Report Generation and Scheduling in .NET**
- [Generate Multiple Reports in Parallel](#anchor-generate-multiple-reports-in-parallel)
- [Integrate Report Generation With ASP.NET Core Background Services](#anchor-integrate-report-generation-with-aspnet-core-background-services)
- **Download the Complete Test Project**
- [Test Project Download](#anchor-download-the-complete-test-project)
<a id="anchor-c-report-generator-html-templates-to-pdf"></a>
## C# Report Generator: HTML Templates to PDF
HTML-to-PDF generation relies on a linear architectural pipeline. Instead of a proprietary file format, the application uses standard data models to populate Razor views or HTML templates. The resulting HTML string is then passed to a rendering engine like `IronPdf`, which captures the visual output as a PDF document. This approach decouples the report design from the hosting environment, allowing the exact same code to execute on any platform that supports .NET.
This workflow mirrors standard web development. Front-end developers create the layout using CSS and preview it immediately in any browser. Backend developers then bind the data using C#. This separation allows teams to use their existing version control, code review, and continuous deployment processes for reports just as they do for the rest of the application.
HTML enables features unavailable in Crystal Reports: interactive charts, responsive tables, and shared styles for consistent branding.
## Why Replace Crystal Reports in .NET Applications
The migration away from Crystal Reports is not the result of a single catastrophic issue or sudden abandonment by SAP. Instead, it is the accumulation of friction points that collectively make the platform increasingly difficult to justify for new projects and more challenging to maintain in existing solutions. Identifying these pain points clarifies why many teams are searching for alternatives and which criteria are most important when evaluating replacement options.
## No .NET 8 or .NET Core Support
Crystal Reports does not support .NET Core or .NET 5-10. SAP has stated on forums that they do not plan to add support. The SDK uses COM components, which are incompatible with cross-platform .NET. Support for modern .NET would require a complete rewrite, which SAP has declined to do.
As a result, teams building new applications on current .NET versions cannot use Crystal Reports. Organizations standardized on .NET 8 or .NET 10 are unable to integrate it. For existing applications, upgrading to a modern .NET runtime requires replacing the reporting system first.
## Complex Licensing and Hidden Costs
Crystal Reports licensing distinguishes between designer licenses, runtime licenses, server deployments, and embedded use. Rules vary for desktop, web, and terminal services. Compliance in one setup may need extra licenses in another. If gaps appear after deployment, unexpected costs arise. Many organizations decide that the uncertainty is worse than migrating to a solution with clearer licensing.
## Windows-Only Platform Lock-In
Crystal Reports only runs on Windows with the legacy .NET Framework. You cannot deploy these applications to Linux containers, Azure App Service on Linux, AWS Lambda, or Google Cloud Run. As organizations use containerized, platform-agnostic, and serverless systems, these restrictions become more important.
Development teams building microservices face extra challenges. If nine services run in lightweight Linux containers but one needs Windows for Crystal Reports, deployment is more complicated. You need Windows container images, Windows-compatible hosting, and separate deployment settings. The reporting service becomes an exception, blocking standardization.
## Set Up a C# Report Generator in .NET 10
Getting started with IronPDF is simple. Install the library through NuGet like any other .NET dependency. There is no extra software to download or a separate runtime installer for production servers.
### Choose a Template Approach: Razor, HTML, or Hybrid
IronPDF supports three distinct approaches to building report templates. Each approach offers specific advantages depending on team composition, project requirements, and long-term maintenance considerations.
**Razor Views** offer the richest development experience for teams already working in the .NET ecosystem. Strongly-typed models with full IntelliSense support in Visual Studio and VS Code, compile-time checking, and the full power of C# for loops, conditionals, null handling, and string formatting are available. Razor's syntax is familiar to those who have built ASP.NET Core applications, eliminating the learning curve associated with template engines from other ecosystems. The templates reside in the project alongside other source files, participate in refactoring operations, and compile as part of the normal build process.
**Plain HTML with String Interpolation** works well for simpler reports or teams that prefer keeping templates separate from .NET code entirely. HTML templates can be stored as embedded resources compiled into the assembly, external files deployed alongside the application, or even retrieved from a database or content management system at runtime. Basic data binding uses `string.Replace()` for single values or a lightweight templating library like Scriban or Fluid for more advanced scenarios. This approach maximizes portability, allowing designers to edit templates without any .NET tooling installed, using only a text editor and a web browser for preview.
**Hybrid Approaches** combine both techniques for scenarios requiring flexibility. For instance, a Razor view might be rendered to generate the main HTML structure, then post-processed with additional string replacements for dynamic elements that do not fit cleanly into the view model. Alternatively, an HTML template designed by a non-developer can be loaded, and Razor partial views can be used to render only the complex, data-driven sections before combining everything. The HTML-to-PDF conversion is agnostic to the HTML source, allowing you to mix approaches based on each report's needs.
Given these options, this tutorial focuses primarily on Razor views because they offer the best balance of type safety, maintainability, and feature richness for typical business reporting scenarios. The skills transfer directly if future requirements include working with plain HTML templates, as both methods produce HTML strings.
## Build a Data-Driven PDF Report in C#
This section demonstrates the complete creation of a sales invoice report, from start to finish. The example covers the essential pattern used for all reports: define a model that structures the data, create a Razor template that transforms data into formatted HTML, render that template to an HTML string, and convert the HTML to a PDF document ready for viewing, emailing, or archiving.
### Create the HTML/CSS Report Template
The first step is to define the data model. A real invoice requires customer information, line items with descriptions and pricing, calculated totals, tax handling, and company branding elements. Model classes should be structured to reflect these groupings:
```csharp
// Invoice data model with customer, company, and line item details
public class InvoiceModel
{
public string InvoiceNumber { get; set; } = string.Empty;
public DateTime InvoiceDate { get; set; }
public DateTime DueDate { get; set; }
public CompanyInfo Company { get; set; } = new();
public CustomerInfo Customer { get; set; } = new();
public List<LineItem> Items { get; set; } = new();
// Computed totals - business logic stays in the model
public decimal Subtotal => Items.Sum(x => x.Total);
public decimal TaxRate { get; set; } = 0.08m;
public decimal TaxAmount => Subtotal * TaxRate;
public decimal GrandTotal => Subtotal + TaxAmount;
}
// Company details for invoice header
public class CompanyInfo
{
public string Name { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string LogoPath { get; set; } = string.Empty;
}
// Customer billing information
public class CustomerInfo
{
public string Name { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
// Individual invoice line item
public class LineItem
{
public string Description { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Total => Quantity * UnitPrice;
}
```
The computed properties for `Subtotal`, `TaxAmount`, and `GrandTotal` are included in the model. These calculations belong in the model rather than the template, keeping Razor views focused on presentation while the model handles business logic. This separation makes unit testing straightforward, allowing calculations to be verified without rendering any HTML.
Now create the Razor view that transforms this model into a professionally formatted invoice. Save this as `InvoiceTemplate.cshtml` in your Views folder:
```html
@model InvoiceModel
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* Reset and base styles */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 12px; color: #333; line-height: 1.5; }
.invoice-container { max-width: 800px; margin: 0 auto; padding: 40px; }
/* Header with company info and invoice title */
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 40px; padding-bottom: 20px; border-bottom: 2px solid #3498db; }
.company-info h1 { font-size: 24px; color: #2c3e50; margin-bottom: 10px; }
.company-info p { color: #7f8c8d; font-size: 11px; }
.invoice-title { text-align: right; }
.invoice-title h2 { font-size: 32px; color: #3498db; margin-bottom: 10px; }
.invoice-title p { font-size: 12px; color: #7f8c8d; }
/* Address blocks */
.addresses { display: flex; justify-content: space-between; margin-bottom: 30px; }
.address-block { width: 45%; }
.address-block h3 { font-size: 11px; text-transform: uppercase; color: #95a5a6; margin-bottom: 8px; letter-spacing: 1px; }
.address-block p { font-size: 12px; }
/* Line items table */
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
.items-table th { background-color: #3498db; color: white; padding: 12px; text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
.items-table th:last-child, .items-table td:last-child { text-align: right; }
.items-table td { padding: 12px; border-bottom: 1px solid #ecf0f1; }
.items-table tr:nth-child(even) { background-color: #f9f9f9; }
/* Totals section */
.totals { float: right; width: 300px; }
.totals-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #ecf0f1; }
.totals-row.grand-total { border-bottom: none; border-top: 2px solid #3498db; font-size: 16px; font-weight: bold; color: #2c3e50; padding-top: 12px; }
/* Footer */
.footer { clear: both; margin-top: 60px; padding-top: 20px; border-top: 1px solid #ecf0f1; text-align: center; color: #95a5a6; font-size: 10px; }
</style>
</head>
<body>
<div class="invoice-container">
<!-- Company header and invoice details -->
<div class="header">
<div class="company-info">
<h1>@Model.Company.Name</h1>
<p>@Model.Company.Address</p>
<p>@Model.Company.City</p>
<p>@Model.Company.Phone | @Model.Company.Email</p>
</div>
<div class="invoice-title">
<h2>INVOICE</h2>
<p>Invoice #: @Model.InvoiceNumber</p>
<p>Date: @Model.InvoiceDate.ToString("MMMM dd, yyyy")</p>
<p>Due Date: @Model.DueDate.ToString("MMMM dd, yyyy")</p>
</div>
</div>
<!-- Bill to address -->
<div class="addresses">
<div class="address-block">
<h3>Bill To</h3>
<p>@Model.Customer.Name</p>
<p>@Model.Customer.Address</p>
<p>@Model.Customer.City</p>
<p>@Model.Customer.Email</p>
</div>
</div>
<!-- Line items table -->
<table class="items-table">
<thead>
<tr><th>Description</th><th>Quantity</th><th>Unit Price</th><th>Total</th></tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Description</td>
<td>@item.Quantity</td>
<td>@item.UnitPrice.ToString("C")</td>
<td>@item.Total.ToString("C")</td>
</tr>
}
</tbody>
</table>
<!-- Totals -->
<div class="totals">
<div class="totals-row"><span>Subtotal:</span><span>@Model.Subtotal.ToString("C")</span></div>
<div class="totals-row"><span>Tax (@(Model.TaxRate * 100)%):</span><span>@Model.TaxAmount.ToString("C")</span></div>
<div class="totals-row grand-total"><span>Total Due:</span><span>@Model.GrandTotal.ToString("C")</span></div>
</div>
<!-- Footer message -->
<div class="footer">
<p>Thank you for your business!</p>
<p>Payment is due within 30 days. Please include invoice number with your payment.</p>
</div>
</div>
</body>
</html>
```
The CSS embedded in this template handles all visual styling such as colors, fonts, spacing, and table formatting. IronPDF also supports modern CSS features like flexbox, grid layouts, and CSS variables. The rendered PDF matches Chrome's print preview exactly, which makes debugging straightforward: if something looks wrong in the PDF, open the HTML in a browser and use developer tools to inspect and adjust styles.
### Bind Data to the Template
With the model and template in place, rendering the PDF requires connecting them through IronPDF's `ChromePdfRenderer`. The key step is converting the Razor view to an HTML string, then passing that string to the renderer:
```csharp
using IronPdf;
// Service class for generating invoice PDFs from Razor views
public class InvoiceReportService
{
private readonly IRazorViewEngine _razorViewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
public InvoiceReportService(
IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_razorViewEngine = razorViewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
// Generate PDF from invoice model
public async Task<byte[]> GenerateInvoicePdfAsync(InvoiceModel invoice)
{
// Render Razor view to HTML string
string html = await RenderViewToStringAsync("InvoiceTemplate", invoice);
// Configure PDF renderer with margins and paper size
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.MarginTop = 10;
renderer.RenderingOptions.MarginBottom = 10;
renderer.RenderingOptions.MarginLeft = 10;
renderer.RenderingOptions.MarginRight = 10;
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.Letter;
// Convert HTML to PDF and return bytes
var pdfDocument = renderer.RenderHtmlAsPdf(html);
return pdfDocument.BinaryData;
}
// Helper method to render a Razor view to string
private async Task<string> RenderViewToStringAsync<TModel>(string viewName, TModel model)
{
var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
using var stringWriter = new StringWriter();
var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);
if (!viewResult.Success)
throw new InvalidOperationException($"View '{viewName}' not found.");
var viewDictionary = new ViewDataDictionary<TModel>(
new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = model };
var viewContext = new ViewContext(actionContext, viewResult.View, viewDictionary,
new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
stringWriter, new HtmlHelperOptions());
await viewResult.View.RenderAsync(viewContext);
return stringWriter.ToString();
}
}
```
For simpler scenarios, when you don't need the full ASP.NET Core MVC setup, like in a console app or background service, you can just use HTML strings with interpolation and `StringBuilder` for the dynamic parts.
#### Sample Output
<iframe loading="lazy" src="/static-assets/pdf/tutorials/crystal-reports-alternative/01-basic-invoice.pdf" width="100%" height="400px">
</iframe>
<a id="anchor-add-headers-footers-and-page-numbers"></a>
### Add Headers, Footers, and Page Numbers
Professional reports typically include consistent headers and footers across all pages, displaying company branding, document titles, generation dates, and page numbers. IronPDF provides two approaches for implementing these elements: text-based headers for simple content requiring minimal formatting, and HTML headers for full styling control with logos and custom layouts.
Text-based headers work well for basic information and render faster since they don't require additional HTML parsing:
```cs
using IronPdf;
using IronSoftware.Drawing;
// Configure text-based headers and footers
var renderer = new ChromePdfRenderer();
// Set starting page number
renderer.RenderingOptions.FirstPageNumber = 1;
// Add centered header with divider line
renderer.RenderingOptions.TextHeader = new TextHeaderFooter
{
CenterText = "CONFIDENTIAL - Internal Use Only",
DrawDividerLine = true,
Font = FontTypes.Arial,
FontSize = 10
};
// Add footer with date on left, page numbers on right
renderer.RenderingOptions.TextFooter = new TextHeaderFooter
{
LeftText = "{date} {time}",
RightText = "Page {page} of {total-pages}",
DrawDividerLine = true,
Font = FontTypes.Arial,
FontSize = 9
};
// Set margins to accommodate header/footer
renderer.RenderingOptions.MarginTop = 25;
renderer.RenderingOptions.MarginBottom = 20;
```
The merge fields available include `{page}` for the current page number, `{total-pages}` for the document's total page count, `{date}` and `{time}` for generation timestamps, `{url}` for the source URL if rendering from a web page, and `{html-title}` and `{pdf-title}` for document titles. These placeholders are automatically replaced during rendering.
For headers with logos, custom fonts, or complex multi-column layouts, use HTML headers that support full CSS styling:
```cs
using IronPdf;
using System;
var renderer = new ChromePdfRenderer();
// Configure HTML header with logo and custom layout
renderer.RenderingOptions.HtmlHeader = new HtmlHeaderFooter
{
MaxHeight = 30,
HtmlFragment = @"
<div style='display: flex; justify-content: space-between; align-items: center;
width: 100%; font-family: Arial; font-size: 10px; color: #666;'>
<img src='logo.png' style='height: 25px;'>
<span>Company Name Inc.</span>
<span>Invoice Report</span>
</div>",
BaseUrl = new Uri(@"C:\assets\images\").AbsoluteUri
};
// Configure HTML footer with page info and generation date
renderer.RenderingOptions.HtmlFooter = new HtmlHeaderFooter
{
MaxHeight = 20,
HtmlFragment = @"
<div style='text-align: center; font-size: 9px; color: #999;
border-top: 1px solid #ddd; padding-top: 5px;'>
Page {page} of {total-pages} | Generated on {date}
</div>",
DrawDividerLine = false
};
```
#### Sample Output
<iframe loading="lazy" src="/static-assets/pdf/tutorials/crystal-reports-alternative/03-text-headers-footers.pdf" width="100%" height="400px">
</iframe>
<a id="anchor-create-dynamic-tables-and-repeating-sections"></a>
### Create Dynamic Tables and Repeating Sections
Reports often need to display collections of data that span multiple pages. Razor's loop constructs handle this naturally by iterating over collections and generating table rows or card elements for each item.
Here's a complete Employee Directory example demonstrating grouped data presentation with departmental sections:
```csharp
// Employee directory data models
public class EmployeeDirectoryModel
{
public List<Department> Departments { get; set; } = new();
public DateTime GeneratedDate { get; set; } = DateTime.Now;
}
// Department grouping with manager info
public class Department
{
public string Name { get; set; } = string.Empty;
public string ManagerName { get; set; } = string.Empty;
public List<Employee> Employees { get; set; } = new();
}
// Individual employee details
public class Employee
{
public string Name { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public string PhotoUrl { get; set; } = string.Empty;
public DateTime HireDate { get; set; }
}
```
The CSS property `page-break-inside: avoid` on the department class tells the PDF renderer to keep department sections together on a single page when possible. If a department's content would cause a page break mid-section, the renderer moves the entire section to the next page instead. The selector `.department:not(:first-child)` with `page-break-before: always` forces each department after the first to start on a new page, creating clean section separation throughout the directory.
#### Sample Output
<iframe loading="lazy" src="/static-assets/pdf/tutorials/crystal-reports-alternative/02-employee-directory.pdf" width="100%" height="400px">
</iframe>
## Advanced C# Report Generation With IronPDF
Business reports frequently require capabilities beyond static tables and text. Charts visualize trends that would be tedious to comprehend in tabular form. Conditional formatting draws attention to items requiring action. Sub-reports combine data from multiple sources into cohesive documents. This section covers implementing each of these features using IronPDF's Chromium rendering engine.
<a id="anchor-add-charts-and-graphs-to-pdf-reports"></a>
### Add Charts and Graphs to PDF Reports
Because JavaScript executes during rendering, you can use any client-side charting library to generate visualizations directly in your reports. The chart gets rasterized as part of the page and appears in the final PDF exactly as it would on screen. Chart.js offers an excellent balance of simplicity, capability, and documentation for most reporting needs.
Include Chart.js from a CDN and configure your chart with data serialized from your C# model:
```html
@model SalesReportModel
<!-- Include Chart.js library from CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Canvas element for chart rendering -->
<canvas id="salesChart"></canvas>
<script>
// Initialize bar chart with data from C# model
const ctx = document.getElementById('salesChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
// Serialize model data to JavaScript arrays
labels: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthLabels)),
datasets: [{
label: 'Monthly Sales',
data: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthlySales)),
backgroundColor: 'rgba(52, 152, 219, 0.7)'
}]
}
});
</script>
```
When rendering pages that include JavaScript-generated content, configure the renderer to wait for scripts to complete execution before capturing the page:
```cs
using IronPdf;
string html = "<h1>Report</h1>";
// Configure renderer to wait for JavaScript execution
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.WaitFor.JavaScript(500); // Wait 500ms for JS to complete
var pdf = renderer.RenderHtmlAsPdf(html);
```
#### Sample Output
<iframe loading="lazy" src="/static-assets/pdf/tutorials/crystal-reports-alternative/03-sales-chart-report.pdf" width="100%" height="400px">
</iframe>
<a id="anchor-apply-conditional-formatting-and-business-logic"></a>
### Apply Conditional Formatting and Business Logic
Inventory reports benefit from visual indicators that immediately draw attention to items requiring action. Rather than forcing users to scan through hundreds of rows looking for problems, conditional formatting makes exceptions visually obvious. Use Razor's inline expressions to apply CSS classes based on data values:
```html
<!-- Iterate through inventory items sorted by quantity -->
@foreach (var item in Model.Items.OrderBy(x => x.Quantity))
{
// Apply CSS class based on stock level thresholds
var rowClass = item.Quantity <= Model.CriticalStockThreshold ? "stock-critical" :
item.Quantity <= Model.LowStockThreshold ? "stock-low" : "";
<tr class="@rowClass">
<td>@item.SKU</td>
<td>@item.ProductName</td>
<td class="text-right">
<!-- Badge styling for critical stock items -->
<span class="quantity-badge @(item.Quantity <= 5 ? "badge-critical" : "badge-ok")">
@item.Quantity
</span>
</td>
</tr>
}
```
#### Sample Output
<iframe loading="lazy" src="/static-assets/pdf/tutorials/crystal-reports-alternative/04-inventory-report.pdf" width="100%" height="400px">
</iframe>
<a id="anchor-create-sub-reports-and-section-breaks"></a>
### Create Sub-Reports and Section Breaks
For combining independently generated reports into one document, use IronPDF's merge functionality:
```csharp
using IronPdf;
// Combine multiple reports into a single PDF document
public byte[] GenerateCombinedReport(SalesReportModel sales, InventoryReportModel inventory)
{
var renderer = new ChromePdfRenderer();
// Render each report section separately
var salesPdf = renderer.RenderHtmlAsPdf(RenderSalesReport(sales));
var inventoryPdf = renderer.RenderHtmlAsPdf(RenderInventoryReport(inventory));
// Merge PDFs into one document
var combined = PdfDocument.Merge(salesPdf, inventoryPdf);
return combined.BinaryData;
}
```
#### Sample Output
<iframe loading="lazy" src="/static-assets/pdf/tutorials/crystal-reports-alternative/05-combined-report.pdf" width="100%" height="400px">
</iframe>
<a id="anchor-generate-a-table-of-contents"></a>
### Generate a Table of Contents
IronPDF can automatically generate a table of contents based on heading elements in your HTML:
```cs
using IronPdf;
// Generate PDF with automatic table of contents
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.TableOfContents = TableOfContentsTypes.WithPageNumbers;
var pdf = renderer.RenderHtmlFileAsPdf("report.html");
```
## Migrate From Crystal Reports to IronPDF
Migrating an established reporting system requires careful planning to minimize disruption while capturing the opportunity to modernize and simplify. You'll move faster by understanding how Crystal Reports concepts map to the HTML-based approach, rather than trying to replicate every feature literally or preserve every quirk of the original reports.
<a id="anchor-map-crystal-reports-concepts-to-ironpdf"></a>
### Map Crystal Reports Concepts to IronPDF
Understanding the conceptual mapping helps you systematically translate existing reports:
<div class="content-table dotnet-core-pdf-table">
<table>
<thead>
<tr>
<th>Crystal Reports</th>
<th>IronPDF Equivalent</th>
</tr>
</thead>
<tbody>
<tr>
<td>Report sections</td>
<td>HTML divs with CSS page-break properties</td>
</tr>
<tr>
<td>Parameter fields</td>
<td>Model properties passed to Razor views</td>
</tr>
<tr>
<td>Formula fields</td>
<td>C# computed properties in model classes</td>
</tr>
<tr>
<td>Running totals</td>
<td>LINQ aggregations</td>
</tr>
<tr>
<td>Sub-reports</td>
<td>Partial views or merged PDF documents</td>
</tr>
<tr>
<td>Grouping/sorting</td>
<td>LINQ operations before passing data to template</td>
</tr>
<tr>
<td>Cross-tab reports</td>
<td>HTML tables using nested loops</td>
</tr>
<tr>
<td>Conditional formatting</td>
<td>Razor @if blocks with CSS classes</td>
</tr>
</tbody>
</table>
</div>
<a id="anchor-best-strategy-for-converting-rpt-templates"></a>
### Best Strategy for Converting .rpt Templates
Do not attempt to parse .rpt files programmatically. Instead, treat the existing PDF outputs as visual specifications and rebuild the logic using a systematic four-step strategy:
1. **Inventory:** Catalog all .rpt files with their purpose, data sources, and usage frequency. Remove obsolete reports to reduce migration scope.
2. **Prioritize:** Migrate high-frequency reports first. Target reports with simple layouts or persistent maintenance issues.
3. **Reference:** Export existing Crystal Reports as PDFs. Use these as visual specifications for developers to match.
4. **Validate:** Test with production data volumes. Templates that render instantly with 10 rows may slow with 10,000 rows.
## Batch Report Generation and Scheduling in .NET
Production systems often need to generate many reports simultaneously or run report jobs on schedules. IronPDF's thread-safe design supports both scenarios efficiently.
<a id="anchor-generate-multiple-reports-in-parallel"></a>
### Generate Multiple Reports in Parallel
For batch processing, use `Parallel.ForEachAsync` or async patterns with `Task.WhenAll`:
```csharp
using IronPdf;
using System.Collections.Concurrent;
// Generate multiple invoices in parallel using async processing
public async Task<List<ReportResult>> GenerateInvoiceBatchAsync(List<InvoiceModel> invoices)
{
var results = new ConcurrentBag<ReportResult>();
// Process invoices concurrently with controlled parallelism
await Parallel.ForEachAsync(invoices,
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
async (invoice, token) =>
{
// Each thread gets its own renderer instance
var renderer = new ChromePdfRenderer();
string html = BuildInvoiceHtml(invoice);
var pdf = await renderer.RenderHtmlAsPdfAsync(html);
// Save individual invoice PDF
string filename = $"Invoice_{invoice.InvoiceNumber}.pdf";
await pdf.SaveAsAsync(filename);
results.Add(new ReportResult { InvoiceNumber = invoice.InvoiceNumber, Success = true });
});
return results.ToList();
}
```
#### Sample Output
The batch processing example generates multiple invoices in parallel. Here's one of the generated batch invoices:
<iframe loading="lazy" src="/static-assets/pdf/tutorials/crystal-reports-alternative/06-batch-INV-2026-001.pdf" width="100%" height="400px">
</iframe>
<a id="anchor-integrate-report-generation-with-aspnet-core-background-services"></a>
### Integrate Report Generation With ASP.NET Core Background Services
Scheduled report generation fits naturally into ASP.NET Core's hosted service infrastructure:
```csharp
// Background service for scheduled report generation
public class DailyReportService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Calculate next run time (6 AM daily)
var nextRun = DateTime.Now.Date.AddDays(1).AddHours(6);
await Task.Delay(nextRun - DateTime.Now, stoppingToken);
// Create scoped service for report generation
using var scope = _serviceProvider.CreateScope();
var reportService = scope.ServiceProvider.GetRequiredService<IReportGenerationService>();
// Generate and distribute daily report
var salesReport = await reportService.GenerateDailySalesSummaryAsync();
// Email or save reports as needed
}
}
}
```
<a id="anchor-download-the-complete-test-project"></a>
## Download the Complete Test Project
All code examples from this tutorial are available in a ready-to-run .NET 10 test project. The [download](/static-assets/pdf/tutorials/crystal-reports-alternative/CrystalReportsAlternativeTests.zip) includes the complete source code, data models, HTML templates, and a test runner that generates all sample PDFs shown above.
## Next Steps
The examples throughout this guide demonstrate that [IronPDF](https://ironpdf.com/) handles the full range of business reporting needs: simple [invoices](/tutorials/csharp-invoice-processing/) with line items and totals, complex employee directories with grouped data and photos, inventory [reports](/how-to/csharp-pdf-reports/) with conditional formatting and charts, [batch processing](/tutorials/batch-pdf-processing-csharp/) of hundreds of documents in parallel, and scheduled generation through background services.
If you're evaluating alternatives for an existing Crystal Reports implementation, start with a single high-value report. Rebuild it using the [HTML-to-PDF](/tutorials/html-to-pdf/) patterns shown here, compare the development experience and output quality, then expand from there. Many teams find that their first converted report takes a few hours as they establish patterns and base templates, while subsequent reports take only minutes to assemble as they reuse [Razor templates](/how-to/cshtml-to-pdf-razor/) and styling. For layout precision, the [pixel-perfect rendering guide](/how-to/pixel-perfect-html-to-pdf/) covers how to match Crystal Reports output exactly with CSS.
Ready to start building? [Download IronPDF](download-modal) and try it with a free trial. The same library handles everything from single report rendering to [high-volume batch generation](/how-to/async/) across .NET environments. If you have questions about migrating reports or need architecture guidance, [reach out to our engineering support team](/troubleshooting/engineering-request-pdf/).
HTML-to-PDF report generation in C# .NET with IronPDF replaces Crystal Reports' proprietary .rpt designer with standard HTML, CSS, and Razor templates, enabling .NET developers to build data-driven business reports using the web development skills they already have. That includes full support for dynamic tables, JavaScript-powered charts, conditional formatting, multi-document batch processing, and cross-platform deployment across any environment that runs .NET.
TL;DR: Quickstart Guide
This tutorial covers replacing Crystal Reports with HTML-to-PDF report generation in C# .NET, from basic templates to batch processing and scheduled generation.
Who this is for: .NET developers replacing Crystal Reports or building new reporting systems from scratch.
What you'll build: Three complete report implementations (sales invoice, employee directory, inventory report), plus Chart.js visualizations, branded headers/footers, table of contents generation, sub-report merging, and parallel batch processing.
Where it runs: .NET 10, .NET 8 LTS, .NET Framework 4.6.2+, and .NET Standard 2.0. No Windows-only COM dependencies.
When to use this approach: When Crystal Reports' lack of .NET Core support, Windows lock-in, or complex licensing becomes a bottleneck.
Why it matters technically: HTML/CSS renders identically across platforms, integrates with CI/CD, and executes JavaScript for charts, all without a proprietary designer or per-document fees.
To follow along with the code examples, install IronPdf via NuGet (Install-Package IronPdf). Generate your first report with just a few lines of code:
1Install IronPDF with NuGet Package Manager
PM > Install-Package IronPdf
Install-Package IronPdf
2Copy and run this code snippet.
// Install-Package IronPdfvar pdf = new IronPdf.ChromePdfRenderer() .RenderHtmlAsPdf("<h1>Sales Report</h1><table><tr><td>Q1</td><td>$50,000</td></tr></table>") .SaveAs("sales-report.pdf");
// Install-Package IronPdf
var pdf = new IronPdf.ChromePdfRenderer()
.RenderHtmlAsPdf("<h1>Sales Report</h1><table><tr><td>Q1</td><td>$50,000</td></tr></table>")
.SaveAs("sales-report.pdf");
C#
3Deploy to test on your live environment
Start using IronPDF in your project today with a free trial
After you've purchased or signed up for a 30-day trial of IronPDF, add your license key at the start of your application.
HTML-to-PDF generation relies on a linear architectural pipeline. Instead of a proprietary file format, the application uses standard data models to populate Razor views or HTML templates. The resulting HTML string is then passed to a rendering engine like IronPdf, which captures the visual output as a PDF document. This approach decouples the report design from the hosting environment, allowing the exact same code to execute on any platform that supports .NET.
This workflow mirrors standard web development. Front-end developers create the layout using CSS and preview it immediately in any browser. Backend developers then bind the data using C#. This separation allows teams to use their existing version control, code review, and continuous deployment processes for reports just as they do for the rest of the application.
HTML enables features unavailable in Crystal Reports: interactive charts, responsive tables, and shared styles for consistent branding.
Why Replace Crystal Reports in .NET Applications
The migration away from Crystal Reports is not the result of a single catastrophic issue or sudden abandonment by SAP. Instead, it is the accumulation of friction points that collectively make the platform increasingly difficult to justify for new projects and more challenging to maintain in existing solutions. Identifying these pain points clarifies why many teams are searching for alternatives and which criteria are most important when evaluating replacement options.
No .NET 8 or .NET Core Support
Crystal Reports does not support .NET Core or .NET 5-10. SAP has stated on forums that they do not plan to add support. The SDK uses COM components, which are incompatible with cross-platform .NET. Support for modern .NET would require a complete rewrite, which SAP has declined to do.
As a result, teams building new applications on current .NET versions cannot use Crystal Reports. Organizations standardized on .NET 8 or .NET 10 are unable to integrate it. For existing applications, upgrading to a modern .NET runtime requires replacing the reporting system first.
Complex Licensing and Hidden Costs
Crystal Reports licensing distinguishes between designer licenses, runtime licenses, server deployments, and embedded use. Rules vary for desktop, web, and terminal services. Compliance in one setup may need extra licenses in another. If gaps appear after deployment, unexpected costs arise. Many organizations decide that the uncertainty is worse than migrating to a solution with clearer licensing.
Windows-Only Platform Lock-In
Crystal Reports only runs on Windows with the legacy .NET Framework. You cannot deploy these applications to Linux containers, Azure App Service on Linux, AWS Lambda, or Google Cloud Run. As organizations use containerized, platform-agnostic, and serverless systems, these restrictions become more important.
Development teams building microservices face extra challenges. If nine services run in lightweight Linux containers but one needs Windows for Crystal Reports, deployment is more complicated. You need Windows container images, Windows-compatible hosting, and separate deployment settings. The reporting service becomes an exception, blocking standardization.
Set Up a C# Report Generator in .NET 10
Getting started with IronPDF is simple. Install the library through NuGet like any other .NET dependency. There is no extra software to download or a separate runtime installer for production servers.
Choose a Template Approach: Razor, HTML, or Hybrid
IronPDF supports three distinct approaches to building report templates. Each approach offers specific advantages depending on team composition, project requirements, and long-term maintenance considerations.
Razor Views offer the richest development experience for teams already working in the .NET ecosystem. Strongly-typed models with full IntelliSense support in Visual Studio and VS Code, compile-time checking, and the full power of C# for loops, conditionals, null handling, and string formatting are available. Razor's syntax is familiar to those who have built ASP.NET Core applications, eliminating the learning curve associated with template engines from other ecosystems. The templates reside in the project alongside other source files, participate in refactoring operations, and compile as part of the normal build process.
Plain HTML with String Interpolation works well for simpler reports or teams that prefer keeping templates separate from .NET code entirely. HTML templates can be stored as embedded resources compiled into the assembly, external files deployed alongside the application, or even retrieved from a database or content management system at runtime. Basic data binding uses string.Replace() for single values or a lightweight templating library like Scriban or Fluid for more advanced scenarios. This approach maximizes portability, allowing designers to edit templates without any .NET tooling installed, using only a text editor and a web browser for preview.
Hybrid Approaches combine both techniques for scenarios requiring flexibility. For instance, a Razor view might be rendered to generate the main HTML structure, then post-processed with additional string replacements for dynamic elements that do not fit cleanly into the view model. Alternatively, an HTML template designed by a non-developer can be loaded, and Razor partial views can be used to render only the complex, data-driven sections before combining everything. The HTML-to-PDF conversion is agnostic to the HTML source, allowing you to mix approaches based on each report's needs.
Given these options, this tutorial focuses primarily on Razor views because they offer the best balance of type safety, maintainability, and feature richness for typical business reporting scenarios. The skills transfer directly if future requirements include working with plain HTML templates, as both methods produce HTML strings.
Build a Data-Driven PDF Report in C#
This section demonstrates the complete creation of a sales invoice report, from start to finish. The example covers the essential pattern used for all reports: define a model that structures the data, create a Razor template that transforms data into formatted HTML, render that template to an HTML string, and convert the HTML to a PDF document ready for viewing, emailing, or archiving.
Create the HTML/CSS Report Template
The first step is to define the data model. A real invoice requires customer information, line items with descriptions and pricing, calculated totals, tax handling, and company branding elements. Model classes should be structured to reflect these groupings:
// Invoice data model with customer, company, and line item detailspublic class InvoiceModel{ public stringInvoiceNumber { get; set; } = string.Empty; public DateTimeInvoiceDate { get; set; } public DateTimeDueDate { get; set; } public CompanyInfoCompany { get; set; } = new(); public CustomerInfoCustomer { get; set; } = new(); public List<LineItem> Items { get; set; } = new(); // Computed totals - business logic stays in the model public decimalSubtotal => Items.Sum(x => x.Total); public decimalTaxRate { get; set; } = 0.08m; public decimalTaxAmount => Subtotal * TaxRate; public decimalGrandTotal => Subtotal + TaxAmount;}// Company details for invoice headerpublic class CompanyInfo{ public stringName { get; set; } = string.Empty; public stringAddress { get; set; } = string.Empty; public stringCity { get; set; } = string.Empty; public stringPhone { get; set; } = string.Empty; public stringEmail { get; set; } = string.Empty; public stringLogoPath { get; set; } = string.Empty;}// Customer billing informationpublic class CustomerInfo{ public stringName { get; set; } = string.Empty; public stringAddress { get; set; } = string.Empty; public stringCity { get; set; } = string.Empty; public stringEmail { get; set; } = string.Empty;}// Individual invoice line itempublic class LineItem{ public stringDescription { get; set; } = string.Empty; public intQuantity { get; set; } public decimalUnitPrice { get; set; } public decimalTotal => Quantity * UnitPrice;}
// Invoice data model with customer, company, and line item details
public class InvoiceModel
{
public string InvoiceNumber { get; set; } = string.Empty;
public DateTime InvoiceDate { get; set; }
public DateTime DueDate { get; set; }
public CompanyInfo Company { get; set; } = new();
public CustomerInfo Customer { get; set; } = new();
public List<LineItem> Items { get; set; } = new();
// Computed totals - business logic stays in the model
public decimal Subtotal => Items.Sum(x => x.Total);
public decimal TaxRate { get; set; } = 0.08m;
public decimal TaxAmount => Subtotal * TaxRate;
public decimal GrandTotal => Subtotal + TaxAmount;
}
// Company details for invoice header
public class CompanyInfo
{
public string Name { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string LogoPath { get; set; } = string.Empty;
}
// Customer billing information
public class CustomerInfo
{
public string Name { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
// Individual invoice line item
public class LineItem
{
public string Description { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Total => Quantity * UnitPrice;
}
ImportsSystemImportsSystem.Collections.GenericImportsSystem.Linq' Invoice data model with customer, company, and line item detailsPublic Class InvoiceModel Public Property InvoiceNumberAsString = String.Empty Public Property InvoiceDateAsDateTime Public Property DueDateAsDateTime Public Property CompanyAs New CompanyInfo() Public Property CustomerAs New CustomerInfo() Public Property ItemsAs New List(OfLineItem)() ' Computed totals - business logic stays in the model PublicReadOnlyPropertySubtotalAsDecimal Get ReturnItems.Sum(Function(x) x.Total)End Get End Property Public Property TaxRateAsDecimal = 0.08D PublicReadOnlyPropertyTaxAmountAsDecimal Get ReturnSubtotal * TaxRateEnd Get End Property PublicReadOnlyPropertyGrandTotalAsDecimal Get ReturnSubtotal + TaxAmountEnd Get End PropertyEnd Class' Company details for invoice headerPublic Class CompanyInfo Public Property NameAsString = String.Empty Public Property AddressAsString = String.Empty Public Property CityAsString = String.Empty Public Property PhoneAsString = String.Empty Public Property EmailAsString = String.Empty Public Property LogoPathAsString = String.EmptyEnd Class' Customer billing informationPublic Class CustomerInfo Public Property NameAsString = String.Empty Public Property AddressAsString = String.Empty Public Property CityAsString = String.Empty Public Property EmailAsString = String.EmptyEnd Class' Individual invoice line itemPublic Class LineItem Public Property DescriptionAsString = String.Empty Public Property QuantityAsInteger Public Property UnitPriceAsDecimal PublicReadOnlyPropertyTotalAsDecimal Get ReturnQuantity * UnitPriceEnd Get End PropertyEnd Class
Imports System
Imports System.Collections.Generic
Imports System.Linq
' Invoice data model with customer, company, and line item details
Public Class InvoiceModel
Public Property InvoiceNumber As String = String.Empty
Public Property InvoiceDate As DateTime
Public Property DueDate As DateTime
Public Property Company As New CompanyInfo()
Public Property Customer As New CustomerInfo()
Public Property Items As New List(Of LineItem)()
' Computed totals - business logic stays in the model
Public ReadOnly Property Subtotal As Decimal
Get
Return Items.Sum(Function(x) x.Total)
End Get
End Property
Public Property TaxRate As Decimal = 0.08D
Public ReadOnly Property TaxAmount As Decimal
Get
Return Subtotal * TaxRate
End Get
End Property
Public ReadOnly Property GrandTotal As Decimal
Get
Return Subtotal + TaxAmount
End Get
End Property
End Class
' Company details for invoice header
Public Class CompanyInfo
Public Property Name As String = String.Empty
Public Property Address As String = String.Empty
Public Property City As String = String.Empty
Public Property Phone As String = String.Empty
Public Property Email As String = String.Empty
Public Property LogoPath As String = String.Empty
End Class
' Customer billing information
Public Class CustomerInfo
Public Property Name As String = String.Empty
Public Property Address As String = String.Empty
Public Property City As String = String.Empty
Public Property Email As String = String.Empty
End Class
' Individual invoice line item
Public Class LineItem
Public Property Description As String = String.Empty
Public Property Quantity As Integer
Public Property UnitPrice As Decimal
Public ReadOnly Property Total As Decimal
Get
Return Quantity * UnitPrice
End Get
End Property
End Class
The computed properties for Subtotal, TaxAmount, and GrandTotal are included in the model. These calculations belong in the model rather than the template, keeping Razor views focused on presentation while the model handles business logic. This separation makes unit testing straightforward, allowing calculations to be verified without rendering any HTML.
Now create the Razor view that transforms this model into a professionally formatted invoice. Save this as InvoiceTemplate.cshtml in your Views folder:
The CSS embedded in this template handles all visual styling such as colors, fonts, spacing, and table formatting. IronPDF also supports modern CSS features like flexbox, grid layouts, and CSS variables. The rendered PDF matches Chrome's print preview exactly, which makes debugging straightforward: if something looks wrong in the PDF, open the HTML in a browser and use developer tools to inspect and adjust styles.
Bind Data to the Template
With the model and template in place, rendering the PDF requires connecting them through IronPDF's ChromePdfRenderer. The key step is converting the Razor view to an HTML string, then passing that string to the renderer:
using IronPdf;// Service class for generating invoice PDFs from Razor viewspublic class InvoiceReportService{ private readonly IRazorViewEngine _razorViewEngine; private readonly ITempDataProvider _tempDataProvider; private readonly IServiceProvider _serviceProvider; publicInvoiceReportService( IRazorViewEngine razorViewEngine, ITempDataProvider tempDataProvider, IServiceProvider serviceProvider) { _razorViewEngine = razorViewEngine; _tempDataProvider = tempDataProvider; _serviceProvider = serviceProvider; } // Generate PDF from invoice model public async Task<byte[]> GenerateInvoicePdfAsync(InvoiceModel invoice) { // Render Razor view to HTML string string html = awaitRenderViewToStringAsync("InvoiceTemplate", invoice); // Configure PDF renderer with margins and paper size var renderer = new ChromePdfRenderer(); renderer.RenderingOptions.MarginTop = 10; renderer.RenderingOptions.MarginBottom = 10; renderer.RenderingOptions.MarginLeft = 10; renderer.RenderingOptions.MarginRight = 10; renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.Letter; // Convert HTML to PDF and return bytes var pdfDocument = renderer.RenderHtmlAsPdf(html); return pdfDocument.BinaryData; } // Helper method to render a Razor view to string private async Task<string> RenderViewToStringAsync<TModel>(string viewName, TModel model) { var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider }; var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); using var stringWriter = new StringWriter(); var viewResult = _razorViewEngine.FindView(actionContext, viewName, false); if (!viewResult.Success) throw new InvalidOperationException($"View '{viewName}' not found."); var viewDictionary = new ViewDataDictionary<TModel>( new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = model }; var viewContext = new ViewContext(actionContext, viewResult.View, viewDictionary, new TempDataDictionary(actionContext.HttpContext, _tempDataProvider), stringWriter, new HtmlHelperOptions()); await viewResult.View.RenderAsync(viewContext); return stringWriter.ToString(); }}
using IronPdf;
// Service class for generating invoice PDFs from Razor views
public class InvoiceReportService
{
private readonly IRazorViewEngine _razorViewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
public InvoiceReportService(
IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_razorViewEngine = razorViewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
// Generate PDF from invoice model
public async Task<byte[]> GenerateInvoicePdfAsync(InvoiceModel invoice)
{
// Render Razor view to HTML string
string html = await RenderViewToStringAsync("InvoiceTemplate", invoice);
// Configure PDF renderer with margins and paper size
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.MarginTop = 10;
renderer.RenderingOptions.MarginBottom = 10;
renderer.RenderingOptions.MarginLeft = 10;
renderer.RenderingOptions.MarginRight = 10;
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.Letter;
// Convert HTML to PDF and return bytes
var pdfDocument = renderer.RenderHtmlAsPdf(html);
return pdfDocument.BinaryData;
}
// Helper method to render a Razor view to string
private async Task<string> RenderViewToStringAsync<TModel>(string viewName, TModel model)
{
var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
using var stringWriter = new StringWriter();
var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);
if (!viewResult.Success)
throw new InvalidOperationException($"View '{viewName}' not found.");
var viewDictionary = new ViewDataDictionary<TModel>(
new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = model };
var viewContext = new ViewContext(actionContext, viewResult.View, viewDictionary,
new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
stringWriter, new HtmlHelperOptions());
await viewResult.View.RenderAsync(viewContext);
return stringWriter.ToString();
}
}
ImportsIronPdfImportsMicrosoft.AspNetCore.Mvc.RazorImportsMicrosoft.AspNetCore.Mvc.ViewFeaturesImportsMicrosoft.Extensions.DependencyInjectionImportsMicrosoft.AspNetCore.HttpImportsMicrosoft.AspNetCore.MvcImportsMicrosoft.AspNetCore.RoutingImportsSystem.IOImportsSystem.Threading.Tasks' Service class for generating invoice PDFs from Razor viewsPublic Class InvoiceReportService PrivateReadOnly _razorViewEngine AsIRazorViewEngine PrivateReadOnly _tempDataProvider AsITempDataProvider PrivateReadOnly _serviceProvider AsIServiceProvider Public Sub New(razorViewEngine AsIRazorViewEngine, tempDataProvider AsITempDataProvider, serviceProvider AsIServiceProvider) _razorViewEngine = razorViewEngine _tempDataProvider = tempDataProvider _serviceProvider = serviceProvider End Sub ' Generate PDF from invoice model PublicAsync Function GenerateInvoicePdfAsync(invoice AsInvoiceModel) AsTask(OfByte()) ' Render Razor view to HTML string Dim html AsString = AwaitRenderViewToStringAsync("InvoiceTemplate", invoice) ' Configure PDF renderer with margins and paper size Dim renderer As New ChromePdfRenderer() renderer.RenderingOptions.MarginTop = 10 renderer.RenderingOptions.MarginBottom = 10 renderer.RenderingOptions.MarginLeft = 10 renderer.RenderingOptions.MarginRight = 10 renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.Letter ' Convert HTML to PDF and return bytes Dim pdfDocument = renderer.RenderHtmlAsPdf(html) Return pdfDocument.BinaryData End Function ' Helper method to render a Razor view to string PrivateAsync Function RenderViewToStringAsync(OfTModel)(viewName AsString, model AsTModel) AsTask(OfString) Dim httpContext As New DefaultHttpContextWith {.RequestServices = _serviceProvider} Dim actionContext As New ActionContext(httpContext, New RouteData(), New ActionDescriptor())Using stringWriter As New StringWriter() Dim viewResult = _razorViewEngine.FindView(actionContext, viewName, False) IfNot viewResult.SuccessThenThrow New InvalidOperationException($"View '{viewName}' not found.") End If Dim viewDictionary As New ViewDataDictionary(OfTModel)( New EmptyModelMetadataProvider(), New ModelStateDictionary()) With {.Model = model} Dim viewContext As New ViewContext(actionContext, viewResult.View, viewDictionary, New TempDataDictionary(actionContext.HttpContext, _tempDataProvider), stringWriter, New HtmlHelperOptions())Await viewResult.View.RenderAsync(viewContext) Return stringWriter.ToString()EndUsing End FunctionEnd Class
Imports IronPdf
Imports Microsoft.AspNetCore.Mvc.Razor
Imports Microsoft.AspNetCore.Mvc.ViewFeatures
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.AspNetCore.Http
Imports Microsoft.AspNetCore.Mvc
Imports Microsoft.AspNetCore.Routing
Imports System.IO
Imports System.Threading.Tasks
' Service class for generating invoice PDFs from Razor views
Public Class InvoiceReportService
Private ReadOnly _razorViewEngine As IRazorViewEngine
Private ReadOnly _tempDataProvider As ITempDataProvider
Private ReadOnly _serviceProvider As IServiceProvider
Public Sub New(razorViewEngine As IRazorViewEngine, tempDataProvider As ITempDataProvider, serviceProvider As IServiceProvider)
_razorViewEngine = razorViewEngine
_tempDataProvider = tempDataProvider
_serviceProvider = serviceProvider
End Sub
' Generate PDF from invoice model
Public Async Function GenerateInvoicePdfAsync(invoice As InvoiceModel) As Task(Of Byte())
' Render Razor view to HTML string
Dim html As String = Await RenderViewToStringAsync("InvoiceTemplate", invoice)
' Configure PDF renderer with margins and paper size
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.MarginTop = 10
renderer.RenderingOptions.MarginBottom = 10
renderer.RenderingOptions.MarginLeft = 10
renderer.RenderingOptions.MarginRight = 10
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.Letter
' Convert HTML to PDF and return bytes
Dim pdfDocument = renderer.RenderHtmlAsPdf(html)
Return pdfDocument.BinaryData
End Function
' Helper method to render a Razor view to string
Private Async Function RenderViewToStringAsync(Of TModel)(viewName As String, model As TModel) As Task(Of String)
Dim httpContext As New DefaultHttpContext With {.RequestServices = _serviceProvider}
Dim actionContext As New ActionContext(httpContext, New RouteData(), New ActionDescriptor())
Using stringWriter As New StringWriter()
Dim viewResult = _razorViewEngine.FindView(actionContext, viewName, False)
If Not viewResult.Success Then
Throw New InvalidOperationException($"View '{viewName}' not found.")
End If
Dim viewDictionary As New ViewDataDictionary(Of TModel)(
New EmptyModelMetadataProvider(), New ModelStateDictionary()) With {.Model = model}
Dim viewContext As New ViewContext(actionContext, viewResult.View, viewDictionary,
New TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
stringWriter, New HtmlHelperOptions())
Await viewResult.View.RenderAsync(viewContext)
Return stringWriter.ToString()
End Using
End Function
End Class
For simpler scenarios, when you don't need the full ASP.NET Core MVC setup, like in a console app or background service, you can just use HTML strings with interpolation and StringBuilder for the dynamic parts.
Sample Output
Add Headers, Footers, and Page Numbers
Professional reports typically include consistent headers and footers across all pages, displaying company branding, document titles, generation dates, and page numbers. IronPDF provides two approaches for implementing these elements: text-based headers for simple content requiring minimal formatting, and HTML headers for full styling control with logos and custom layouts.
Text-based headers work well for basic information and render faster since they don't require additional HTML parsing:
using IronPdf;using IronSoftware.Drawing;// Configure text-based headers and footersvar renderer = new ChromePdfRenderer();// Set starting page numberrenderer.RenderingOptions.FirstPageNumber = 1;// Add centered header with divider linerenderer.RenderingOptions.TextHeader = new TextHeaderFooter{CenterText = "CONFIDENTIAL - Internal Use Only",DrawDividerLine = true,Font = FontTypes.Arial,FontSize = 10};// Add footer with date on left, page numbers on rightrenderer.RenderingOptions.TextFooter = new TextHeaderFooter{LeftText = "{date} {time}",RightText = "Page {page} of {total-pages}",DrawDividerLine = true,Font = FontTypes.Arial,FontSize = 9};// Set margins to accommodate header/footerrenderer.RenderingOptions.MarginTop = 25;renderer.RenderingOptions.MarginBottom = 20;
using IronPdf;
using IronSoftware.Drawing;
// Configure text-based headers and footers
var renderer = new ChromePdfRenderer();
// Set starting page number
renderer.RenderingOptions.FirstPageNumber = 1;
// Add centered header with divider line
renderer.RenderingOptions.TextHeader = new TextHeaderFooter
{
CenterText = "CONFIDENTIAL - Internal Use Only",
DrawDividerLine = true,
Font = FontTypes.Arial,
FontSize = 10
};
// Add footer with date on left, page numbers on right
renderer.RenderingOptions.TextFooter = new TextHeaderFooter
{
LeftText = "{date} {time}",
RightText = "Page {page} of {total-pages}",
DrawDividerLine = true,
Font = FontTypes.Arial,
FontSize = 9
};
// Set margins to accommodate header/footer
renderer.RenderingOptions.MarginTop = 25;
renderer.RenderingOptions.MarginBottom = 20;
ImportsIronPdfImportsIronSoftware.Drawing' Configure text-based headers and footersDim renderer As New ChromePdfRenderer()' Set starting page numberrenderer.RenderingOptions.FirstPageNumber = 1' Add centered header with divider linerenderer.RenderingOptions.TextHeader = New TextHeaderFooterWith { .CenterText = "CONFIDENTIAL - Internal Use Only", .DrawDividerLine = True, .Font = FontTypes.Arial, .FontSize = 10}' Add footer with date on left, page numbers on rightrenderer.RenderingOptions.TextFooter = New TextHeaderFooterWith { .LeftText = "{date} {time}", .RightText = "Page {page} of {total-pages}", .DrawDividerLine = True, .Font = FontTypes.Arial, .FontSize = 9}' Set margins to accommodate header/footerrenderer.RenderingOptions.MarginTop = 25renderer.RenderingOptions.MarginBottom = 20
Imports IronPdf
Imports IronSoftware.Drawing
' Configure text-based headers and footers
Dim renderer As New ChromePdfRenderer()
' Set starting page number
renderer.RenderingOptions.FirstPageNumber = 1
' Add centered header with divider line
renderer.RenderingOptions.TextHeader = New TextHeaderFooter With {
.CenterText = "CONFIDENTIAL - Internal Use Only",
.DrawDividerLine = True,
.Font = FontTypes.Arial,
.FontSize = 10
}
' Add footer with date on left, page numbers on right
renderer.RenderingOptions.TextFooter = New TextHeaderFooter With {
.LeftText = "{date} {time}",
.RightText = "Page {page} of {total-pages}",
.DrawDividerLine = True,
.Font = FontTypes.Arial,
.FontSize = 9
}
' Set margins to accommodate header/footer
renderer.RenderingOptions.MarginTop = 25
renderer.RenderingOptions.MarginBottom = 20
The merge fields available include {page} for the current page number, {total-pages} for the document's total page count, {date} and {time} for generation timestamps, {url} for the source URL if rendering from a web page, and {html-title} and {pdf-title} for document titles. These placeholders are automatically replaced during rendering.
For headers with logos, custom fonts, or complex multi-column layouts, use HTML headers that support full CSS styling:
using IronPdf;using System;var renderer = new ChromePdfRenderer();// Configure HTML header with logo and custom layoutrenderer.RenderingOptions.HtmlHeader = new HtmlHeaderFooter{MaxHeight = 30,HtmlFragment = @"<div style='display: flex; justify-content: space-between; align-items: center; width: 100%; font-family: Arial; font-size: 10px; color: #666;'> <img src='logo.png' style='height: 25px;'> <span>Company Name Inc.</span> <span>Invoice Report</span></div>",BaseUrl = new Uri(@"C:\assets\images\").AbsoluteUri};// Configure HTML footer with page info and generation daterenderer.RenderingOptions.HtmlFooter = new HtmlHeaderFooter{MaxHeight = 20,HtmlFragment = @"<div style='text-align: center; font-size: 9px; color: #999; border-top: 1px solid #ddd; padding-top: 5px;'> Page {page} of {total-pages} | Generated on {date}</div>",DrawDividerLine = false};
using IronPdf;
using System;
var renderer = new ChromePdfRenderer();
// Configure HTML header with logo and custom layout
renderer.RenderingOptions.HtmlHeader = new HtmlHeaderFooter
{
MaxHeight = 30,
HtmlFragment = @"
<div style='display: flex; justify-content: space-between; align-items: center;
width: 100%; font-family: Arial; font-size: 10px; color: #666;'>
<img src='logo.png' style='height: 25px;'>
<span>Company Name Inc.</span>
<span>Invoice Report</span>
</div>",
BaseUrl = new Uri(@"C:\assets\images\").AbsoluteUri
};
// Configure HTML footer with page info and generation date
renderer.RenderingOptions.HtmlFooter = new HtmlHeaderFooter
{
MaxHeight = 20,
HtmlFragment = @"
<div style='text-align: center; font-size: 9px; color: #999;
border-top: 1px solid #ddd; padding-top: 5px;'>
Page {page} of {total-pages} | Generated on {date}
</div>",
DrawDividerLine = false
};
ImportsIronPdfImportsSystemDim renderer As New ChromePdfRenderer()' Configure HTML header with logo and custom layoutrenderer.RenderingOptions.HtmlHeader = New HtmlHeaderFooterWith { .MaxHeight = 30, .HtmlFragment = "<div style='display: flex; justify-content: space-between; align-items: center; width: 100%; font-family: Arial; font-size: 10px; color: #666;'> <img src='logo.png' style='height: 25px;'> <span>Company Name Inc.</span> <span>Invoice Report</span></div>", .BaseUrl = New Uri("C:\assets\images\").AbsoluteUri}' Configure HTML footer with page info and generation daterenderer.RenderingOptions.HtmlFooter = New HtmlHeaderFooterWith { .MaxHeight = 20, .HtmlFragment = "<div style='text-align: center; font-size: 9px; color: #999; border-top: 1px solid #ddd; padding-top: 5px;'> Page {page} of {total-pages} | Generated on {date}</div>", .DrawDividerLine = False}
Imports IronPdf
Imports System
Dim renderer As New ChromePdfRenderer()
' Configure HTML header with logo and custom layout
renderer.RenderingOptions.HtmlHeader = New HtmlHeaderFooter With {
.MaxHeight = 30,
.HtmlFragment = "
<div style='display: flex; justify-content: space-between; align-items: center;
width: 100%; font-family: Arial; font-size: 10px; color: #666;'>
<img src='logo.png' style='height: 25px;'>
<span>Company Name Inc.</span>
<span>Invoice Report</span>
</div>",
.BaseUrl = New Uri("C:\assets\images\").AbsoluteUri
}
' Configure HTML footer with page info and generation date
renderer.RenderingOptions.HtmlFooter = New HtmlHeaderFooter With {
.MaxHeight = 20,
.HtmlFragment = "
<div style='text-align: center; font-size: 9px; color: #999;
border-top: 1px solid #ddd; padding-top: 5px;'>
Page {page} of {total-pages} | Generated on {date}
</div>",
.DrawDividerLine = False
}
Sample Output
Create Dynamic Tables and Repeating Sections
Reports often need to display collections of data that span multiple pages. Razor's loop constructs handle this naturally by iterating over collections and generating table rows or card elements for each item.
Here's a complete Employee Directory example demonstrating grouped data presentation with departmental sections:
// Employee directory data modelspublic class EmployeeDirectoryModel{ public List<Department> Departments { get; set; } = new(); public DateTimeGeneratedDate { get; set; } = DateTime.Now;}// Department grouping with manager infopublic class Department{ public stringName { get; set; } = string.Empty; public stringManagerName { get; set; } = string.Empty; public List<Employee> Employees { get; set; } = new();}// Individual employee detailspublic class Employee{ public stringName { get; set; } = string.Empty; public stringTitle { get; set; } = string.Empty; public stringEmail { get; set; } = string.Empty; public stringPhone { get; set; } = string.Empty; public stringPhotoUrl { get; set; } = string.Empty; public DateTimeHireDate { get; set; }}
// Employee directory data models
public class EmployeeDirectoryModel
{
public List<Department> Departments { get; set; } = new();
public DateTime GeneratedDate { get; set; } = DateTime.Now;
}
// Department grouping with manager info
public class Department
{
public string Name { get; set; } = string.Empty;
public string ManagerName { get; set; } = string.Empty;
public List<Employee> Employees { get; set; } = new();
}
// Individual employee details
public class Employee
{
public string Name { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public string PhotoUrl { get; set; } = string.Empty;
public DateTime HireDate { get; set; }
}
' Employee directory data modelsPublic Class EmployeeDirectoryModel Public Property DepartmentsAsList(OfDepartment) = New List(OfDepartment)() Public Property GeneratedDateAsDateTime = DateTime.NowEnd Class' Department grouping with manager infoPublic Class Department Public Property NameAsString = String.Empty Public Property ManagerNameAsString = String.Empty Public Property EmployeesAsList(OfEmployee) = New List(OfEmployee)()End Class' Individual employee detailsPublic Class Employee Public Property NameAsString = String.Empty Public Property TitleAsString = String.Empty Public Property EmailAsString = String.Empty Public Property PhoneAsString = String.Empty Public Property PhotoUrlAsString = String.Empty Public Property HireDateAsDateTimeEnd Class
' Employee directory data models
Public Class EmployeeDirectoryModel
Public Property Departments As List(Of Department) = New List(Of Department)()
Public Property GeneratedDate As DateTime = DateTime.Now
End Class
' Department grouping with manager info
Public Class Department
Public Property Name As String = String.Empty
Public Property ManagerName As String = String.Empty
Public Property Employees As List(Of Employee) = New List(Of Employee)()
End Class
' Individual employee details
Public Class Employee
Public Property Name As String = String.Empty
Public Property Title As String = String.Empty
Public Property Email As String = String.Empty
Public Property Phone As String = String.Empty
Public Property PhotoUrl As String = String.Empty
Public Property HireDate As DateTime
End Class
The CSS property page-break-inside: avoid on the department class tells the PDF renderer to keep department sections together on a single page when possible. If a department's content would cause a page break mid-section, the renderer moves the entire section to the next page instead. The selector .department:not(:first-child) with page-break-before: always forces each department after the first to start on a new page, creating clean section separation throughout the directory.
Sample Output
Advanced C# Report Generation With IronPDF
Business reports frequently require capabilities beyond static tables and text. Charts visualize trends that would be tedious to comprehend in tabular form. Conditional formatting draws attention to items requiring action. Sub-reports combine data from multiple sources into cohesive documents. This section covers implementing each of these features using IronPDF's Chromium rendering engine.
Add Charts and Graphs to PDF Reports
Because JavaScript executes during rendering, you can use any client-side charting library to generate visualizations directly in your reports. The chart gets rasterized as part of the page and appears in the final PDF exactly as it would on screen. Chart.js offers an excellent balance of simplicity, capability, and documentation for most reporting needs.
Include Chart.js from a CDN and configure your chart with data serialized from your C# model:
@model SalesReportModel<!-- Include Chart.js library from CDN --><script src="https://cdn.jsdelivr.net/npm/chart.js"></script><!-- Canvas element for chart rendering --><canvas id="salesChart"></canvas><script> // Initialize bar chart with data from C# model const ctx = document.getElementById('salesChart').getContext('2d'); new Chart(ctx, { type: 'bar', data: { // Serialize model data to JavaScript arrays labels: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthLabels)), datasets: [{ label: 'Monthly Sales', data: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthlySales)), backgroundColor: 'rgba(52, 152, 219, 0.7)' }] } });</script>
@model SalesReportModel
<!-- Include Chart.js library from CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Canvas element for chart rendering -->
<canvas id="salesChart"></canvas>
<script>
// Initialize bar chart with data from C# model
const ctx = document.getElementById('salesChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
// Serialize model data to JavaScript arrays
labels: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthLabels)),
datasets: [{
label: 'Monthly Sales',
data: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthlySales)),
backgroundColor: 'rgba(52, 152, 219, 0.7)'
}]
}
});
</script>
HTML
When rendering pages that include JavaScript-generated content, configure the renderer to wait for scripts to complete execution before capturing the page:
using IronPdf;string html = "<h1>Report</h1>";// Configure renderer to wait for JavaScript executionvar renderer = new ChromePdfRenderer();renderer.RenderingOptions.WaitFor.JavaScript(500); // Wait 500ms for JS to completevar pdf = renderer.RenderHtmlAsPdf(html);
using IronPdf;
string html = "<h1>Report</h1>";
// Configure renderer to wait for JavaScript execution
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.WaitFor.JavaScript(500); // Wait 500ms for JS to complete
var pdf = renderer.RenderHtmlAsPdf(html);
ImportsIronPdfDim html AsString = "<h1>Report</h1>"' Configure renderer to wait for JavaScript executionDim renderer As New ChromePdfRenderer()renderer.RenderingOptions.WaitFor.JavaScript(500) ' Wait 500ms for JS to completeDim pdf = renderer.RenderHtmlAsPdf(html)
Imports IronPdf
Dim html As String = "<h1>Report</h1>"
' Configure renderer to wait for JavaScript execution
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.WaitFor.JavaScript(500) ' Wait 500ms for JS to complete
Dim pdf = renderer.RenderHtmlAsPdf(html)
Sample Output
Apply Conditional Formatting and Business Logic
Inventory reports benefit from visual indicators that immediately draw attention to items requiring action. Rather than forcing users to scan through hundreds of rows looking for problems, conditional formatting makes exceptions visually obvious. Use Razor's inline expressions to apply CSS classes based on data values:
<!-- Iterate through inventory items sorted by quantity -->@foreach (var item in Model.Items.OrderBy(x => x.Quantity)){ // Apply CSS class based on stock level thresholds var rowClass = item.Quantity <= Model.CriticalStockThreshold ? "stock-critical" : item.Quantity <= Model.LowStockThreshold ? "stock-low" : ""; <tr class="@rowClass"> <td>@item.SKU</td> <td>@item.ProductName</td> <td class="text-right"> <!-- Badge styling for critical stock items --> <span class="quantity-badge @(item.Quantity <= 5 ? "badge-critical" : "badge-ok")"> @item.Quantity </span> </td> </tr>}
<!-- Iterate through inventory items sorted by quantity -->
@foreach (var item in Model.Items.OrderBy(x => x.Quantity))
{
// Apply CSS class based on stock level thresholds
var rowClass = item.Quantity <= Model.CriticalStockThreshold ? "stock-critical" :
item.Quantity <= Model.LowStockThreshold ? "stock-low" : "";
<tr class="@rowClass">
<td>@item.SKU</td>
<td>@item.ProductName</td>
<td class="text-right">
<!-- Badge styling for critical stock items -->
<span class="quantity-badge @(item.Quantity <= 5 ? "badge-critical" : "badge-ok")">
@item.Quantity
</span>
</td>
</tr>
}
HTML
Sample Output
Create Sub-Reports and Section Breaks
For combining independently generated reports into one document, use IronPDF's merge functionality:
using IronPdf;// Combine multiple reports into a single PDF documentpublic byte[] GenerateCombinedReport(SalesReportModel sales, InventoryReportModel inventory){ var renderer = new ChromePdfRenderer(); // Render each report section separately var salesPdf = renderer.RenderHtmlAsPdf(RenderSalesReport(sales)); var inventoryPdf = renderer.RenderHtmlAsPdf(RenderInventoryReport(inventory)); // Merge PDFs into one document var combined = PdfDocument.Merge(salesPdf, inventoryPdf); return combined.BinaryData;}
using IronPdf;
// Combine multiple reports into a single PDF document
public byte[] GenerateCombinedReport(SalesReportModel sales, InventoryReportModel inventory)
{
var renderer = new ChromePdfRenderer();
// Render each report section separately
var salesPdf = renderer.RenderHtmlAsPdf(RenderSalesReport(sales));
var inventoryPdf = renderer.RenderHtmlAsPdf(RenderInventoryReport(inventory));
// Merge PDFs into one document
var combined = PdfDocument.Merge(salesPdf, inventoryPdf);
return combined.BinaryData;
}
ImportsIronPdf' Combine multiple reports into a single PDF documentPublic Function GenerateCombinedReport(sales AsSalesReportModel, inventory AsInventoryReportModel) AsByte() Dim renderer As New ChromePdfRenderer() ' Render each report section separately Dim salesPdf = renderer.RenderHtmlAsPdf(RenderSalesReport(sales)) Dim inventoryPdf = renderer.RenderHtmlAsPdf(RenderInventoryReport(inventory)) ' Merge PDFs into one document Dim combined = PdfDocument.Merge(salesPdf, inventoryPdf) Return combined.BinaryDataEnd Function
Imports IronPdf
' Combine multiple reports into a single PDF document
Public Function GenerateCombinedReport(sales As SalesReportModel, inventory As InventoryReportModel) As Byte()
Dim renderer As New ChromePdfRenderer()
' Render each report section separately
Dim salesPdf = renderer.RenderHtmlAsPdf(RenderSalesReport(sales))
Dim inventoryPdf = renderer.RenderHtmlAsPdf(RenderInventoryReport(inventory))
' Merge PDFs into one document
Dim combined = PdfDocument.Merge(salesPdf, inventoryPdf)
Return combined.BinaryData
End Function
Sample Output
Generate a Table of Contents
IronPDF can automatically generate a table of contents based on heading elements in your HTML:
using IronPdf;// Generate PDF with automatic table of contentsvar renderer = new ChromePdfRenderer();renderer.RenderingOptions.TableOfContents = TableOfContentsTypes.WithPageNumbers;var pdf = renderer.RenderHtmlFileAsPdf("report.html");
using IronPdf;
// Generate PDF with automatic table of contents
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.TableOfContents = TableOfContentsTypes.WithPageNumbers;
var pdf = renderer.RenderHtmlFileAsPdf("report.html");
ImportsIronPdf' Generate PDF with automatic table of contentsDim renderer As New ChromePdfRenderer()renderer.RenderingOptions.TableOfContents = TableOfContentsTypes.WithPageNumbersDim pdf = renderer.RenderHtmlFileAsPdf("report.html")
Imports IronPdf
' Generate PDF with automatic table of contents
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.TableOfContents = TableOfContentsTypes.WithPageNumbers
Dim pdf = renderer.RenderHtmlFileAsPdf("report.html")
My favorite library of this kind is IronPDF. It allows for fast and efficient manipulation of PDF files. It also has many valuable features, like exporting to PDF/A format and digitally signing PDF documents.
IronOCR means we can save $40,000 annually from manual processing, while enhancing productivity and freeing up resources for high-impact tasks. I would highly recommend it.
Migrating an established reporting system requires careful planning to minimize disruption while capturing the opportunity to modernize and simplify. You'll move faster by understanding how Crystal Reports concepts map to the HTML-based approach, rather than trying to replicate every feature literally or preserve every quirk of the original reports.
Map Crystal Reports Concepts to IronPDF
Understanding the conceptual mapping helps you systematically translate existing reports:
Crystal Reports
IronPDF Equivalent
Report sections
HTML divs with CSS page-break properties
Parameter fields
Model properties passed to Razor views
Formula fields
C# computed properties in model classes
Running totals
LINQ aggregations
Sub-reports
Partial views or merged PDF documents
Grouping/sorting
LINQ operations before passing data to template
Cross-tab reports
HTML tables using nested loops
Conditional formatting
Razor @if blocks with CSS classes
Best Strategy for Converting .rpt Templates
Do not attempt to parse .rpt files programmatically. Instead, treat the existing PDF outputs as visual specifications and rebuild the logic using a systematic four-step strategy:
Inventory: Catalog all .rpt files with their purpose, data sources, and usage frequency. Remove obsolete reports to reduce migration scope.
Prioritize: Migrate high-frequency reports first. Target reports with simple layouts or persistent maintenance issues.
Reference: Export existing Crystal Reports as PDFs. Use these as visual specifications for developers to match.
Validate: Test with production data volumes. Templates that render instantly with 10 rows may slow with 10,000 rows.
Batch Report Generation and Scheduling in .NET
Production systems often need to generate many reports simultaneously or run report jobs on schedules. IronPDF's thread-safe design supports both scenarios efficiently.
Generate Multiple Reports in Parallel
For batch processing, use Parallel.ForEachAsync or async patterns with Task.WhenAll:
using IronPdf;using System.Collections.Concurrent;// Generate multiple invoices in parallel using async processingpublic async Task<List<ReportResult>> GenerateInvoiceBatchAsync(List<InvoiceModel> invoices){ var results = new ConcurrentBag<ReportResult>(); // Process invoices concurrently with controlled parallelism awaitParallel.ForEachAsync(invoices, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, async (invoice, token) => { // Each thread gets its own renderer instance var renderer = new ChromePdfRenderer(); string html = BuildInvoiceHtml(invoice); var pdf = await renderer.RenderHtmlAsPdfAsync(html); // Save individual invoice PDF string filename = $"Invoice_{invoice.InvoiceNumber}.pdf"; await pdf.SaveAsAsync(filename); results.Add(new ReportResult { InvoiceNumber = invoice.InvoiceNumber, Success = true }); }); return results.ToList();}
using IronPdf;
using System.Collections.Concurrent;
// Generate multiple invoices in parallel using async processing
public async Task<List<ReportResult>> GenerateInvoiceBatchAsync(List<InvoiceModel> invoices)
{
var results = new ConcurrentBag<ReportResult>();
// Process invoices concurrently with controlled parallelism
await Parallel.ForEachAsync(invoices,
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
async (invoice, token) =>
{
// Each thread gets its own renderer instance
var renderer = new ChromePdfRenderer();
string html = BuildInvoiceHtml(invoice);
var pdf = await renderer.RenderHtmlAsPdfAsync(html);
// Save individual invoice PDF
string filename = $"Invoice_{invoice.InvoiceNumber}.pdf";
await pdf.SaveAsAsync(filename);
results.Add(new ReportResult { InvoiceNumber = invoice.InvoiceNumber, Success = true });
});
return results.ToList();
}
ImportsIronPdfImportsSystem.Collections.ConcurrentImportsSystem.Threading.Tasks' Generate multiple invoices in parallel using async processingPublicAsync Function GenerateInvoiceBatchAsync(invoices AsList(OfInvoiceModel)) AsTask(OfList(OfReportResult)) Dim results As New ConcurrentBag(OfReportResult)() ' Process invoices concurrently with controlled parallelismAwaitTask.Run(Async Function()AwaitParallel.ForEachAsync(invoices, New ParallelOptionsWith {.MaxDegreeOfParallelism = Environment.ProcessorCount},Async Function(invoice, token) ' Each thread gets its own renderer instance Dim renderer As New ChromePdfRenderer() Dim html AsString = BuildInvoiceHtml(invoice) Dim pdf = Await renderer.RenderHtmlAsPdfAsync(html) ' Save individual invoice PDF Dim filename AsString = $"Invoice_{invoice.InvoiceNumber}.pdf"Await pdf.SaveAsAsync(filename) results.Add(New ReportResultWith {.InvoiceNumber = invoice.InvoiceNumber, .Success = True}) End Function) End Function) Return results.ToList()End Function
Imports IronPdf
Imports System.Collections.Concurrent
Imports System.Threading.Tasks
' Generate multiple invoices in parallel using async processing
Public Async Function GenerateInvoiceBatchAsync(invoices As List(Of InvoiceModel)) As Task(Of List(Of ReportResult))
Dim results As New ConcurrentBag(Of ReportResult)()
' Process invoices concurrently with controlled parallelism
Await Task.Run(Async Function()
Await Parallel.ForEachAsync(invoices,
New ParallelOptions With {.MaxDegreeOfParallelism = Environment.ProcessorCount},
Async Function(invoice, token)
' Each thread gets its own renderer instance
Dim renderer As New ChromePdfRenderer()
Dim html As String = BuildInvoiceHtml(invoice)
Dim pdf = Await renderer.RenderHtmlAsPdfAsync(html)
' Save individual invoice PDF
Dim filename As String = $"Invoice_{invoice.InvoiceNumber}.pdf"
Await pdf.SaveAsAsync(filename)
results.Add(New ReportResult With {.InvoiceNumber = invoice.InvoiceNumber, .Success = True})
End Function)
End Function)
Return results.ToList()
End Function
Sample Output
The batch processing example generates multiple invoices in parallel. Here's one of the generated batch invoices:
Integrate Report Generation With ASP.NET Core Background Services
Scheduled report generation fits naturally into ASP.NET Core's hosted service infrastructure:
// Background service for scheduled report generationpublic class DailyReportService : BackgroundService{ private readonly IServiceProvider _serviceProvider; protected override async TaskExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { // Calculate next run time (6 AM daily) var nextRun = DateTime.Now.Date.AddDays(1).AddHours(6); awaitTask.Delay(nextRun - DateTime.Now, stoppingToken); // Create scoped service for report generation using var scope = _serviceProvider.CreateScope(); var reportService = scope.ServiceProvider.GetRequiredService<IReportGenerationService>(); // Generate and distribute daily report var salesReport = await reportService.GenerateDailySalesSummaryAsync(); // Email or save reports as needed } }}
// Background service for scheduled report generation
public class DailyReportService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Calculate next run time (6 AM daily)
var nextRun = DateTime.Now.Date.AddDays(1).AddHours(6);
await Task.Delay(nextRun - DateTime.Now, stoppingToken);
// Create scoped service for report generation
using var scope = _serviceProvider.CreateScope();
var reportService = scope.ServiceProvider.GetRequiredService<IReportGenerationService>();
// Generate and distribute daily report
var salesReport = await reportService.GenerateDailySalesSummaryAsync();
// Email or save reports as needed
}
}
}
ImportsSystemImportsSystem.ThreadingImportsSystem.Threading.TasksImportsMicrosoft.Extensions.DependencyInjection' Background service for scheduled report generationPublic Class DailyReportServiceInheritsBackgroundService PrivateReadOnly _serviceProvider AsIServiceProviderProtectedOverridesAsync Function ExecuteAsync(stoppingToken AsCancellationToken) AsTask WhileNot stoppingToken.IsCancellationRequested ' Calculate next run time (6 AM daily) Dim nextRun = DateTime.Now.Date.AddDays(1).AddHours(6)AwaitTask.Delay(nextRun - DateTime.Now, stoppingToken) ' Create scoped service for report generationUsing scope = _serviceProvider.CreateScope() Dim reportService = scope.ServiceProvider.GetRequiredService(OfIReportGenerationService)() ' Generate and distribute daily report Dim salesReport = Await reportService.GenerateDailySalesSummaryAsync() ' Email or save reports as neededEndUsingEndWhile End FunctionEnd Class
Imports System
Imports System.Threading
Imports System.Threading.Tasks
Imports Microsoft.Extensions.DependencyInjection
' Background service for scheduled report generation
Public Class DailyReportService
Inherits BackgroundService
Private ReadOnly _serviceProvider As IServiceProvider
Protected Overrides Async Function ExecuteAsync(stoppingToken As CancellationToken) As Task
While Not stoppingToken.IsCancellationRequested
' Calculate next run time (6 AM daily)
Dim nextRun = DateTime.Now.Date.AddDays(1).AddHours(6)
Await Task.Delay(nextRun - DateTime.Now, stoppingToken)
' Create scoped service for report generation
Using scope = _serviceProvider.CreateScope()
Dim reportService = scope.ServiceProvider.GetRequiredService(Of IReportGenerationService)()
' Generate and distribute daily report
Dim salesReport = Await reportService.GenerateDailySalesSummaryAsync()
' Email or save reports as needed
End Using
End While
End Function
End Class
Download the Complete Test Project
All code examples from this tutorial are available in a ready-to-run .NET 10 test project. The download includes the complete source code, data models, HTML templates, and a test runner that generates all sample PDFs shown above.
Next Steps
The examples throughout this guide demonstrate that IronPDF handles the full range of business reporting needs: simple invoices with line items and totals, complex employee directories with grouped data and photos, inventory reports with conditional formatting and charts, batch processing of hundreds of documents in parallel, and scheduled generation through background services.
If you're evaluating alternatives for an existing Crystal Reports implementation, start with a single high-value report. Rebuild it using the HTML-to-PDF patterns shown here, compare the development experience and output quality, then expand from there. Many teams find that their first converted report takes a few hours as they establish patterns and base templates, while subsequent reports take only minutes to assemble as they reuse Razor templates and styling. For layout precision, the pixel-perfect rendering guide covers how to match Crystal Reports output exactly with CSS.
How can I replace Crystal Reports with IronPDF in C#?
You can replace Crystal Reports by using IronPDF to generate reports in C# using HTML/CSS and Razor templates, which provide a modern alternative for creating dynamic, data-driven PDF reports.
What are the benefits of using HTML-to-PDF report generation with IronPDF?
HTML-to-PDF report generation with IronPDF offers benefits such as cross-platform compatibility, integration with CI/CD processes, execution of JavaScript for interactive charts, and avoids the need for a proprietary designer or per-document fees that come with Crystal Reports.
Can IronPDF generate reports across different .NET environments?
Yes, IronPDF supports report generation across various .NET environments, including .NET 10, .NET 8 LTS, and .NET Framework 4.6.2+, allowing broader deployment options without Windows-only restrictions.
What types of reports can be created with IronPDF?
With IronPDF, you can create a variety of reports such as sales invoices, employee directories, and inventory reports, complete with features like Chart.js visualizations, branded headers/footers, and dynamic tables.
How does IronPDF handle report styling and formatting?
IronPDF uses standard HTML and CSS for styling and formatting reports, enabling pixel-perfect rendering and the use of modern web design techniques like flexbox and grid layouts.
What is the process to generate a PDF report using IronPDF?
To generate a PDF report, define your data model and create an HTML/CSS or Razor template. Use IronPDF's ChromePdfRenderer to convert the template into an HTML string, and then render it as a PDF. You can also apply headers, footers, and conditional formatting.
How can I include interactive charts in my reports with IronPDF?
Interactive charts can be included by using JavaScript charting libraries like Chart.js within your HTML template. IronPDF supports JavaScript execution, rendering the charts into the final PDF output.
Can IronPDF merge sub-reports into a single PDF?
Yes, IronPDF allows you to merge multiple PDF documents, enabling you to create comprehensive reports that include multiple sections or sub-reports.
Is it possible to automate report generation with IronPDF?
IronPDF supports automation of report generation through batch processing and can be integrated with ASP.NET Core Background Services for scheduled report generation tasks.
What is the best approach to migrate from Crystal Reports to IronPDF?
The best approach involves mapping Crystal Reports concepts to their IronPDF equivalents, prioritizing high-use reports, referencing existing outputs as specifications, and validating the new implementations against production data.
Curtis Chau holds a Bachelor’s degree in Computer Science (Carleton University) and specializes in front-end development with expertise in Node.js, TypeScript, JavaScript, and React. Passionate about crafting intuitive and aesthetically pleasing user interfaces, Curtis enjoys working with modern frameworks and creating well-structured, visually appealing manuals.