Skip to content

.NET 导入导出实践 ​

在企业级应用中,Excel/CSV 的导入导出是极高频的需求。本文总结了一套适用于 .NET 环境的通用架构方案,旨在解决性能瓶颈、代码耦合以及错误反馈不友好等常见问题。

1. 核心定位与设计目标 ​

1.1 基础设施组件化 ​

导入导出功能不应仅作为散落在各处的工具类,而应被视为 基础设施组件 (Infrastructure Component)。这意味着它需要具备统一的接口抽象、可插拔的底层实现以及标准化的数据模型。

1.2 设计原则 ​

  • 简单易用:开发人员应能通过少量代码完成基本的导入导出任务。
  • 职责分离:Controller 仅负责请求协调,Service 负责数据查询,基础设施层负责格式处理。
  • 错误友好:导入时必须具备完整的错误收集机制,避免“一行报错就停”。
  • 可扩展性:预留多格式(CSV、PDF、Word)及异步任务的扩展空间。

2. 技术选型建议 ​

在 .NET 生态中,针对 Excel 处理有多种成熟方案。推荐根据业务规模进行选型:

库优点缺点推荐场景
MiniExcel流式读写,内存占用极低;完全免费;API 极简。功能相对简单,不支持复杂样式和图表。首选方案;大数据量处理;个人/小团队项目。
ClosedXMLMIT 协议;API 友好;功能全(样式、公式)。大数据量下性能一般,内存消耗较大。需要复杂 Excel 样式、公式支持的场景。
EPPlus性能优秀,功能极其强大。商用需授权(v5+)。企业内部已购授权的重型报表系统。
NPOI功能最强;完全免费。API 极其复杂(偏底层),维护成本高。极端复杂的 Excel 历史文档处理。

3. 架构设计方案 ​

3.1 抽象接口定义 ​

通过接口抽象底层实现,确保在未来替换库(如从 MiniExcel 迁往 ClosedXML)时,业务逻辑层无需改动。

csharp
public interface IExcelExporter
{
    Task<ExportResult> ExportAsync<T>(IEnumerable<T> data, Dictionary<string, string> columnMapping, string fileName);
}

public interface IExcelImporter
{
    Task<ImportResult<T>> ImportAsync<T>(Stream stream, Dictionary<string, string> columnMapping) where T : class, new();
    Task<ExportResult> GenerateTemplateAsync<T>(Dictionary<string, string> columnMapping);
}

3.2 错误收集机制 ​

这是提升导入用户体验的关键。不要抛出第一个异常就终止,而应收集所有验证错误并返回。

csharp
public class ImportResult<T>
{
    public int SuccessCount { get; set; }
    public int FailureCount { get; set; }
    public List<T> SuccessData { get; set; } = new();
    public List<ImportError> Errors { get; set; } = new();
    public byte[]? ErrorFile { get; set; } // 可选:返回带批注的错误文件
}

public class ImportError
{
    public int RowNumber { get; set; }
    public string FieldName { get; set; }
    public string ErrorMessage { get; set; }
}

4. 核心实践指南 ​

4.1 统一入口模式 ​

Controller 不应直接引用 MiniExcel 或 ClosedXML。推荐在 Controller 中注入抽象接口,由基础设施服务完成具体工作。

csharp
[HttpGet("export")]
public async Task<IActionResult> Export([FromQuery] QueryParameter param)
{
    // 1. 获取业务数据
    var data = await _businessService.GetDataAsync(param);

    // 2. 调用封装好的导出服务
    var result = await _excelExporter.ExportAsync(
        data,
        ExportConfig.Columns, // 属性名 -> 列标题映射
        "业务报表"
    );

    return File(result.Stream, result.ContentType, result.FileName);
}

4.2 导入模板机制 ​

为避免用户因为“少了一列”或“顺序不对”导致导入失败,每个导入功能应配对提供一个动态生成的模板下载接口。模板的列顺序应与导入逻辑严格一致。

4.3 安全红线 ​

  • 数量限制:同步导出接口必须设置 MaxRows(如 50,000 条),超过则强制要求用户添加筛选条件。
  • 权限验证:导出动作通常涉及敏感数据,必须在 API 层强制执行角色权限检查。

5. 多格式扩展策略 ​

当系统需要支持 CSV、PDF 等多种格式时,建议引入 工厂模式 和 策略模式:

  1. IDataExporter:定义通用导出接口(包含 FileExtension 和 MimeType)。
  2. ExporterFactory:根据前端传入的格式参数(format=csv 或 format=xlsx)返回对应的实现类。
  3. 流式打包:对于批量导出多个文件的场景,应使用 System.IO.Compression 进行流式 ZIP 压缩,避免内存溢出。

6. 性能优化建议 ​

  • 流式处理:大数据量导出时,优先选择支持 IAsyncEnumerable 或流式写入的库(如 MiniExcel)。
  • 同步 vs 异步:
    • < 10,000 行:同步处理,直接返回文件。
    • 10,000 - 50,000 行:同步处理,需前端显示 Loading。
    • > 50,000 行:异步处理,提交任务到后台队列(如 Hangfire),完成后通知用户下载。
  • 内存管理:在转换数据模型时,尽量使用 yield return 或直接操作数据流,避免在内存中创建巨大的 List<T>。

参考资料 ​

方舟 · Ark