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

洋葱/整洁架构中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.cs
    
    抽象接口只定义业务需要的方法,不暴露任何第三方库的类型,示例:
    public 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的正确实现流程

  1. 定义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; }
    }
    
  2. 编写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"
            };
        }
    }
    
  3. 实现基础设施层的生成逻辑
    在Infrastructure层的InvoiceExcelGenerator类里写具体的Excel操作逻辑,引用第三方库完成数据填充、样式设置、字节流生成即可,不需要考虑业务规则。
  4. 依赖注入注册
    在Program启动代码里,把Infrastructure层的生成实现注册为对应接口的生命周期服务(一般用Scoped即可),MediatR会自动完成依赖注入。
  5. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 11:42:17