在 C# 中生成报表;像 Crystal Reports (.NET 10)

This article was translated from English: Does it need improvement?
Translated
View the article in English

使用 IronPDF 在 C# .NET 中生成HTML-to-PDF 报表,用标准的 HTML、CSS 和 Razor 模板取代 Crystal Reports 专有的 .rpt 设计器,使 .NET 开发人员能够使用已有的 Web 开发技能构建数据驱动的业务报表。 这包括完全支持动态表格、JavaScript 驱动的图表、条件格式化、多文档批处理以及在任何运行 .NET 的环境中进行跨平台部署。

as-heading:2(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 安装 IronPdf(Install-Package IronPdf)。 只需几行代码即可生成您的第一份报告:

Nuget Icon立即开始使用 NuGet 创建 PDF 文件:

  1. 使用 NuGet 包管理器安装 IronPDF

    PM > Install-Package IronPdf

  2. 复制并运行这段代码。

    // 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");
  3. 部署到您的生产环境中进行测试

    立即开始在您的项目中使用 IronPDF,免费试用!
    arrow pointer

购买或注册 IronPDF 30 天试用版后,请在应用程序的开头添加许可证密钥。

IronPdf.License.LicenseKey = "KEY";
IronPdf.License.LicenseKey = "KEY";
Imports IronPdf

IronPdf.License.LicenseKey = "KEY"
$vbLabelText   $csharpLabel

今天在您的项目中使用 IronPDF,免费试用。

第一步:
green arrow pointer
NuGet 使用 NuGet 安装

PM >  Install-Package IronPdf

IronPDF 上查看 NuGet 快速安装。超过 1000 万次下载,它正以 C# 改变 PDF 开发。 您也可以下载 DLLWindows 安装程序

as-heading:2(目录)

C#报告生成器:将 HTML 模板转换为 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 兼容主机和单独的部署设置。 报告服务成为例外,阻碍了标准化。

在 .NET 10 中设置 C# 和 num; 报告生成器

开始使用 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 字符串。

在 C# 中构建数据驱动的 PDF 报告;

本节演示了从头到尾创建销售发票报告的全过程。 该示例涵盖了所有报告使用的基本模式:定义一个数据结构模型,创建一个将数据转换为格式化 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
$vbLabelText   $csharpLabel

模型中包含小计、税额和总计的计算属性。 这些计算属于模型而不是模板,使 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>
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
$vbLabelText   $csharpLabel

对于更简单的场景,当您不需要完整的 ASP.NET Core MVC 设置时,比如在控制台应用程序或后台服务中,您可以只使用带插值的 HTML 字符串和 StringBuilder 来处理动态部分。

输出示例

添加页眉、页脚和页码

专业报告通常包括所有页面一致的页眉和页脚,显示公司品牌、文档标题、生成日期和页码。 IronPDF 提供了两种实现这些元素的方法:基于文本的标题,用于需要最少格式化的简单内容;HTML 标题,用于带有徽标和自定义布局的完全样式控制。

基于文本的标题可以很好地提供基本信息,而且由于不需要额外的 HTML 解析,因此渲染速度更快:

: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
$vbLabelText   $csharpLabel

可用的合并字段包括:{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
}
$vbLabelText   $csharpLabel

输出示例

创建动态表格和重复章节

报告通常需要显示跨越多个页面的数据集合。 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
$vbLabelText   $csharpLabel

部门类上的 CSS 属性page-break-inside: avoid告诉 PDF 渲染器尽可能将部门章节集中在一页上。 如果一个部门的内容会导致中途分页,渲染器会将整个部分移到下一页。 选择器.department:not(:first-child)page-break-before: always会强制第一个部门之后的每个部门从新页面开始,从而在整个目录中创建清晰的分区。

输出示例

使用 IronPDF 生成高级 C# 报告。

商业报告经常需要超越静态表格和文本的功能。 图表可以直观地反映趋势,而表格的形式则难以理解。 条件格式化可引起对需要采取行动的项目的注意。 子报告将多个来源的数据合并成具有凝聚力的文件。 本节内容包括使用 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>
HTML

在呈现包含 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)
$vbLabelText   $csharpLabel

输出示例

应用条件格式化和业务逻辑

可视化指示器可立即引起人们对需要采取行动的项目的注意,从而使库存报告受益匪浅。 条件格式化使例外情况一目了然,而不是强迫用户在数百行中寻找问题。 使用 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>
}
HTML

输出示例

创建子报告和分节符

若要将独立生成的报告合并为一个文档,请使用 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
$vbLabelText   $csharpLabel

输出示例

生成目录

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")
$vbLabelText   $csharpLabel

从 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
$vbLabelText   $csharpLabel

输出示例

批处理示例可并行生成多张发票。 下面是生成的批量发票之一:

将报告生成与 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
$vbLabelText   $csharpLabel

下载完整测试项目

本教程中的所有代码示例都包含在一个可随时运行的 .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 文档来快速生成报告。

Curtis Chau
技术作家

Curtis Chau 拥有卡尔顿大学的计算机科学学士学位,专注于前端开发,精通 Node.js、TypeScript、JavaScript 和 React。他热衷于打造直观且美观的用户界面,喜欢使用现代框架并创建结构良好、视觉吸引力强的手册。

除了开发之外,Curtis 对物联网 (IoT) 有浓厚的兴趣,探索将硬件和软件集成的新方法。在空闲时间,他喜欢玩游戏和构建 Discord 机器人,将他对技术的热爱与创造力相结合。

准备开始了吗?
Nuget 下载 17,386,124 | 版本: 2026.2 刚刚发布