Generate Reports in C# Like Crystal Reports (.NET 10)

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

using IronPDF 在 C# .NET 中將 HTML 轉為 PDF 報告,可將 Crystal Reports 的專有 .rpt 設計器替換為標準的 HTML、CSS 和 Razor 範本,讓 .NET 開發人員能運用現有的網頁開發技能,建立資料驅動的商業報告。 這包括對動態表格、JavaScript 驅動的圖表、條件格式化、多文件批次處理,以及在任何運行 .NET 的環境中進行跨平台部署的全面支援。

TL;DR:快速入門指南

本教學將介紹如何在 C# .NET 中以 HTML 轉 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。 只需幾行程式碼,即可生成您的第一份報告:

  1. using NuGet 套件管理員安裝 https://www.nuget.org/packages/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

請至 NuGet 查閱 https://www.nuget.org/packages/IronPdf 以快速安裝。該套件下載量已突破 1,000 萬次,正透過 C# 徹底改變 PDF 開發領域。 您亦可下載 DLL 檔案或 Windows 安裝程式

目錄

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 Standard 8 或 .NET Standard 10 作為標準平台的組織無法進行整合。 對於現有應用程式,若要升級至現代化的 .NET 執行環境,必須先更換報表系統。

複雜的授權條款與隱藏成本

Crystal Reports 的授權模式區分為設計者授權、執行時授權、伺服器部署及嵌入式使用。 桌面版、網頁版及終端服務的規則各不相同。 某種設定下的合規性,在另一種設定下可能需要額外的授權。 若在部署後出現漏洞,將導致意外成本。許多組織認為,這種不確定性比遷移至授權條款更明確的解決方案還要糟糕。

僅限 Windows 的平台鎖定

Crystal Reports 僅能在搭載舊版 .NET Framework 的 Windows 系統上運行。 您無法將這些應用程式部署至 Linux 容器、基於 Linux 的 Azure App Service、AWS Lambda 或 Google Cloud Run。 隨著組織採用容器化、平台無關及無伺服器系統,這些限制變得愈發重要。

正在建置微服務的開發團隊面臨額外的挑戰。 若九項服務在輕量級 Linux 容器中運行,但其中一項因需使用 Crystal Reports 而必須在 Windows 環境下運作,部署過程便會更加複雜。 您需要 Windows 容器映像檔、與 Windows 相容的主機服務,以及獨立的部署設定。 報告服務成為例外,阻礙了標準化。

Set Up a C# Report Generator in .NET 10

開始使用 IronPDF 非常簡單。 請透過 NuGet 安裝此函式庫,如同安裝其他 .NET 依賴項一樣。 無需額外下載軟體,亦無需為生產伺服器安裝獨立的執行環境安裝程式。

選擇範本方式:Razor、HTML 或混合式

IronPDF 支援三種不同的報表範本建置方式。 每種方法皆具備特定優勢,具體取決於團隊組成、專案需求以及長期維護考量。

Razor Views 為已在 .NET 生態系統中工作的團隊提供最豐富的開發體驗。 提供在 Visual Studio 和 VS Code 中具備完整 IntelliSense 支援的強類型模型、編譯時檢查,以及 C# 所具備的完整功能,包括迴圈、條件語句、空值處理和字串格式化。 對於曾開發過 .NET Core 應用程式的人來說,Razor 的語法相當熟悉,因此無需像使用其他生態系統的模板引擎那樣經歷學習曲線。 這些範本與其他原始檔一同存放於專案中,會參與重構操作,並作為常規建置流程的一部分進行編譯。

對於較簡單的報表,或偏好將範本與 .NET 程式碼完全分離的團隊而言,採用字串插值功能的純 HTML 格式效果甚佳。 HTML 範本可儲存為編譯至程序集中的內嵌資源、與應用程式一同部署的外部檔案,甚至可在執行時從資料庫或內容管理系統中擷取。基本資料綁定使用 string.Replace() 處理單一值,或採用 Scriban 或 Fluid 等輕量級範本庫來處理更進階的場景。 此方法能最大化可移植性,讓設計師無需安裝任何 .NET 工具即可編輯範本,僅需使用文字編輯器和網頁瀏覽器進行預覽。

混合式方法結合了這兩種技術,適用於需要靈活性的情境。 例如,Razor 檢視頁面可能會被渲染以產生主要 HTML 結構,隨後針對無法完美融入檢視模型的動態元素,進行額外的字串替換後處理。 此外,亦可載入由非開發者設計的 HTML 範本,並利用 Razor 局部檢視(partial views)僅渲染複雜的資料驅動區塊,最後再將所有內容整合。 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
$vbLabelText   $csharpLabel

模型中已包含 TaxAmountGrandTotal 的計算屬性。 這些計算應置於模型中而非範本中,讓 Razor 檢視專注於呈現,而由模型處理業務邏輯。 這種分離使單元測試變得簡單明瞭,讓計算結果能在不渲染任何 HTML 的情況下進行驗證。

現在請建立一個 Razor 檢視,將此模型轉換為格式 Professional 的發票。請將其儲存至 Views 資料夾中,並命名為 InvoiceTemplate.cshtml

@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.Com/pany.Name</h1>
                <p>@Model.Com/pany.Address</p>
                <p>@Model.Com/pany.City</p>
                <p>@Model.Com/pany.Phone | @Model.Com/pany.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.Com/pany.Name</h1>
                <p>@Model.Com/pany.Address</p>
                <p>@Model.Com/pany.City</p>
                <p>@Model.Com/pany.Phone | @Model.Com/pany.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 亦支援 flexbox、網格佈局及 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 表示動態部分。

翻譯範例

添加頁首、頁尾和頁碼

Professional報告通常會在所有頁面中使用一致的頁首與頁尾,顯示公司品牌標識、文件標題、生成日期及頁碼。 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} 代表來源網址(若從網頁渲染),以及 {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 的選取器,會強制將第一個部門之後的每個部門都置於新頁面上,從而使整個目錄中各區段的分隔清晰明確。

翻譯範例

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>
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 page-break 屬性的 HTML div 元素
參數欄位 傳遞至 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.Co/llections.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.Co/llections.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 轉 PDF 範例進行重建,比較開發體驗與輸出品質,並以此為基礎進一步擴展。 許多團隊發現,首次轉換報告時,由於需要建立模式和基礎範本,通常需耗時數小時;但隨後報告只需數分鐘即可完成,因為他們會重複使用 Razor 範本和樣式。 為確保版面精準度,像素級精準渲染指南詳述了如何讓 Crystal Reports 輸出與 CSS 完全對應。

準備開始建置了嗎? 下載 IronPDF 並透過免費試用版體驗其功能。 同一個函式庫可處理從單一報表渲染到跨 .NET 環境的大量批次生成等所有需求。 若您對遷移報告有任何疑問,或需要架構方面的指導,請聯繫我們的工程支援團隊

常見問題

什麼是 IronPDF?

IronPDF 是一款 C# 函式庫,讓開發人員能夠透過程式化方式建立、編輯及產生 PDF 文件,為 Crystal Reports 等傳統報表工具提供了一種現代化的替代方案。

IronPDF 如何作為 Crystal Reports 的替代方案?

IronPDF 提供了一種靈活且現代的報表生成方式,允許開發人員使用 HTML/CSS 範本,這些範本可輕鬆進行樣式設定與修改,這與 Crystal Reports 較為僵化的結構形成鮮明對比。

我可以使用 IronPDF 來建立發票嗎?

是的,您可以使用 IronPDF 的 HTML/CSS 範來建立詳細且客製化的發票,輕鬆設計出 Professional 外觀的文件。

是否可以使用 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 擁有卡爾頓大學(Carleton University)的電腦科學學士學位,專精於前端開發,並精通 Node.js、TypeScript、JavaScript 及 React。他熱衷於打造直觀且美觀的用戶介面,喜歡運用現代框架,並創建結構完善、視覺上吸引人的手冊。

除了開發工作之外,Curtis 對物聯網(IoT)抱有濃厚興趣,致力於探索整合硬體與軟體的創新方法。閒暇時,他喜歡玩遊戲和開發 Discord 機器人,將對科技的熱愛與創意相結合。

準備開始了嗎?
Nuget 下載 18,918,602 | 版本: 2026.5 just released
Still Scrolling Icon

還在往下捲動嗎?

想要快速確認成果嗎? PM > Install-Package IronPdf
執行範例 觀看您的 HTML 轉為 PDF。