Generate Reports in C# Like Crystal Reports (.NET 10)
使用 IronPDF 在 C# .NET 中将 HTML 转换为 PDF 报告,用标准的 HTML、CSS 和 Razor 模板取代了 Crystal Reports 专有的 .rpt 设计器,使 .NET 开发人员能够利用他们已有的 Web 开发技能来构建数据驱动的业务报告。 这包括完全支持动态表格、JavaScript 驱动的图表、条件格式化、多文档批处理以及在任何运行 .NET 的环境中进行跨平台部署。
TL;DR:快速入门指南
本教程涵盖用 C# .NET 生成 HTML-to-PDF 报告来取代 Crystal Reports,从基本模板到批处理和计划生成。
- 适用对象:替换 Crystal Reports 或从头开始构建新报表系统的 .NET 开发人员。
- 您将构建的内容:三个完整的报告实现(销售发票、员工目录、库存报告),Plus Chart.js 可视化、品牌页眉/页脚、目录生成、子报告合并和并行批处理。
- 运行环境: .NET 10、.NET 8 LTS、.NET Framework 4.6.2+ 和 .NET Standard 2.0。无仅适用于 Windows 的 COM 依赖性。
- 何时使用此方法:当 Crystal Reports 缺乏 .NET Core 支持、Windows 锁定或复杂的许可成为瓶颈时。
- 在技术上为何重要: HTML/CSS 在不同平台上的渲染效果完全相同,可与 CI/CD 集成,并可执行 JavaScript 图表,所有这一切都无需专有设计器或按文档收费。
如需参考代码示例,请通过 NuGet (Install-Package IronPdf) 安装 IronPdf。 只需几行代码即可生成您的第一份报告:
-
使用 NuGet 包管理器安装 https://www.nuget.org/packages/IronPdf
PM > Install-Package IronPdf -
复制并运行这段代码。
// 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"); -
部署到您的生产环境中进行测试
通过免费试用立即在您的项目中开始使用IronPDF
购买或注册 IronPDF 30 天试用版后,请在应用程序的开头添加许可证密钥。
IronPdf.License.LicenseKey = "KEY";
IronPdf.License.LicenseKey = "KEY";
Imports IronPdf
IronPdf.License.LicenseKey = "KEY"
今天在您的项目中使用 IronPDF,免费试用。
目录
- TL;DR: 快速入门指南
- HTML 模板到 PDF 架构
- 为什么要在 .NET 应用程序中替换 Crystal Reports.
- 在 .NET 10 中设置 C# 报告生成器
- 用 C# 构建数据驱动的 PDF 报告
- 使用 IronPDF 生成高级 C# 报告。
- 从 Crystal Reports 迁移到 IronPDF。 将 Crystal Reports 概念映射到IronPDF
- 在 .NET 中批量生成报告并进行调度。
- 并行生成多个报告
- Integrate Report Generation With ASP.NET Core Background Services 报告生成与 ASP.NET Core 后台服务集成</a
- 下载完整测试项目
C# Report Generator: HTML Templates to PDF
HTML 到 PDF 的生成依赖于线性架构管道。应用程序使用标准数据模型填充 Razor 视图或 HTML 模板,而不是专有文件格式。 生成的 HTML 字符串随后会被传递给 IronPdf 之类的渲染引擎,该引擎将视觉输出渲染为 PDF 文档。 这种方法将报告设计与托管环境分离开来,使完全相同的代码可以在任何支持 .NET 的平台上执行。
该工作流程反映了标准的网络开发。 前端开发人员使用 CSS 创建布局,并可在任何浏览器中立即预览。 然后,后端开发人员使用 C# 绑定数据。 这种分离允许团队使用现有的版本控制、代码审查和持续部署流程来处理报告,就像处理应用程序的其他部分一样。
HTML 可以实现 Crystal Reports 中无法实现的功能:交互式图表、响应式表格和共享样式,以实现一致的品牌形象。
为什么要在 .NET 应用程序中替换 Crystal Reports.
从 Crystal Reports 迁移到其他系统并不是单一灾难性问题或 SAP 突然放弃的结果。相反,它是各种摩擦点的累积,这些摩擦点共同导致新项目越来越难以证明该平台的合理性,现有解决方案的维护也更具挑战性。 找出这些痛点,就能明确为什么许多团队都在寻找替代方案,以及在评估替代方案时哪些标准是最重要的。
不支持 .NET 8 或 .NET Core.
Crystal Reports 不支持 .NET Core 或 .NET 5-10。SAP 在论坛上表示,他们不打算添加支持。 SDK 使用 COM 组件,与跨平台 .NET 不兼容。 对现代 .NET 的支持需要完全重写,而 SAP 拒绝这样做。
因此,在当前 .NET 版本上构建新应用程序的团队不能使用 Crystal Reports。 以 .NET 8 或 .NET 10 为标准的组织无法集成。 对于现有应用程序,升级到现代 .NET 运行时需要首先更换报告系统。
复杂的许可和隐藏成本
Crystal Reports 许可证区分为设计器许可证、运行时许可证、服务器部署和嵌入式使用。 桌面、网络和终端服务的规则各不相同。 在一种设置中符合要求,在另一种设置中可能需要额外的许可证。 如果在部署后出现漏洞,就会产生意想不到的成本。许多组织认为,这种不确定性比迁移到具有更明确许可的解决方案更糟糕。
仅限 Windows 平台锁定
Crystal Reports 只能在使用传统 .NET Framework 的 Windows 上运行。 您不能将这些应用程序部署到 Linux 容器、Azure App Service on Linux、AWS Lambda 或 Google Cloud Run。 随着企业使用容器化、平台无关和无服务器系统,这些限制变得更加重要。
构建微服务的开发团队面临着额外的挑战。 如果有九个服务在轻量级 Linux 容器中运行,而 Crystal Reports 需要使用 Windows,那么部署工作就会变得更加复杂。 您需要 Windows 容器镜像、Windows 兼容主机和单独的部署设置。 报告服务成为例外,阻碍了标准化。
Set Up a C# Report Generator in .NET 10
开始使用 IronPDF 非常简单。 像安装其他 .NET 依赖项一样,通过 NuGet 安装该库。 不需要下载额外的软件,也不需要为生产服务器单独安装运行时安装程序。
选择模板方法:Razor、HTML 或混合
IronPDF 支持三种不同的报告模板构建方法。 根据团队组成、项目要求和长期维护方面的考虑,每种方法都具有特定的优势。
Razor Views 为已经在 .NET 生态系统中工作的团队提供了最丰富的开发体验。 强类型模型在 Visual Studio 和 VS Code 中具有完整的 IntelliSense 支持、编译时检查以及 C# for 循环、条件、空处理和字符串格式化的全部功能。 Razor 的语法对于那些已经构建了 ASP.NET Core 应用程序的人来说非常熟悉,消除了与其他生态系统的模板引擎相关的学习曲线。 模板与其他源文件一起驻留在项目中,参与重构操作,并作为正常构建流程的一部分进行编译。
Plain HTML with String Interpolation 对于较简单的报告或喜欢将模板与 .NET 代码完全分开的团队非常适用。 HTML 模板可以存储为编译到程序集中的嵌入式资源、与应用程序一同部署的外部文件,甚至可以在运行时从数据库或内容管理系统中检索。基本的数据绑定使用 string.Replace() 处理单个值,或在更复杂的场景中使用 Scriban 或 Fluid 等轻量级模板库。 这种方法最大限度地提高了可移植性,允许设计人员在不安装任何 .NET 工具的情况下编辑模板,只需使用文本编辑器和网络浏览器进行预览。
混合方法结合了这两种技术,适用于需要灵活性的场景。 例如,在渲染 Razor 视图时,可能会生成主要的 HTML 结构,然后对不适合视图模型的动态元素进行额外的字符串替换后处理。 另外,还可以加载非开发人员设计的 HTML 模板,并使用 Razor 部分视图在组合所有内容之前,只呈现复杂的数据驱动部分。 HTML 到 PDF 的转换与 HTML 源无关,您可以根据每份报告的需求混合使用不同的方法。
考虑到这些选项,本教程主要侧重于 Razor 视图,因为它们在类型安全性、可维护性和功能丰富度方面为典型的业务报表场景提供了最佳平衡。 如果未来的需求包括使用纯 HTML 模板,则技能可以直接转换,因为这两种方法都能生成 HTML 字符串。
Build a Data-Driven PDF Report in C
本节演示了从头到尾创建销售发票报告的全过程。 该示例涵盖了所有报告使用的基本模式:定义一个数据结构模型,创建一个将数据转换为格式化 HTML 的 Razor 模板,将该模板渲染为 HTML 字符串,并将 HTML 转换为 PDF 文档,以便随时查看、通过电子邮件发送或存档。
创建 HTML/CSS 报告模板
第一步是定义数据模型。 真实的发票需要客户信息、带有说明和定价的细列项目、计算出的总额、税务处理以及公司品牌元素。 模型类的结构应反映这些分组:
// 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;
}
// 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;
}
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
TaxAmount 和 GrandTotal 的计算属性已包含在模型中。 这些计算属于模型而不是模板,使 Razor 视图专注于展示,而模型处理业务逻辑。 这种分离使单元测试变得简单明了,可以在不渲染任何 HTML 的情况下验证计算结果。
现在创建一个 Razor 视图,将此模型转换为格式专业的发票。将其保存为 InvoiceTemplate.cshtml 并放置在 Views 文件夹中:
@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">
<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>
<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>
<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>
<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>
<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>
@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">
<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>
<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>
<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>
<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>
<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>
该模板中嵌入的 CSS 可处理所有视觉样式,如颜色、字体、间距和表格格式。 IronPDF 还支持柔性框、网格布局和 CSS 变量等现代 CSS 功能。 渲染后的 PDF 与 Chrome 浏览器的打印预览完全匹配,这使得调试变得简单明了:如果 PDF 中的某些内容看起来不对,请在浏览器中打开 HTML,然后使用开发工具检查并调整样式。
将数据绑定到模板
在模型和模板就绪后,生成 PDF 需要通过 IronPDF 的 ChromePdfRenderer 将其连接起来。 关键步骤是将 Razor 视图转换为 HTML 字符串,然后将该字符串传递给呈现器:
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();
}
}
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();
}
}
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
对于更简单的场景,例如控制台应用程序或后台服务,若无需完整的 ASP.NET Core MVC 配置,您可以直接使用带插值的 HTML 字符串,并使用 StringBuilder 表示动态部分。
输出示例
添加页眉、页脚和页码
专业报告通常包括所有页面一致的页眉和页脚,显示公司品牌、文档标题、生成日期和页码。 IronPDF 提供了两种实现这些元素的方法:基于文本的标题,用于需要最少格式化的简单内容;HTML 标题,用于带有徽标和自定义布局的完全样式控制。
Text-based headers work well for basic information and render faster since they don't require additional HTML parsing:
:path=/static-assets/pdf/content-code-examples/tutorials/crystal-reports-alternative-csharp/text-headers-footers.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;
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
可用的合并字段包括:{page} 表示当前页码,{total-pages} 表示文档总页数,{date} 和 {time} 表示生成时间戳,{url} 用于网页渲染时的源 URL,以及 {html-title} 和 {pdf-title} 用于文档标题。 这些占位符会在渲染过程中自动替换。
对于带有徽标、自定义字体或复杂多栏布局的标题,请使用支持完整 CSS 样式的 HTML 标题:
:path=/static-assets/pdf/content-code-examples/tutorials/crystal-reports-alternative-csharp/html-headers-footers.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
};
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
}
输出示例
创建动态表格和重复章节
报告通常需要显示跨越多个页面的数据集合。 Razor 的循环结构通过遍历集合并为每个项目生成表格行或卡片元素,自然而然地处理了这一问题。
下面是一个完整的 "员工目录 "示例,展示了以部门为单位的分组数据展示:
// 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 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 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
部门类中的 CSS 属性 page-break-inside: avoid 指示 PDF 渲染器在可能的情况下,将部门部分集中显示在同一页面上。 如果一个部门的内容会导致中途分页,渲染器会将整个部分移到下一页。 使用 .department:not(:first-child) 与 page-break-before: always 组合的选定符,会强制第一个部门之后的所有部门均从新页面开始,从而在整个目录中实现清晰的章节划分。
输出示例
Advanced C# Report Generation With IronPDF
商业报告经常需要超越静态表格和文本的功能。 图表可以直观地反映趋势,而表格的形式则难以理解。 条件格式化可引起对需要采取行动的项目的注意。 子报告将多个来源的数据合并成具有凝聚力的文件。 本节内容包括使用 IronPDF 的 Chromium 渲染引擎实现上述各项功能。
在 PDF 报告中添加图表和图形
由于 JavaScript 在渲染过程中执行,因此您可以使用任何客户端图表库直接在报告中生成可视化效果。 图表将作为页面的一部分进行光栅化处理,并在最终的 PDF 中显示,与屏幕上一模一样。 Chart.js 在简洁性、功能和文档方面取得了极佳的平衡,可满足大多数报告需求。
从 CDN 包含 Chart.js,并使用从 C# 模型序列化的数据配置图表:
@model SalesReportModel
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<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
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<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>
在呈现包含 JavaScript 生成内容的页面时,应配置呈现器,等待脚本执行完毕后再捕获页面:
:path=/static-assets/pdf/content-code-examples/tutorials/crystal-reports-alternative-csharp/javascript-wait-rendering.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);
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)
输出示例
应用条件格式化和业务逻辑
可视化指示器可立即引起人们对需要采取行动的项目的注意,从而使库存报告受益匪浅。 条件格式化使例外情况一目了然,而不是强迫用户在数百行中寻找问题。 使用 Razor 的内联表达式根据数据值应用 CSS 类:
@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">
<span class="quantity-badge @(item.Quantity <= 5 ? "badge-critical" : "badge-ok")">
@item.Quantity
</span>
</td>
</tr>
}
@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">
<span class="quantity-badge @(item.Quantity <= 5 ? "badge-critical" : "badge-ok")">
@item.Quantity
</span>
</td>
</tr>
}
输出示例
创建子报告和分节符
若要将独立生成的报告合并为一个文档,请使用 IronPDF 的合并功能:
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;
}
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;
}
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
输出示例
生成目录
IronPDF 可根据 HTML 中的标题元素自动生成目录:
:path=/static-assets/pdf/content-code-examples/tutorials/crystal-reports-alternative-csharp/table-of-contents.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");
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")
从 Crystal Reports 迁移到 IronPDF.
迁移已建立的报告系统需要精心策划,以尽量减少中断,同时抓住机会实现现代化和简化。 通过了解 Crystal Reports 概念如何映射到基于 HTML 的方法,而不是试图从字面上复制每一个功能或保留原始报表的每一个怪癖,您的工作会进展得更快。
将 Crystal Reports 概念映射到 IronPDF.
了解概念映射有助于系统地翻译现有报告:
| Crystal Reports | IronPDF 同等产品 |
|---|---|
| 报告章节 | 带有 CSS 分页属性的 HTML divs |
| 参数字段 | 传递给 Razor 视图的模型属性 |
| 公式字段 | C# 模型类中的计算属性 |
| 运行总数 | LINQ 聚合 |
| 子报告 | 部分视图或合并 PDF 文档 |
| 分组/排序 | 将数据传递给模板之前的 LINQ 操作 |
| 交叉表报告 | 使用嵌套循环的 HTML 表格 |
| 条件格式 | 使用 CSS 类的 Razor @if 块 |
转换 .rpt 模板的最佳策略
请勿尝试以编程方式解析 .rpt 文件。 在翻译过程中,我们不能一概而论,而是要将现有的 PDF 输出视为可视化规范,并采用系统化的四步策略重建逻辑:
1.目录:列出所有 .rpt 文件的用途、数据来源和使用频率。 删除过时的报告,以缩小迁移范围。
2.优先考虑:首先迁移高频率报告。针对布局简单或存在持续维护问题的报告。
3.参考文献:将现有 Crystal Reports 导出为 PDF。 将这些内容作为视觉规范供开发人员匹配。
4.验证:使用生产数据量进行测试。 模板在渲染 10 行时速度很快,但在渲染 10,000 行时速度可能会变慢。
.NET 中的批量报告生成和调度
生产系统经常需要同时生成许多报告或按计划运行报告作业。 IronPDF 的线程安全设计可有效支持这两种情况。
并行生成多个报告
对于批处理,请使用 Parallel.ForEachAsync 或配合 Task.WhenAll 的异步模式:
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();
}
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();
}
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
输出示例
批处理示例可并行生成多张发票。 下面是生成的批量发票之一:
将报告生成与 ASP.NET Core 后台服务相结合
预定报告生成自然地融入了 ASP.NET Core 的托管服务基础架构:
// 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
}
}
}
// 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
}
}
}
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
下载完整测试项目
本教程中的所有代码示例都包含在一个可随时运行的 .NET 10 测试项目中。 下载包括完整的源代码、数据模型、HTML 模板和可生成上述所有 PDF 示例的测试运行程序。
下一步
本指南中的示例展示了 IronPDF可处理各种业务报告需求:简单的发票(包含细列项目和总数)、包含分组数据和照片的复杂员工目录、包含条件格式和图表的库存报告、并行处理数百份文档的批处理,以及通过后台服务计划生成。
如果您正在评估现有 Crystal Reports 实施的替代方案,请从单个高价值报表开始。 使用此处显示的HTML-to-PDF模式重建它,比较开发经验和输出质量,然后再进行扩展。 许多团队发现,在建立模式和基础模板后,他们的第一份转换报告只需要几个小时,而在重复使用 Razor 模板和样式后,后续报告的编写只需要几分钟。 关于布局精度,像素完美呈现指南涵盖了如何使用 CSS 精确匹配 Crystal Reports 输出。
准备好开始构建了吗? 下载IronPDF并试用免费试用版。 同一个库可以处理从单个报告渲染到跨 .NET 环境的 大批量批量生成的所有工作。 如果您对迁移报告有疑问或需要架构指导,请联系我们的工程支持团队。
常见问题解答
什么是IronPDF?
IronPDF 是一个 C# 库,使开发人员能够以编程方式创建、编辑和生成 PDF 文档,为 Crystal Reports 等传统报表工具提供了一个现代化的替代方案。
IronPDF 如何作为 Crystal Reports 的替代产品?
IronPDF 允许开发人员使用 HTML/CSS 模板,从而提供了一种灵活、现代的报表生成方法,这些模板可以轻松地进行样式设计和修改,而 Crystal Reports 的结构则较为僵化。
我可以使用 IronPDF 创建发票吗?
是的,您可以使用 IronPDF 的 HTML/CSS 模板创建详细的定制发票,轻松设计出专业外观的文档。
是否可以用 IronPDF 生成员工目录?
当然可以。IronPDF 允许您利用动态数据和 HTML/CSS 生成全面的员工目录,从而实现清晰有序的展示。
IronPDF 如何帮助编制库存报告?
IronPDF 可以通过使用 HTML/CSS 模板来简化库存报告的创建,这些模板可以动态填充数据,提供最新且具有视觉吸引力的报告。
在 IronPDF 中使用 HTML/CSS 模板有哪些优势?
在 IronPDF 中使用 HTML/CSS 模板可提供设计的灵活性、更新的便捷性以及与网络技术的兼容性,从而更容易维护和增强报告布局。
IronPDF 支持 .NET 10 吗?
是的,IronPDF 与 .NET 10 兼容,确保开发人员可以利用最新的 .NET 功能和改进来满足他们的报表生成需求。
IronPDF 如何提高报表生成速度?
IronPDF 对性能进行了优化,可以通过高效处理 HTML/CSS 并将其渲染为高质量的 PDF 文档来快速生成报告。

