You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于C# Entity Framework在API端生成带数据绑定的PDF

基于C# EF Core的API侧PDF生成+数据绑定+邮件附件实现方案

整个流程不需要前端参与,API侧按「仓储层取数映射→PDF服务渲染绑定→邮件附件发送」三个环节拆分实现即可,全程保持职责分离,方便后续维护。

第一步:Repository层完成数据查询与映射

不要在PDF生成逻辑里直接操作DbContext,把数据查询逻辑收敛在仓储层,而且不要直接返回EF跟踪的实体类给PDF渲染模块,专门定义PDF绑定用的强类型ViewModel,避免导航属性循环引用、多余字段加载的问题。

// PDF数据绑定专用模型,和EF实体解耦
public class OrderPdfViewModel
{
    public string OrderNo { get; set; }
    public string CustomerName { get; set; }
    public DateTime CreateTime { get; set; }
    public List<OrderItemPdfDto> Items { get; set; }
    public decimal TotalAmount { get; set; }
}

public class OrderItemPdfDto
{
    public string ProductName { get; set; }
    public decimal Price { get; set; }
    public int Quantity { get; set; }
    public decimal SubTotal { get; set; }
}

// 仓储层实现,用EF查询直接映射成ViewModel
public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _dbContext;
    public OrderRepository(AppDbContext dbContext) => _dbContext = dbContext;

    public async Task<OrderPdfViewModel> GetOrderPdfDataAsync(Guid orderId)
    {
        var data = await _dbContext.Orders
            .AsNoTracking()
            .Where(o => o.Id == orderId)
            .Select(o => new OrderPdfViewModel
            {
                OrderNo = o.OrderNo,
                CustomerName = o.Customer.FullName,
                CreateTime = o.CreateTime,
                TotalAmount = o.TotalAmount,
                Items = o.Items.Select(i => new OrderItemPdfDto
                {
                    ProductName = i.Product.Name,
                    Price = i.UnitPrice,
                    Quantity = i.Quantity,
                    SubTotal = i.UnitPrice * i.Quantity
                }).ToList()
            })
            .FirstOrDefaultAsync();
        
        return data ?? throw new KeyNotFoundException("对应业务数据不存在");
    }
}

第二步:封装独立PDF生成服务完成数据绑定

PDF生成逻辑不要写在Controller里,单独抽服务层实现,推荐用不需要依赖浏览器内核的PDF生成库,直接Nuget安装即可,部署时不用额外装WebView或者浏览器运行时,适合API服务场景。
安装命令:Install-Package QuestPDF
服务实现代码:

public interface IPdfGenerator
{
    Task<byte[]> GenerateAsync<T>(string templateName, T bindData);
}

public class PdfGenerator : IPdfGenerator
{
    public async Task<byte[]> GenerateAsync<T>(string templateName, T bindData)
    {
        // 这里以订单PDF为例,实际可以根据templateName匹配不同模板
        if (templateName != "OrderNotify") throw new NotSupportedException("不支持的PDF模板");
        var data = bindData as OrderPdfViewModel;

        var doc = Document.Create(container =>
        {
            container.Page(page =>
            {
                page.Size(PageSizes.A4);
                page.Margin(2, Unit.Centimetre);
                // 头部绑定订单号
                page.Header().Text($"订单确认通知 - {data.OrderNo}").Bold().FontSize(18).AlignCenter();
                // 正文绑定业务字段
                page.Content().Column(col =>
                {
                    col.Spacing(12);
                    col.Item().Text($"客户姓名:{data.CustomerName}");
                    col.Item().Text($"下单时间:{data.CreateTime:yyyy-MM-dd HH:mm:ss}");
                    // 表格绑定明细列表
                    col.Item().Table(table =>
                    {
                        table.ColumnsDefinition(cd =>
                        {
                            cd.RelativeColumn(3);
                            cd.RelativeColumn(1);
                            cd.RelativeColumn(1);
                            cd.RelativeColumn(1);
                        });
                        table.Header(h =>
                        {
                            h.Cell().Text("商品名称").Bold();
                            h.Cell().Text("单价").Bold();
                            h.Cell().Text("数量").Bold();
                            h.Cell().Text("小计").Bold();
                        });
                        foreach (var item in data.Items)
                        {
                            table.Cell().Text(item.ProductName);
                            table.Cell().Text(item.Price.ToString("N2"));
                            table.Cell().Text(item.Quantity.ToString());
                            table.Cell().Text(item.SubTotal.ToString("N2"));
                        }
                    });
                    col.Item().Text($"订单总金额:{data.TotalAmount:N2} 元").Bold().AlignRight();
                });
                // 页脚页码
                page.Footer().AlignCenter().Text(t =>
                {
                    t.Span("第 ");
                    t.CurrentPageNumber();
                    t.Span(" 页 / 共 ");
                    t.TotalPages();
                    t.Span(" 页");
                });
            });
        });

        return await doc.GeneratePdfAsync();
    }
}

核心注意点:

  • PDF服务只负责接收已经查好的强类型数据做渲染,不要在服务里直接操作DbContext或者调用Repository,保证职责单一,方便单元测试
  • 生成结果直接返回byte[]二进制数组,不需要把PDF存到服务器本地磁盘,减少IO开销,也避免临时文件清理问题
  • 如果习惯用Razor模板写HTML再转PDF也可以,核心逻辑不变:传入强类型绑定数据→模板渲染→输出PDF二进制流

第三步:API接口串联流程,挂载PDF附件发邮件

Controller里通过构造注入依赖的仓储、PDF服务、邮件服务,把整个流程串起来即可,邮件发送直接用.NET内置的System.Net.Mail实现,不需要额外第三方组件。

[ApiController]
[Route("api/notify")]
public class NotifyController : ControllerBase
{
    private readonly IOrderRepository _orderRepo;
    private readonly IPdfGenerator _pdfGenerator;
    private readonly SmtpClient _smtpClient;

    public NotifyController(IOrderRepository orderRepo, IPdfGenerator pdfGenerator, SmtpClient smtpClient)
    {
        _orderRepo = orderRepo;
        _pdfGenerator = pdfGenerator;
        _smtpClient = smtpClient;
    }

    [HttpPost("order/{orderId}")]
    public async Task<IActionResult> SendOrderNotify(Guid orderId, [FromQuery] string receiveEmail)
    {
        // 1. 从仓储层取绑定用的业务数据
        var bindData = await _orderRepo.GetOrderPdfDataAsync(orderId);
        // 2. 生成PDF二进制流
        var pdfBytes = await _pdfGenerator.GenerateAsync("OrderNotify", bindData);
        // 3. 构造邮件内容
        var mail = new MailMessage
        {
            From = new MailAddress("notify@yourdomain.com", "业务系统通知"),
            Subject = $"您的订单{bindData.OrderNo}已确认",
            Body = $"您好{bindData.CustomerName},您提交的订单已确认生效,详情请查看附件。",
            IsBodyHtml = false
        };
        mail.To.Add(receiveEmail);
        // 挂载PDF附件,直接用内存流,不需要读取本地文件
        mail.Attachments.Add(new Attachment(
            new MemoryStream(pdfBytes), 
            $"订单_{bindData.OrderNo}.pdf", 
            MediaTypeNames.Application.Pdf
        ));
        // 4. 发送邮件
        await _smtpClient.SendMailAsync(mail);
        
        return Ok(new { success = true, msg = "通知邮件发送完成" });
    }
}

常见踩坑说明

  • 依赖注入配置时,DbContext、Repository、PdfGenerator、SmtpClient统一注册为Scoped生命周期即可,不要注册为Singleton,避免DbContext被多线程复用导致的跟踪异常
  • 如果PDF包含中文,记得把用到的中文字体文件嵌入到程序集里,不然部署到Linux环境后会出现中文乱码
  • 大文件PDF生成一定要用异步方法,避免阻塞API请求线程
  • 附件用到的MemoryStream不需要手动释放,MailMessage对象Dispose时会自动释放关联的流资源

内容的提问来源于stack exchange,提问作者Anusiya

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 20:42:20