如何使用Razor在C#中无头地将CSHTML转换为PDF
Fluid是一个.NET库(在NuGet上作为Fluid.Core发布,由Sebastien Ros编写,采用MIT许可)实现了Liquid模板语言,为开发者提供了一种灵活的方式来渲染动态模板并将内容与表现逻辑分离。 Fluid在生成动态文本输出时很有效,但它不直接支持PDF生成——开发者必须集成一个额外的PDF库来将HTML输出转换为PDF文档。 这种双库方法带来了复杂性,而许多开发团队都希望消除这种复杂性。
本指南提供了从使用外部 PDF 库的 Fluid(模板化)到IronPDF的完整迁移路径,为评估这一过渡的 .NET 专业开发人员提供了分步说明、代码比较和实用示例。
为什么要从 Fluid(模板化)迁移到 IronPDF.
Fluid 是一个基于 Liquid 的优秀模板引擎,但将其用于 PDF 生成会引入显著的复杂性:
双库依赖: Fluid 只生成 HTML——你需要一个单独的 PDF 库(wkhtmltopdf、PuppeteerSharp 等)来创建 PDF,这使得你的依赖项和维护负担翻倍。
集成复杂性:协调两个库意味着管理两套配置、错误处理和更新。 当出现故障时,调试变得更具挑战性。
Liquid语法学习曲线:开发人员必须学习Liquid模板语法({{ }}, {% %}),而C#已经内置了强大的字符串处理功能。
PDF 控制有限:您的 PDF 输出质量取决于您选择与 Fluid 搭配使用的 PDF 库,而不是专用的渲染引擎。
调试挑战:模板生成或 PDF 生成阶段都可能出现错误,这使得故障排除比使用单一集成解决方案更加困难。
线程安全问题:TemplateContext不是线程安全的,需要在并发应用中小心管理。
IronPDFvs Fluid(模板化):功能比较
了解架构差异有助于技术决策者评估迁移投资:
| 方面 | 流体 + PDF 库 | IronPDF |
|---|---|---|
| 依赖关系 | 2 个以上软件包(Fluid + PDF 库) | 单个软件包 |
| 模板 | Liquid语法({{ }}) |
C# 字符串插值或 Razor |
| PDF 生成 | 需要外部库 | 内置 Chromium 引擎 |
| CSS支持 | 取决于 PDF 库 | 带有 Flexbox/Grid 的完整 CSS3 |
| JavaScript语言 | 取决于 PDF 库 | 完全支持 JavaScript |
| 线程安全 | 模板上下文不是线程安全的 | ChromePdfRenderer 是线程安全的 |
| 学习曲线 | Liquid + PDF 库 API | HTML/CSS(网络标准) |
| 错误处理 | 两个错误源 | 单一错误源 |
快速入门:从 Fluid 迁移到 IronPDF.
迁移工作可以通过以下基本步骤立即开始。
步骤 1:替换 NuGet 软件包
移除 Fluid 和任何外部 PDF 库:
# Remove Fluid and external PDF library
dotnet remove package Fluid.Core
dotnet remove package WkHtmlToPdf-DotNet # or whatever PDF library you used
dotnet remove package PuppeteerSharp # if used
# Remove Fluid and external PDF library
dotnet remove package Fluid.Core
dotnet remove package WkHtmlToPdf-DotNet # or whatever PDF library you used
dotnet remove package PuppeteerSharp # if used
安装 IronPDF:
# InstallIronPDF(all-in-one solution)
dotnet add package IronPdf
# InstallIronPDF(all-in-one solution)
dotnet add package IronPdf
步骤 2:更新命名空间
将Fluid命名空间替换为IronPDF:
// Before (Fluid + external PDF library)
using Fluid;
using Fluid.Values;
using SomeExternalPdfLibrary;
// After (IronPDF)
using IronPdf;
using IronPdf.Rendering; // For RenderingOptions
// Before (Fluid + external PDF library)
using Fluid;
using Fluid.Values;
using SomeExternalPdfLibrary;
// After (IronPDF)
using IronPdf;
using IronPdf.Rendering; // For RenderingOptions
Imports Fluid
Imports Fluid.Values
Imports SomeExternalPdfLibrary
Imports IronPdf
Imports IronPdf.Rendering ' For RenderingOptions
步骤 3:初始化许可证
在应用程序启动时添加许可证初始化:
IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";
IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";
IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY"
代码迁移示例
将基本 HTML 转换为 PDF.
最基本的操作揭示了这些方法之间的关键区别。
流畅的方法:
// NuGet: Install-Package Fluid.Core
using Fluid;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var parser = new FluidParser();
var template = parser.Parse("<html><body><h1>Hello {{name}}!</h1></body></html>");
var context = new TemplateContext();
context.SetValue("name", "World");
var html = await template.RenderAsync(context);
// Fluid only generates HTML - you'd need another library to convert to PDF
File.WriteAllText("output.html", html);
}
}
// NuGet: Install-Package Fluid.Core
using Fluid;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var parser = new FluidParser();
var template = parser.Parse("<html><body><h1>Hello {{name}}!</h1></body></html>");
var context = new TemplateContext();
context.SetValue("name", "World");
var html = await template.RenderAsync(context);
// Fluid only generates HTML - you'd need another library to convert to PDF
File.WriteAllText("output.html", html);
}
}
Imports Fluid
Imports System.IO
Imports System.Threading.Tasks
Module Program
Async Function Main() As Task
Dim parser As New FluidParser()
Dim template = parser.Parse("<html><body><h1>Hello {{name}}!</h1></body></html>")
Dim context As New TemplateContext()
context.SetValue("name", "World")
Dim html = Await template.RenderAsync(context)
' Fluid only generates HTML - you'd need another library to convert to PDF
File.WriteAllText("output.html", html)
End Function
End Module
IronPDF 方法:
// NuGet: Install-Package IronPdf
using IronPdf;
using System;
class Program
{
static void Main()
{
var renderer = new ChromePdfRenderer();
var html = "<html><body><h1>Hello World!</h1></body></html>";
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("output.pdf");
}
}
// NuGet: Install-Package IronPdf
using IronPdf;
using System;
class Program
{
static void Main()
{
var renderer = new ChromePdfRenderer();
var html = "<html><body><h1>Hello World!</h1></body></html>";
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("output.pdf");
}
}
Imports IronPdf
Imports System
Class Program
Shared Sub Main()
Dim renderer = New ChromePdfRenderer()
Dim html = "<html><body><h1>Hello World!</h1></body></html>"
Dim pdf = renderer.RenderHtmlAsPdf(html)
pdf.SaveAs("output.pdf")
End Sub
End Class
Fluid需要创建一个SetValue(),异步渲染以获取HTML,然后写入文件——这仍然不是PDF。 代码中的注释明确指出 "Fluid 只能生成 HTML - 您需要另一个库来转换为 PDF"。
IronPDF消除了这种复杂性:创建一个渲染器,调用RenderHtmlAsPdf(),直接保存为PDF。 无中间 HTML 文件,无附加库。
有关 HTML 转 PDF 的高级应用场景,请参阅 HTML 转 PDF 指南。
带动态数据的发票模板
带有多个变量的文档模板可以清楚地显示模板模式的差异。
流畅的方法:
// NuGet: Install-Package Fluid.Core
using Fluid;
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var parser = new FluidParser();
var template = parser.Parse(@"
<html><body>
<h1>Invoice #{{invoiceNumber}}</h1>
<p>Date: {{date}}</p>
<p>Customer: {{customer}}</p>
<p>Total: ${{total}}</p>
</body></html>");
var context = new TemplateContext();
context.SetValue("invoiceNumber", "12345");
context.SetValue("date", DateTime.Now.ToShortDateString());
context.SetValue("customer", "John Doe");
context.SetValue("total", 599.99);
var html = await template.RenderAsync(context);
// Fluid outputs HTML - requires additional PDF library
File.WriteAllText("invoice.html", html);
}
}
// NuGet: Install-Package Fluid.Core
using Fluid;
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var parser = new FluidParser();
var template = parser.Parse(@"
<html><body>
<h1>Invoice #{{invoiceNumber}}</h1>
<p>Date: {{date}}</p>
<p>Customer: {{customer}}</p>
<p>Total: ${{total}}</p>
</body></html>");
var context = new TemplateContext();
context.SetValue("invoiceNumber", "12345");
context.SetValue("date", DateTime.Now.ToShortDateString());
context.SetValue("customer", "John Doe");
context.SetValue("total", 599.99);
var html = await template.RenderAsync(context);
// Fluid outputs HTML - requires additional PDF library
File.WriteAllText("invoice.html", html);
}
}
Imports Fluid
Imports System
Imports System.IO
Imports System.Threading.Tasks
Module Program
Async Function Main() As Task
Dim parser As New FluidParser()
Dim template = parser.Parse("
<html><body>
<h1>Invoice #{{invoiceNumber}}</h1>
<p>Date: {{date}}</p>
<p>Customer: {{customer}}</p>
<p>Total: ${{total}}</p>
</body></html>")
Dim context As New TemplateContext()
context.SetValue("invoiceNumber", "12345")
context.SetValue("date", DateTime.Now.ToShortDateString())
context.SetValue("customer", "John Doe")
context.SetValue("total", 599.99)
Dim html = Await template.RenderAsync(context)
' Fluid outputs HTML - requires additional PDF library
File.WriteAllText("invoice.html", html)
End Function
End Module
IronPDF 方法:
// NuGet: Install-Package IronPdf
using IronPdf;
using System;
class Program
{
static void Main()
{
var renderer = new ChromePdfRenderer();
var invoiceNumber = "12345";
var date = DateTime.Now.ToShortDateString();
var customer = "John Doe";
var total = 599.99;
var html = $@"
<html><body>
<h1>Invoice #{invoiceNumber}</h1>
<p>Date: {date}</p>
<p>Customer: {customer}</p>
<p>Total: ${total}</p>
</body></html>";
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("invoice.pdf");
}
}
// NuGet: Install-Package IronPdf
using IronPdf;
using System;
class Program
{
static void Main()
{
var renderer = new ChromePdfRenderer();
var invoiceNumber = "12345";
var date = DateTime.Now.ToShortDateString();
var customer = "John Doe";
var total = 599.99;
var html = $@"
<html><body>
<h1>Invoice #{invoiceNumber}</h1>
<p>Date: {date}</p>
<p>Customer: {customer}</p>
<p>Total: ${total}</p>
</body></html>";
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("invoice.pdf");
}
}
Imports IronPdf
Imports System
Module Program
Sub Main()
Dim renderer As New ChromePdfRenderer()
Dim invoiceNumber As String = "12345"
Dim [date] As String = DateTime.Now.ToShortDateString()
Dim customer As String = "John Doe"
Dim total As Double = 599.99
Dim html As String = $"
<html><body>
<h1>Invoice #{invoiceNumber}</h1>
<p>Date: {[date]}</p>
<p>Customer: {customer}</p>
<p>Total: ${total}</p>
</body></html>"
Dim pdf = renderer.RenderHtmlAsPdf(html)
pdf.SaveAs("invoice.pdf")
End Sub
End Module
Fluid使用Liquid的context.SetValue()。 注释明确指出"Fluid输出HTML - 需要额外的PDF库。" IronPDF使用标准C#字符串插值($"{variable}")——开发人员已经熟悉的语法——并直接输出至PDF。
探索 IronPDF 教程,了解更多文档生成模式。
使用循环的动态数据
带有集合和循环的模板展示了控制流的差异。
流畅的方法:
// NuGet: Install-Package Fluid.Core
using Fluid;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var parser = new FluidParser();
var template = parser.Parse(@"
<html><body>
<h1>{{title}}</h1>
<ul>
{% for item in items %}
<li>{{item}}</li>
{% endfor %}
</ul>
</body></html>");
var context = new TemplateContext();
context.SetValue("title", "My List");
context.SetValue("items", new[] { "Item 1", "Item 2", "Item 3" });
var html = await template.RenderAsync(context);
// Fluid generates HTML only - separate PDF conversion needed
File.WriteAllText("template-output.html", html);
}
}
// NuGet: Install-Package Fluid.Core
using Fluid;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var parser = new FluidParser();
var template = parser.Parse(@"
<html><body>
<h1>{{title}}</h1>
<ul>
{% for item in items %}
<li>{{item}}</li>
{% endfor %}
</ul>
</body></html>");
var context = new TemplateContext();
context.SetValue("title", "My List");
context.SetValue("items", new[] { "Item 1", "Item 2", "Item 3" });
var html = await template.RenderAsync(context);
// Fluid generates HTML only - separate PDF conversion needed
File.WriteAllText("template-output.html", html);
}
}
Imports Fluid
Imports System.Collections.Generic
Imports System.IO
Imports System.Threading.Tasks
Class Program
Shared Async Function Main() As Task
Dim parser As New FluidParser()
Dim template = parser.Parse("
<html><body>
<h1>{{title}}</h1>
<ul>
{% for item in items %}
<li>{{item}}</li>
{% endfor %}
</ul>
</body></html>")
Dim context As New TemplateContext()
context.SetValue("title", "My List")
context.SetValue("items", New String() {"Item 1", "Item 2", "Item 3"})
Dim html = Await template.RenderAsync(context)
' Fluid generates HTML only - separate PDF conversion needed
File.WriteAllText("template-output.html", html)
End Function
End Class
IronPDF 方法:
// NuGet: Install-Package IronPdf
using IronPdf;
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var renderer = new ChromePdfRenderer();
var title = "My List";
var items = new[] { "Item 1", "Item 2", "Item 3" };
var html = $@"
<html><body>
<h1>{title}</h1>
<ul>";
foreach (var item in items)
{
html += $"<li>{item}</li>";
}
html += "</ul></body></html>";
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("template-output.pdf");
}
}
// NuGet: Install-Package IronPdf
using IronPdf;
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var renderer = new ChromePdfRenderer();
var title = "My List";
var items = new[] { "Item 1", "Item 2", "Item 3" };
var html = $@"
<html><body>
<h1>{title}</h1>
<ul>";
foreach (var item in items)
{
html += $"<li>{item}</li>";
}
html += "</ul></body></html>";
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("template-output.pdf");
}
}
Imports IronPdf
Imports System
Imports System.Collections.Generic
Module Program
Sub Main()
Dim renderer As New ChromePdfRenderer()
Dim title As String = "My List"
Dim items As String() = {"Item 1", "Item 2", "Item 3"}
Dim html As String = $"
<html><body>
<h1>{title}</h1>
<ul>"
For Each item As String In items
html += $"<li>{item}</li>"
Next
html += "</ul></body></html>"
Dim pdf = renderer.RenderHtmlAsPdf(html)
pdf.SaveAs("template-output.pdf")
End Sub
End Module
Fluid使用Liquid的{% for item in items %}...{% endfor %}语法——开发人员必须学习的一种模板语言。 注释指出"Fluid仅生成HTML - 需要单独的PDF转换。" IronPDF使用标准C# foreach 循环——无需学习新语法——直接输出至PDF。
Fluid API 到IronPDF映射参考
这种映射通过显示直接的 API 对应关系来加速迁移:
核心类映射
| 流体类 | IronPDF 同等产品 |
|---|---|
FluidParser |
不适用 |
FluidTemplate |
不适用 |
TemplateContext |
C# 对象/字符串 |
TemplateOptions |
RenderingOptions |
FluidValue |
本地 C# 类型 |
| 外部 PDF 类 | ChromePdfRenderer |
方法映射
| 流体方法 | IronPDF 同等产品 |
|---|---|
new FluidParser() |
new ChromePdfRenderer() |
parser.Parse(source) |
不适用 |
template.RenderAsync(context) |
renderer.RenderHtmlAsPdf(html) |
context.SetValue("key", value) |
var key = value; |
Liquid语法到C#映射
| 液体语法 | C# 对等语 |
|---|---|
{{variable}} |
$"{variable}" |
{% for item in items %} |
foreach (var item in items) |
{% if condition %} |
if (condition) |
{{x |上例}} |
x.ToUpper() |
{{x |date: '%Y-%m-%d'}} |
x.ToString("yyyy-MM-dd") |
{{x |number_with_precision: 2}} |
x.ToString("F2") |
常见迁移问题和解决方案
问题 1:液体语法转换
Fluid:使用{% control %}语法。
解决方案:替换为 C# 字符串插值和控制流:
// Liquid: {{name | upcase}}
// C#: $"{name.ToUpper()}"
// Liquid: {% for item in items %}{{item}}{% endfor %}
// C#: foreach (var item in items) { html += $"{item}"; }
// Liquid: {{name | upcase}}
// C#: $"{name.ToUpper()}"
// Liquid: {% for item in items %}{{item}}{% endfor %}
// C#: foreach (var item in items) { html += $"{item}"; }
' Liquid: {{name | upcase}}
' C#: $"{name.ToUpper()}"
Dim result As String = name.ToUpper()
' Liquid: {% for item in items %}{{item}}{% endfor %}
' C#: foreach (var item in items) { html += $"{item}"; }
For Each item In items
html &= item.ToString()
Next
问题 2:模板上下文变量
Fluid:使用context.SetValue("key", value)传递数据。
解决方案:使用标准 C# 变量:
// Before (Fluid)
var context = new TemplateContext();
context.SetValue("customer", customerName);
// After (IronPDF)
var customer = customerName;
var html = $"<p>Customer: {customer}</p>";
// Before (Fluid)
var context = new TemplateContext();
context.SetValue("customer", customerName);
// After (IronPDF)
var customer = customerName;
var html = $"<p>Customer: {customer}</p>";
' Before (Fluid)
Dim context As New TemplateContext()
context.SetValue("customer", customerName)
' After (IronPDF)
Dim customer = customerName
Dim html = $"<p>Customer: {customer}</p>"
第 3 期:线程安全
Fluid:TemplateContext不是线程安全的,需要在并发应用中小心管理。
解决方案:ChromePdfRenderer是线程安全的,可以在线程间共享。
// Thread-safe usage
private static readonly ChromePdfRenderer _renderer = new ChromePdfRenderer();
public byte[] GeneratePdf(string html)
{
var pdf = _renderer.RenderHtmlAsPdf(html);
return pdf.BinaryData;
}
// Thread-safe usage
private static readonly ChromePdfRenderer _renderer = new ChromePdfRenderer();
public byte[] GeneratePdf(string html)
{
var pdf = _renderer.RenderHtmlAsPdf(html);
return pdf.BinaryData;
}
' Thread-safe usage
Private Shared ReadOnly _renderer As New ChromePdfRenderer()
Public Function GeneratePdf(html As String) As Byte()
Dim pdf = _renderer.RenderHtmlAsPdf(html)
Return pdf.BinaryData
End Function
第 4 期:两阶段错误处理
流畅:错误可能发生在模板阶段或 PDF 生成阶段。
解决方案:IronPDF只有一个错误源:
try
{
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("output.pdf");
}
catch (Exception ex)
{
// Single point of failure—easier debugging
Console.WriteLine($"PDF generation failed: {ex.Message}");
}
try
{
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("output.pdf");
}
catch (Exception ex)
{
// Single point of failure—easier debugging
Console.WriteLine($"PDF generation failed: {ex.Message}");
}
Try
Dim pdf = renderer.RenderHtmlAsPdf(html)
pdf.SaveAs("output.pdf")
Catch ex As Exception
' Single point of failure—easier debugging
Console.WriteLine($"PDF generation failed: {ex.Message}")
End Try
流体迁移核对表
迁移前任务
审核您的代码库,确定所有 Fluid 使用情况:
# Find all Fluid references
grep -r "FluidParser\|FluidTemplate\|TemplateContext\|using Fluid" --include="*.cs" --include="*.csproj" .
# Find Liquid template files
find . -name "*.liquid" -o -name "*.html" | xargs grep -l "{{"
# Find all Fluid references
grep -r "FluidParser\|FluidTemplate\|TemplateContext\|using Fluid" --include="*.cs" --include="*.csproj" .
# Find Liquid template files
find . -name "*.liquid" -o -name "*.html" | xargs grep -l "{{"
记录所有模板:文件位置、使用的变量、循环和条件以及外部 PDF 库配置。
代码更新任务
1.删除 Fluid.Core NuGet 软件包 2.删除外部 PDF 库包
- 安装IronPDF NuGet包
- 将命名空间导入从
IronPdf - 转换
$"{variable}" - 将
{% for item in collection %}转换为C#foreach - 将
{% if condition %}转换为C#if语句 8.将 Liquid 过滤器转换为 C# 方法(例如,...|upcase→.ToUpper()) - 用
FluidParser - 用直接C#变量替换
TemplateContext.SetValue()11.删除外部 PDF 库调用 12.在启动时添加IronPDF许可证初始化功能
迁移后测试
迁移后,验证这些方面:
- 验证 PDF 输出是否符合预期
- 测试所有模板变体是否能正确呈现
- 检查图像和样式是否正确显示
- 验证分页符是否正确
- 使用各种数据大小进行测试
- 性能测试与 Fluid + 外部库
- 测试并发场景中的线程安全
清理任务
- 删除
.liquid模板文件(如果不再需要) - 删除与 Fluid 相关的辅助代码
- 更新文档
- 清理未使用的依赖关系
迁移到IronPDF的主要优势
从使用外部 PDF 库的 Fluid(模板化)转向IronPDF具有几个关键优势:
单包解决方案:消除对两个库的依赖。IronPDF在一个软件包中同时处理模板制作(通过 HTML/CSS)和 PDF 生成。
无需学习新语法:使用标准的 C# 字符串插值和控制流,而不是学习 Liquid 模板语法。
线程安全渲染:TemplateContext,简化了并发PDF生成。
Chromium渲染引擎:基于Chromium的渲染支持现代CSS(包括Flexbox和Grid)和JavaScript执行。
单一错误源:只需对一个库进行故障排除,而无需在模板和 PDF 生成阶段之间进行协调,从而简化了调试过程。
广泛的.NET覆盖:IronPDF支持.NET Framework 4.6.2+、.NET Core 2.0+、.NET Standard 2.0、以及.NET 5到.NET 10。

