原始代码(ASP.NET WebForm / .NET Framework 4.x)
两步法
Server.Execute("pos.aspx?id=...")触发 ASPX 渲染。- 在目标 ASPX 中重写
Render(HtmlTextWriter writer),把页面输出劫持到StringWriter,再写入文件。
1. ASPX 端:劫持 Render 输出
// .aspx 后台代码
protected override void Render(HtmlTextWriter writer)
{
string htmlname = "POSDemo";
string fileName = string.Empty;
string content = string.Empty;
System.IO.StringWriter stringWriter = new System.IO.StringWriter();
HtmlTextWriter htmlWriter = new HtmlTextWriter(stringWriter);
System.IO.StreamWriter sw = null;
try
{
base.Render(htmlWriter); // 触发标准渲染 → 内存中的 stringWriter
htmlWriter.Close();
content = stringWriter.ToString();
fileName = htmlname + ".html";
string path = HttpContext.Current.Server.MapPath("../Upload/");
System.Text.Encoding code = System.Text.Encoding.GetEncoding("utf-8");
sw = new System.IO.StreamWriter(path + fileName, false, code);
sw.Write(content);
sw.Flush();
}
finally
{
sw?.Dispose();
stringWriter.Dispose();
htmlWriter.Close();
htmlWriter.Dispose();
}
}
2. 商业 PDF 控件转换(EVO PDF / HiQPdf)
public class MyPDFGenerator
{
public static byte[] Create(string sUrl)
{
try
{
var pdfConverter = new PdfConverter(); // 商业组件入口
pdfConverter.LicenseKey = "YOUR-COMMERCIAL-KEY";
pdfConverter.PdfDocumentOptions.PdfPageSize = PdfPageSize.A4;
pdfConverter.PdfDocumentOptions.GenerateSelectablePdf = true;
pdfConverter.PdfDocumentOptions.LiveUrlsEnabled = true;
pdfConverter.PdfDocumentInfo.AuthorName = "Your Company";
byte[] pdfBytes = pdfConverter.GetPdfFromUrlBytes(sUrl);
return pdfBytes;
}
catch
{
return null;
}
}
}
3. 调用 + 输出
byte[] bytes = MyPDFGenerator.Create(Server.MapPath("../Upload/POSDemo.html"));
var response = System.Web.HttpContext.Current.Response;
response.Clear();
response.AddHeader("Content-Type", "binary/octet-stream");
response.AddHeader("Content-Disposition", $"attachment; filename={fileName}.pdf; size={bytes.Length}");
response.BinaryWrite(bytes);
response.End();
三处已过时:
System.Web.UI.Page.Render(HtmlTextWriter)是 WebForm 生命周期钩子,ASP.NET Core 没有等价物。- 商业 PDF 组件
PdfConverter一是贵(LicenseKey + 商业授权),二是依赖 Win32 / WebKit 二进制,跨平台部署困难。 HttpContext.Current是 .NET Framework 的静态访问点,ASP.NET Core 用IHttpContextAccessor注入。
ASP.NET Core 8 现代两步法
第一步:把 ASPX 改为 Razor 视图
现代 ASP.NET Core 用 .cshtml 取代 .aspx。把"渲染成字符串"的逻辑写成一个服务:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.Rendering;
public class TemplateRendererService(IRazorViewEngine viewEngine,
ITempDataProvider tempDataProvider,
ICompositeViewEngine compositeEngine,
IServiceProvider serviceProvider)
{
public async Task<string> RenderAsync(string viewName, object model, CancellationToken ct)
{
var viewResult = viewEngine.FindView(ControllerContext, viewName, isMainPage: false);
var actionContext = new ActionContext(
new DefaultHttpContext { RequestServices = serviceProvider },
new RouteData(),
new ActionDescriptor(),
modelState: new Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary());
var view = viewResult.View;
using var stringWriter = new StringWriter();
var viewContext = new ViewContext(
actionContext, view, new ViewDataDictionary>(model, new Microsoft.AspNetCore.Mvc.ModelBinding.EmptyModelMetadataProvider()),
new TempDataDictionary(actionContext.HttpContext, tempDataProvider),
stringWriter,
new HtmlHelperOptions());
await view.RenderAsync(viewContext);
return stringWriter.ToString();
}
}
这是"在内存渲染 Razor 视图"的完整方案,可替代原 Server.Execute + Render。
第二步:把 HTML 落盘成静态文件
[ApiController]
[Route("api/invoices")]
public class InvoiceController(TemplateRendererService renderer,
IWebHostEnvironment env) : ControllerBase
{
[HttpPost("render-and-save")]
public async Task<IActionResult> RenderAndSave([FromBody] InvoiceRequest req, CancellationToken ct)
{
// 1. 渲染 Razor 视图
var html = await renderer.RenderAsync("InvoiceTemplate", req, ct);
// 2. 写盘(路径用 IWebHostEnvironment 注入,不再用 Server.MapPath)
var uploadDir = Path.Combine(env.WebRootPath, "Upload");
Directory.CreateDirectory(uploadDir);
var fileName = $"Invoice-{req.InvoiceId}.html";
var filePath = Path.Combine(uploadDir, fileName);
await System.IO.File.WriteAllTextAsync(filePath, html, Encoding.UTF8, ct);
// 3. 返回文件路径供后续转 PDF 用
return Ok(new { Path = filePath });
}
}
PDF 导出:3 个开源替代方案
方案 1:wkhtmltopdf(最接近原方案)
如果你的 ASPX 输出是"含 CSS 的 HTML 页面",wkhtmltopdf 用 WebKit 渲染引擎直接出 PDF,**几乎 1:1 复刻浏览器所见即所得**。
# 安装
# Ubuntu: sudo apt install wkhtmltopdf
# macOS: brew install --cask wkhtmltopdf
# Windows: https://wkhtmltopdf.org/downloads.html
public class WkhtmltopdfService
{
public async Task<byte[]> HtmlToPdfAsync(string htmlOrUrl, CancellationToken ct)
{
var psi = new ProcessStartInfo
{
FileName = "wkhtmltopdf",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
psi.ArgumentList.Add("--enable-local-file-access");
psi.ArgumentList.Add("--print-media-type");
psi.ArgumentList.Add("--quiet");
if (htmlOrUrl.StartsWith("http") || File.Exists(htmlOrUrl))
psi.ArgumentList.Add(htmlOrUrl);
else
{
// HTML 字符串 → 临时文件
var tmp = Path.GetTempFileName() + ".html";
await File.WriteAllTextAsync(tmp, htmlOrUrl, ct);
psi.ArgumentList.Add(tmp);
}
psi.ArgumentList.Add("-"); // 输出到 stdout
using var proc = Process.Start(psi);
var pdf = await proc!.StandardOutput.BaseStream.ReadAllBytesAsync(ct);
await proc.WaitForExitAsync(ct);
return pdf;
}
}
优缺点:
- 优点:CSS 支持最好;无须 License;与原 EVO PDF 行为最接近。
- 缺点:依赖外部进程;WebKit 内核较老;新版 wkhtmltopdf 已停止。
方案 2:QuestPDF(C# 首选)
QuestPDF 是现代化的 .NET PDF 库,**用 C# 直接描述页面布局**,不依赖外部进程。
// NuGet: QuestPDF
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
public class InvoicePdfService
{
public byte[] Generate(InvoiceRequest invoice)
{
QuestPDF.Settings.License = LicenseType.Community; // 个人 /年营收 < 100 万 USD 即可用
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, QuestPDF.Infrastructure.Unit.Centimetre);
page.DefaultTextStyle(t => t.FontSize(10));
page.Header().Text($"Invoice #{invoice.InvoiceId}").FontSize(20).Bold();
page.Content().PaddingVertical(1, QuestPDF.Infrastructure.Unit.Centimetre).Column(column =>
{
column.Spacing(10);
column.Item().Text($"Customer: {invoice.CustomerName}");
column.Item().Table(table =>
{
table.ColumnsDefinition(c =>
{
c.ConstantColumn(80); // Item
c.RelativeColumn(); // Description
c.ConstantColumn(60); // Qty
c.ConstantColumn(80); // Price
});
table.Header(h =>
{
h.Cell().Text("Item").Bold();
h.Cell().Text("Description");
h.Cell().AlignRight().Text("Qty");
h.Cell().AlignRight().Text("Price");
});
foreach (var line in invoice.Lines)
{
table.Cell().Text(line.Sku);
table.Cell().Text(line.Description);
table.Cell().AlignRight().Text($"{line.Quantity}");
table.Cell().AlignRight().Text($"{line.UnitPrice:C}");
}
});
});
page.Footer().AlignRight().Text(t =>
{
t.Span("Total: ");
t.Span($"{invoice.Total:C}").Bold();
});
});
}).GeneratePdf();
}
}
优缺点:
- 优点:纯 C#,易于版本控制;无需外部进程;布局代码可测试;中文字体嵌入支持良好。
- 缺点:需重新设计页面布局(不复用 HTML);企业用途需付费 License(年收入 100 万 USD 以上)。
方案 3:PuppeteerSharp(Chromium 引擎)
如果你想"打开 HTML 页面、点击按钮再导出 PDF",PuppeteerSharp 直接调 Chromium。
// NuGet: PuppeteerSharp
using PuppeteerSharp;
public class PuppeteerPdfService
{
public async Task<byte[]> UrlToPdfAsync(string url, CancellationToken ct)
{
await new BrowserFetcher().DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true });
await using var page = await browser.NewPageAsync();
await page.GoToAsync(url);
return await page.PdfDataAsync(new PdfOptions { Format = PaperFormat.A4 });
}
}
优缺点:
- 优点:现代 Chromium 引擎,CSS / JS 完美支持;可编程(点击、截图区域)。
- 缺点:Chromium 二进制大(~150 MB);首次启动慢;不适合高并发。
方案对比
| 方案 | 依赖 | CSS 支持 | 跨平台 | License | 性能 |
|---|---|---|---|---|---|
| 原 EVO PDF | 商业 native DLL | 中等 | Win 优先 | 付费 | 本地快 |
| wkhtmltopdf | WebKit 二进制 | 最好 | 是 | LGPL | 本地快 |
| QuestPDF | 纯 .NET | N/A(自绘) | 是 | 社区 / 商业 | 最快 |
| PuppeteerSharp | Chromium | 最好 | 是 | MIT | 慢(启动) |
选型建议:
- 已有 HTML 模板且团队不熟悉布局 → wkhtmltopdf。
- 愿意重新设计布局、追求高并发 → QuestPDF。
- 需要 JavaScript / 浏览器行为 → PuppeteerSharp。
完整 Controller 示例(QuestPDF)
[ApiController]
[Route("api/invoices")]
public class InvoiceController(InvoicePdfService pdfService) : ControllerBase
{
[HttpPost("{id:int}/pdf")]
public IActionResult GeneratePdf(int id)
{
var invoice = _db.Invoices
.Include(i => i.Lines)
.First(i => i.Id == id);
var bytes = pdfService.Generate(invoice);
return File(bytes, "application/pdf", $"Invoice-{id}.pdf");
}
}
>
使用准则
- 尽量避免"渲染 HTML → 转 PDF":它是 anti-pattern——HTML 是流式的,PDF 是分页的,"所见即所得"只在 wkhtmltopdf 这样的浏览器内核下成立。
- 报表类需求 → QuestPDF 优先:纯 C# 描述页面,单元测试可写,部署简单。
- 需要复用 HTML 模板 → wkhtmltopdf + 模板尽量简单(避免 flex / grid 复杂布局)。
- PDF 里有中文 → 必须嵌入字体。QuestPDF 自动嵌入;wkhtmltopdf 需要
--enable-local-file-access+ 字体路径配置。 - 生产部署用 Docker 一次性带好 wkhtmltopdf / Chromium 二进制,避免运行时手动装。