在 C# 中以HTML轉PDF方式生成報表(替代 Crystal Reports,支援 .NET 10)

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

使用IronPDF在C# .NET產生 HTML 到 PDF 的報告,它用標準的HTML、CSS 和Razor範本取代了 Crystal Reports 的專有設計器,使.NET開發人員能夠利用他們已有的 Web 開發技能來建立資料驅動的業務報告。 這包括對動態表格、 JavaScript 驅動的圖表、條件格式、多文件批次處理以及在任何執行.NET 的環境中進行跨平台部署的全面支援。

TL;DR:快速入門指南

本教學涵蓋如何使用 C# .NET將 Crystal Reports 替換為HTML 到 PDF 的報表產生功能,從基本範本到批次處理和計畫產生。

-適用物件: .NET開發人員,用於取代 Crystal Reports 或從頭開始建立新的報表系統。 -你將建立的內容:三個完整的報表實作(銷售發票、員工目錄、庫存報表),以及 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)。 只需幾行程式碼即可產生您的第一份報告:

  1. 使用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

立即開始在您的項目中使用 IronPDF 並免費試用。

第一步:
green arrow pointer

目錄

C# Report Generator: HTML Templates to PDF

HTML 轉 PDF 的產生依賴線性架構流程。該應用程式不使用專有文件格式,而是使用標準資料模型來填入Razor視圖或 HTML 模板。 產生的 HTML 字串隨後被傳遞給IronPDF等渲染引擎,該引擎會將視覺輸出捕獲為 PDF 文件。 這種方法將報表設計與託管環境解耦,從而允許完全相同的程式碼在任何支援.NET 的平台上執行。

此工作流程與標準 Web 開發流程類似。 前端開發人員使用 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 元件,而 COM 元件與跨平台的.NET不相容。 支援現代.NET需要完全重寫,而 SAP 拒絕這樣做。

因此,在目前.NET版本上建立新應用程式的團隊無法使用 Crystal Reports。 採用.NET 8 或.NET 10 標準的組織無法將其整合。 對於現有應用程序,升級到現代.NET運行時需要先更換報表系統。

複雜的許可證制度和隱性成本

Crystal Reports 的授權方式分為設計器授權、執行階段授權、伺服器部署和嵌入式使用。 桌面、網頁和終端服務的規則各不相同。 一種設定下的合規要求在另一種設定下可能需要額外的許可證。 如果部署後出現漏洞,就會產生意想不到的成本。許多組織認為,這種不確定性比遷移到授權協議更清晰的解決方案更糟糕。

Windows 平台鎖定

Crystal Reports 只能在安裝了舊版.NET Framework的 Windows 系統上運作。 您無法將這些應用程式部署到 Linux 容器、Linux 上的 Azure 應用程式服務、AWS Lambda 或 Google Cloud Run。 隨著組織使用容器化、平台無關和無伺服器系統,這些限制變得更加重要。

建置微服務的開發團隊面臨額外的挑戰。 如果九個服務運行在輕量級 Linux 容器中,但其中一個服務需要 Windows 來執行 Crystal Reports,那麼部署就會更加複雜。 您需要 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# 的全部功能,包括 for 迴圈、條件語句、空值處理和字串格式化。 對於建立過ASP.NET Core應用程式的人來說,Razor 的語法非常熟悉,消除了與其他生態系統中的模板引擎相關的學習曲線。 模板與其他原始檔案一起存在於專案中,參與重構操作,並作為正常建置過程的一部分進行編譯。

對於較簡單的報表或希望將範本與.NET程式碼完全分開的團隊來說,使用字串插值的純 HTML效果很好。 HTML 範本可以作為嵌入式資源編譯到程式集中,也可以作為外部檔案與應用程式一起部署,甚至可以在執行時從資料庫或內容管理系統中檢索。基本資料綁定使用 string.Replace() 處理單一值,或使用像 Scriban 或 Fluid 這樣的輕量級範本庫來處理更高級的場景。 這種方法最大限度地提高了可移植性,允許設計人員在不安裝任何.NET工具的情況下編輯模板,只需使用文字編輯器和 Web 瀏覽器進行預覽即可。

混合方法結合了這兩種技術,適用於需要靈活性的場景。 例如,可以渲染Razor視圖來產生主要的 HTML 結構,然後進行後處理,新增字串替換以適應不適合視圖模型的動態元素。 或者,可以載入由非開發人員設計的 HTML 模板,並使用Razor局部視圖僅渲染複雜的、資料驅動的部分,然後再將所有內容組合起來。 HTML 轉換 PDF 轉換與 HTML 來源無關,您可以根據每個報告的需求混合使用不同的方法。

鑑於這些選項,本教學主要專注於Razor視圖,因為它們在典型的業務報告場景中提供了類型安全、可維護性和功能豐富性的最佳平衡。 如果未來需要使用純 HTML 模板,這些技能可以直接轉移,因為這兩種方法都會產生 HTML 字串。

Build a Data-Driven PDF Report in C

本節示範了從頭到尾建立銷售發票報表的完整流程。 此範例涵蓋了所有報告使用的基本模式:定義一個用於建立資料的模型,建立一個Razor範本將資料轉換為格式化的 HTML,將該範本渲染為 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 功能,例如 flexbox、網格佈局和 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

可用的合併欄位包括:{date}@ 和 {time}(產生時間戳記)、{url}6-195--@DE-195--D-- 網頁、@" {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 分頁屬性的 HTML div
參數字段 傳遞給Razor視圖的模型屬性
公式字段 模型類別中的 C# 計算屬性
累計總分 LINQ 聚合
子報告 部分視圖或合併的 PDF 文檔
分組/排序 在將資料傳遞給模板之前執行 LINQ 操作
交叉表報告 使用巢狀循環的HTML表格
條件格式 Razor @if 程式碼區塊及其 CSS 類

轉換 .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 轉 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 範本建立詳細且可自訂的發票,從而輕鬆設計出專業外觀的文件。

是否可以使用 IronPDF 產生員工名錄?

當然。 IronPDF 利用動態資料和 HTML/CSS,可以產生全面的員工名錄,呈現清晰有序的內容。

IronPDF 如何幫助產生庫存報告?

IronPDF 可以使用 HTML/CSS 範本簡化庫存報告的創建,這些範本可以動態填充數據,從而提供最新且視覺效果吸引人的報告。

在 IronPDF 中使用 HTML/CSS 範本有哪些優勢?

在 IronPDF 中使用 HTML/CSS 範本可以提供設計上的靈活性、更新的便利性以及與 Web 技術的兼容性,更容易維護和改進報告佈局。

IronPDF 是否支援 .NET 10?

是的,IronPDF 與 .NET 10 相容,確保開發人員可以利用最新的 .NET 功能和改進來滿足其報表產生需求。

IronPDF 如何提升報告產生速度?

IronPDF 針對效能進行了最佳化,能夠有效地處理 HTML/CSS 並將其渲染成高品質的 PDF 文檔,從而快速產生報告。

Curtis Chau
技術作家

Curtis Chau 擁有卡爾頓大學計算機科學學士學位,專注於前端開發,擅長於 Node.js、TypeScript、JavaScript 和 React。Curtis 熱衷於創建直觀且美觀的用戶界面,喜歡使用現代框架並打造結構良好、視覺吸引人的手冊。

除了開發之外,Curtis 對物聯網 (IoT) 有著濃厚的興趣,探索將硬體和軟體結合的創新方式。在閒暇時間,他喜愛遊戲並構建 Discord 機器人,結合科技與創意的樂趣。

準備好開始了嗎?
Nuget 下載 18,120,209 | 版本: 2026.4 剛剛發布
Still Scrolling Icon

還在捲動嗎?

想要快速證明? PM > Install-Package IronPdf
執行範例 觀看您的 HTML 變成 PDF。