C#使用CsvHelper按每1000行分块导出CSV流文件并下载的实现方法
你现有代码无法生成独立分块CSV的核心问题有两个:
- 全程复用同一个指向单文件
/static/export.csv的FileStream,所有分块内容最终都会被追加到这一个文件里 - 跨循环复用
MemoryStream和CsvWriter实例,没有在单个分块写完后重置流状态、清空缓冲区,会出现内容重叠、格式错乱的问题
实现方案
核心逻辑是每1000条数据对应一套独立的流和写入器,每个分块写完立即释放资源,按序号生成独立文件即可。如果是Web场景提供下载,推荐直接把分块CSV打包成ZIP返回,不需要把文件落盘到服务器,内存占用更低。
1. 本地生成多份独立CSV文件
using System.Globalization; using System.Text; using CsvHelper; // 按业务需求配置CsvHelper规则 var csvConfig = new CsvConfiguration(CultureInfo.InvariantCulture) { HasHeaderRecord = true, // 每个CSV文件单独写入表头 Encoding = Encoding.UTF8 }; const int singleFileRowLimit = 1000; // 计算总分块数:如果是数据库查询场景,不要提前ToList()拉取全量数据,保持IQueryable类型即可 int totalRows = result.Count(); int totalFileCount = (int)Math.Ceiling(totalRows / (double)singleFileRowLimit); for (int fileIndex = 0; fileIndex < totalFileCount; fileIndex++) { // 按序号生成独立文件名,避免覆盖:export_1.csv、export_2.csv... string savePath = Path.Combine("/static", $"export_{fileIndex + 1}.csv"); // 取当前分块的1000条数据,IQueryable场景下会自动生成分页SQL,每次只拉取当前块的数据 var currentRows = result.Skip(fileIndex * singleFileRowLimit).Take(singleFileRowLimit); // 每个文件单独创建流、写入器,using包裹会自动完成Flush、释放资源,不需要手动调用Flush using var fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write); using var streamWriter = new StreamWriter(fileStream, csvConfig.Encoding); using var csvWriter = new CsvWriter(streamWriter, csvConfig); csvWriter.WriteRecords(currentRows); }
2. Web场景直接流式打包ZIP下载
如果是接口提供导出能力,不需要把文件存到服务器静态目录,直接把分块CSV写入ZIP压缩包返回响应流即可,用户拿到的就是包含所有分块CSV的压缩包:
using System.IO.Compression; using System.Globalization; using System.Text; using CsvHelper; using Microsoft.AspNetCore.Mvc; [HttpGet("export")] public async Task<IActionResult> ExportCsvPackage() { var csvConfig = new CsvConfiguration(CultureInfo.InvariantCulture) { HasHeaderRecord = true, Encoding = Encoding.UTF8 }; const int singleFileRowLimit = 1000; int totalRows = await result.CountAsync(); int totalFileCount = (int)Math.Ceiling(totalRows / (double)singleFileRowLimit); // 设置ZIP下载响应头 Response.Headers.Add("Content-Disposition", "attachment; filename=csv_export.zip"); // 直接把ZIP写入响应流,不需要在服务器生成临时文件 using var zipArchive = new ZipArchive(Response.BodyWriter.AsStream(), ZipArchiveMode.Create); for (int fileIndex = 0; fileIndex < totalFileCount; fileIndex++) { var currentRows = result.Skip(fileIndex * singleFileRowLimit).Take(singleFileRowLimit); // 在压缩包中创建独立CSV条目 var zipEntry = zipArchive.CreateEntry($"export_{fileIndex + 1}.csv", CompressionLevel.Fastest); await using var entryStream = zipEntry.Open(); using var streamWriter = new StreamWriter(entryStream, csvConfig.Encoding); using var csvWriter = new CsvWriter(streamWriter, csvConfig); await csvWriter.WriteRecordsAsync(currentRows); } return new EmptyResult(); }
注意事项
- 不要提前把全量数据加载到内存:如果数据源是数据库,保持查询为
IQueryable类型,Skip/Take会自动生成对应分页SQL,每次只拉取1000条数据,内存占用不会随数据量上涨 - 不要跨分块复用流、StreamWriter、CsvWriter实例:每个分块的写入实例独立创建、用完即释放,避免内容错乱、文件句柄泄漏
- 不需要额外用MemoryStream做中转:直接把CsvWriter绑定到目标文件流/ZIP条目流,减少一次内存拷贝,导出性能更高
- 如果不需要每个CSV带表头,把
HasHeaderRecord设为false即可
内容的提问来源于stack exchange,提问作者Berkay
相关产品推荐
相关产品推荐

