原始代码(ASP.NET WebForm / .NET Framework 4.x)

两步法

  1. Server.Execute("pos.aspx?id=...") 触发 ASPX 渲染。
  2. 在目标 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();
三处已过时:
  1. System.Web.UI.Page.Render(HtmlTextWriter) 是 WebForm 生命周期钩子,ASP.NET Core 没有等价物。
  2. 商业 PDF 组件 PdfConverter 一是贵(LicenseKey + 商业授权),二是依赖 Win32 / WebKit 二进制,跨平台部署困难。
  3. 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 优先付费本地快
wkhtmltopdfWebKit 二进制最好是LGPL本地快
QuestPDF纯 .NETN/A(自绘)是社区 / 商业最快
PuppeteerSharpChromium最好是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");
    }
}

>

使用准则

  1. 尽量避免"渲染 HTML → 转 PDF":它是 anti-pattern——HTML 是流式的,PDF 是分页的,"所见即所得"只在 wkhtmltopdf 这样的浏览器内核下成立。
  2. 报表类需求 → QuestPDF 优先:纯 C# 描述页面,单元测试可写,部署简单。
  3. 需要复用 HTML 模板 → wkhtmltopdf + 模板尽量简单(避免 flex / grid 复杂布局)。
  4. PDF 里有中文 → 必须嵌入字体。QuestPDF 自动嵌入;wkhtmltopdf 需要 --enable-local-file-access + 字体路径配置。
  5. 生产部署用 Docker 一次性带好 wkhtmltopdf / Chromium 二进制,避免运行时手动装。
参考资料