洋葱/整洁架构中Excel/PDF生成类放置位置及MediatR用法
洋葱架构/整洁架构下Excel/PDF生成功能的实现方案
你的初始思路有部分合理性,但没有严格遵循洋葱架构的依赖向内规则,也没有对齐MediatR + CQRS的常规实践,以下是生产环境验证过的标准实现方式:
正确的分层放置规则
洋葱架构的核心要求是所有依赖指向核心层(Domain/Application),核心层不能依赖外层基础设施的具体实现。Excel/PDF生成属于带外部库依赖的基础设施关注点,不能直接把具体实现写到Application层,分层规则如下:
- Application层(核心业务层)
只存放和业务用例绑定的请求模型、MediatR处理逻辑、生成服务的抽象接口,绝对不要直接引用第三方Excel/PDF类库,也不要写具体的格式转换代码。
对应发票导出场景,目录结构不需要额外建Generates文件夹,直接按CQRS约定归到Queries下即可,参考结构:
抽象接口只定义业务需要的方法,不暴露任何第三方库的类型,示例:Solution |- Application |- Features |- Invoices |- Commands // 存放增删改类的写请求 |- Queries // 存放查询、导出类的读请求 |- GetInvoiceExcelByDateRange.cs // MediatR请求+Handler可以放同一个文件,减少文件跳转 |- GetInvoicePdfByDateRange.cs |- Dtos |- Interfaces |- IInvoiceExcelGenerator.cs // 生成服务的抽象接口 |- IInvoicePdfGenerator.cspublic interface IInvoiceExcelGenerator { Task<byte[]> GenerateAsync(List<InvoiceDto> invoices, DateTime startDate, DateTime endDate, CancellationToken ct); } - Infrastructure层(基础设施层)
所有Excel/PDF的具体生成逻辑全放在这一层,可以自由引用EPPlus、NPOI、QuestPDF等第三方类库,实现Application层定义的对应接口。如果有跨业务复用的通用格式处理逻辑,可以在这一层抽公共帮助类,不要把业务相关的格式规则(比如发票的列头、统计公式、水印规则)抽到公共类里,避免业务逻辑散落到基础设施层。参考结构:Solution |- Infrastructure |- Services |- FileGenerators |- InvoiceExcelGenerator.cs // 实现IInvoiceExcelGenerator接口 |- InvoicePdfGenerator.cs // 实现IInvoicePdfGenerator接口 - API/Presentation层
只负责路由匹配、请求参数接收、文件结果返回,不要写任何业务逻辑或者生成逻辑。
结合MediatR的正确实现流程
- 定义MediatR请求模型
导出文件属于无状态变更的读操作,按CQRS约定定义为Query,统一返回通用的文件导出结果模型:// 放在Application层公共位置的通用DTO public class FileExportDto { public byte[] Content { get; set; } public string ContentType { get; set; } public string FileName { get; set; } } // 发票Excel导出请求 public class GetInvoiceExcelByDateRangeQuery : IRequest<FileExportDto> { public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } } - 编写Application层的Query Handler
Handler只做业务逻辑编排:做参数校验、查询业务数据、调用注入的生成接口拿到结果、组装返回值,不要直接写Excel/PDF操作代码:public class GetInvoiceExcelByDateRangeQueryHandler : IRequestHandler<GetInvoiceExcelByDateRangeQuery, FileExportDto> { private readonly IInvoiceRepository _invoiceRepo; private readonly IInvoiceExcelGenerator _excelGenerator; // 构造函数注入依赖,全程只依赖接口,不感知具体实现 public GetInvoiceExcelByDateRangeQueryHandler( IInvoiceRepository invoiceRepo, IInvoiceExcelGenerator excelGenerator) { _invoiceRepo = invoiceRepo; _excelGenerator = excelGenerator; } public async Task<FileExportDto> Handle( GetInvoiceExcelByDateRangeQuery request, CancellationToken cancellationToken) { // 业务校验 if (request.EndDate < request.StartDate) throw new ArgumentException("结束日期不能早于开始日期"); // 查询业务数据 var invoices = await _invoiceRepo.GetByDateRangeAsync( request.StartDate, request.EndDate, cancellationToken); // 调用抽象生成服务 var fileContent = await _excelGenerator.GenerateAsync( invoices, request.StartDate, request.EndDate, cancellationToken); // 返回结果 return new FileExportDto { Content = fileContent, ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", FileName = $"发票报表_{request.StartDate:yyyyMMdd}-{request.EndDate:yyyyMMdd}.xlsx" }; } } - 实现基础设施层的生成逻辑
在Infrastructure层的InvoiceExcelGenerator类里写具体的Excel操作逻辑,引用第三方库完成数据填充、样式设置、字节流生成即可,不需要考虑业务规则。 - 依赖注入注册
在Program启动代码里,把Infrastructure层的生成实现注册为对应接口的生命周期服务(一般用Scoped即可),MediatR会自动完成依赖注入。 - Controller接口实现
你原来的Controller思路基本正确,只需要调整请求类型为Query、接口语义对齐读操作即可:[HttpGet("api/invoices/export/excel")] public async Task<IActionResult> DownloadExcel( [FromQuery] GetInvoiceExcelByDateRangeQuery query, CancellationToken ct) { var result = await Mediator.Send(query, ct); return File(result.Content, result.ContentType, result.FileName); }
初始方案的常见问题
- 把具体生成代码直接放到Application层:会导致核心业务层强依赖第三方类库,后续更换生成库、调整生成逻辑时需要修改核心层代码,违反开闭原则和洋葱架构的依赖规则。
- 单独创建
Generates文件夹:打乱了CQRS统一的Command/Query目录约定,团队协作时会增加代码查找成本,所有用例不管是普通查询还是导出,都按读写分类归到Commands/Queries下即可。 - 将导出请求定义为Command:违反CQRS语义,Command专门用于带状态变更的写操作,导出是纯读操作,应该归为Query。
内容的提问来源于stack exchange,提问作者yozawiratama
相关产品推荐
相关产品推荐

