C#数据处理实战:用EPPlus把List 数据一键导出成美观的Excel文件

在后台开发中,数据导出是高频需求。想象这样的场景:你刚完成一个订单查询接口,前端同事问:"能不能加个导出Excel功能?"传统做法是手动遍历集合、设置单元格样式,不仅代码冗长,遇到日期格式化、多语言列名时更是头疼。EPPlus作为.NET平台最成熟的Excel操作库,能将这些繁琐过程封装成几行优雅代码。

1. 基础封装:从List 到Excel的智能转换

先通过NuGet安装EPPlus包:

Install-Package EPPlus

核心方法 ExportToExcel 接受任意 List<T> 并返回内存流,适合Web直接下载:

public static MemoryStream ExportToExcel<T>(List<T> data, string sheetName = "Sheet1")
{
    using var package = new ExcelPackage();
    var worksheet = package.Workbook.Worksheets.Add(sheetName);
    
    // 属性名映射为中文列头
    var properties = typeof(T).GetProperties();
    for (int i = 0; i < properties.Length; i++)
    {
        var displayName = properties[i].GetCustomAttribute<DisplayNameAttribute>()?.DisplayName 
                          ?? properties[i].Name;
        worksheet.Cells[1, i + 1].Value = displayName;
    }

    // 数据填充
    if (data?.Count > 0)
    {
        worksheet.Cells["A2"].LoadFromCollection(data);
    }

    return new MemoryStream(package.GetAsByteArray());
}

典型应用场景

  • 报表导出(订单、用户列表)
  • 数据备份(数据库查询结果存档)
  • 跨系统数据交换(符合Excel标准格式)

2. 样式优化:专业级表格视觉规范

基础导出只是开始,专业表格需要这些视觉元素:

2.1 智能样式配置

// 表头样式
using (var headerRange = worksheet.Cells[1, 1, 1, properties.Length])
{
    headerRange.Style.Font.Bold = true;
    headerRange.Style.Fill.PatternType = ExcelFillStyle.Solid;
    headerRange.Style.Fill.BackgroundColor.SetColor(Color.LightGray);
    headerRange.Style.Border.BorderAround(ExcelBorderStyle.Medium);
}

// 交替行颜色
var dataRange = worksheet.Cells[2, 1, data.Count + 1, properties.Length];
dataRange.Style.Fill.PatternType = ExcelFillStyle.Solid;
dataRange.Style.Fill.BackgroundColor.SetColor(Color.FromArgb(243, 243, 243));

2.2 自动列宽与数据类型识别

// 根据内容自动调整列宽
worksheet.Cells[worksheet.Dimension.Address].AutoFitColumns();

// 特殊类型格式化
foreach (var prop in properties.Where(p => p.PropertyType == typeof(DateTime)))
{
    int colIndex = Array.IndexOf(properties, prop) + 1;
    worksheet.Column(colIndex).Style.Numberformat.Format = "yyyy-MM-dd";
}

实际效果对比

样式元素 未优化版本 优化后版本
表头 普通文本 加粗灰底
数据行 全白背景 交替灰白
日期字段 原始格式 yyyy-MM-dd
列宽 固定宽度 内容自适应

3. 高级技巧:应对复杂业务场景

3.1 动态列映射

通过 DisplayNameAttribute 实现中英文列名切换:

public class OrderDto
{
    [DisplayName("订单编号")]
    public string OrderId { get; set; }
    
    [DisplayName("创建时间")] 
    public DateTime CreateTime { get; set; }
}

3.2 大数据量分块处理

避免内存溢出的分页写入方案:

int pageSize = 50000;
for (int page = 0; page < Math.Ceiling(data.Count / (double)pageSize); page++)
{
    var chunk = data.Skip(page * pageSize).Take(pageSize);
    worksheet.Cells[page * pageSize + 2, 1].LoadFromCollection(chunk);
    
    // 每处理5万行保存一次
    if (page % 10 == 0) package.Save();
}

3.3 自定义值转换器

处理枚举类型、空值等特殊情况:

public class StatusValueConverter : IValueConverter
{
    public object Convert(object value)
    {
        return value switch {
            OrderStatus.Pending => "待处理",
            OrderStatus.Shipped => "已发货",
            _ => value?.ToString()
        };
    }
}

// 使用转换器
worksheet.Cells[2, statusColIndex, data.Count + 1, statusColIndex]
    .Style.Numberformat.Format = new StatusValueConverter();

4. 实战案例:电商订单导出系统

假设需要实现以下需求:

  • 导出自定义时间段订单
  • 包含商品明细子表
  • 按状态自动标记颜色

核心代码结构

public async Task<IActionResult> ExportOrders(DateTime start, DateTime end)
{
    var orders = await _orderRepo.GetByDateRangeAsync(start, end);
    
    using var package = new ExcelPackage();
    var mainSheet = package.Workbook.Worksheets.Add("订单汇总");
    
    // 主表导出
    mainSheet.Cells["A1"].LoadFromCollection(orders, true);
    
    // 为不同状态设置颜色
    foreach (var order in orders)
    {
        int row = orders.IndexOf(order) + 2;
        var statusCell = mainSheet.Cells[row, statusCol];
        statusCell.Style.Font.Color.SetColor(GetStatusColor(order.Status));
    }
    
    // 商品明细子表
    var itemsSheet = package.Workbook.Worksheets.Add("商品明细");
    itemsSheet.Cells["A1"].LoadFromCollection(
        orders.SelectMany(o => o.Items),
        true
    );
    
    return File(package.GetAsByteArray(), 
               "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
               $"订单_{DateTime.Now:yyyyMMdd}.xlsx");
}

提示:Web项目中使用MemoryStream时,务必配置Response的ContentType和文件名,否则可能下载到损坏文件

5. 性能调优与异常处理

5.1 内存管理最佳实践

  • 使用 ExcelRange Dispose 释放资源
  • 设置 ExcelPackage.Compression 减少内存占用
  • 禁用自动计算公式:
    package.Workbook.CalcMode = ExcelCalcMode.Manual;
    

5.2 健壮性增强

try 
{
    // 导出操作
}
catch (InvalidOperationException ex) 
{
    _logger.LogError(ex, "Excel导出失败");
    throw new UserFriendlyException("生成Excel时发生错误");
}
finally
{
    // 确保资源释放
    package?.Dispose();
}

5.3 扩展性设计

通过策略模式支持多种导出格式:

public interface IDataExporter
{
    MemoryStream Export<T>(List<T> data);
}

public class ExcelExporter : IDataExporter 
{
    public MemoryStream Export<T>(List<T> data) 
    {
        // EPPlus实现
    }
}

// 在DI容器注册
services.AddSingleton<IDataExporter, ExcelExporter>();

6. 现代化改进:.NET Core集成方案

对于ASP.NET Core项目,推荐这些增强实践:

6.1 依赖注入配置

services.AddScoped(provider => 
{
    var settings = new ExcelPackageSettings {
        Compression = CompressionLevel.Fastest
    };
    return new ExcelPackage(settings);
});

6.2 中间件封装

创建可复用的导出端点:

[HttpGet("export")]
public async Task<IActionResult> Export([FromQuery] ExportRequest request)
{
    var data = await _service.GetExportDataAsync(request);
    var stream = _exporter.Export(data);
    return File(stream, "application/octet-stream", $"{request.Type}_{DateTime.Now:yyyyMMdd}.xlsx");
}

6.3 响应式前端集成

配合前端库实现进度显示:

axios.get('/api/orders/export', {
  responseType: 'blob',
  onDownloadProgress: progressEvent => {
    const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
    console.log(`下载进度: ${percent}%`);
  }
}).then(response => {
  const url = window.URL.createObjectURL(new Blob([response.data]));
  const link = document.createElement('a');
  link.href = url;
  link.setAttribute('download', 'orders.xlsx');
  document.body.appendChild(link);
  link.click();
});

7. 单元测试策略

确保导出功能稳定的测试方案:

7.1 基础功能验证

[Fact]
public void Should_Generate_Valid_Excel_From_List()
{
    // Arrange
    var testData = new List<TestModel> { /* 测试数据 */ };
    
    // Act
    var stream = ExcelHelper.ExportToExcel(testData);
    
    // Assert
    using var package = new ExcelPackage(stream);
    Assert.NotNull(package.Workbook);
    Assert.Equal(1, package.Workbook.Worksheets.Count);
    Assert.Equal(testData.Count, package.Workbook.Worksheets[0].Dimension.Rows - 1);
}

7.2 性能基准测试

[Benchmark]
public void Export_10000_Rows()
{
    var largeData = Enumerable.Range(1, 10000)
        .Select(i => new TestModel { /* 数据 */ })
        .ToList();
        
    ExcelHelper.ExportToExcel(largeData);
}

7.3 集成测试示例

[Fact]
public async Task Export_Endpoint_Should_Return_Excel_File()
{
    // Arrange
    var client = _factory.CreateClient();
    
    // Act
    var response = await client.GetAsync("/api/export");
    
    // Assert
    response.EnsureSuccessStatusCode();
    Assert.Equal("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 
        response.Content.Headers.ContentType.MediaType);
}

更多推荐