书接上回,我们继续来聊聊ExcelHelper的具体实现。

01、读取Excel到DataSet单元测试

在上一章我们主要讲解了读取Excel到DataSet的三个重载方法具体实现,还没来得及做单元测试,因此我们首先对这三个方法做个单元测试。具体代码如下:

[Fact]
public void Read_FileName_DataSet()
{
//读取所有工作簿
var dataSet = ExcelHelper.Read("Read.xlsx");
Assert.Equal(3, dataSet.Tables.Count);
var table1 = dataSet.Tables[0];
Assert.Equal("Sheet1", table1.TableName);
Assert.Equal("A", table1.Rows[0][0]);
Assert.Equal("B", table1.Rows[0][1]);
Assert.Equal("1", table1.Rows[0][2]);
Assert.Equal("C", table1.Rows[1][0]);
Assert.Equal("D", table1.Rows[1][1]);
Assert.Equal("2", table1.Rows[1][2]); //读取所有工作簿,并且首行数据作为表头
dataSet = ExcelHelper.Read("Read.xlsx", true);
Assert.Equal(3, dataSet.Tables.Count);
table1 = dataSet.Tables[1];
var columus = table1.Columns;
Assert.Equal("Sheet2", table1.TableName);
Assert.Equal("E", columus[0].ColumnName);
Assert.Equal("F", columus[1].ColumnName);
Assert.Equal("3", columus[2].ColumnName);
Assert.Equal("G", table1.Rows[0][0]);
Assert.Equal("H", table1.Rows[0][1]);
Assert.Equal("4", table1.Rows[0][2]); //根据工作簿名称sheetName读取指定工作簿
dataSet = ExcelHelper.Read("Read.xlsx", true, "Sheet2");
Assert.Single(dataSet.Tables);
Assert.Equal("Sheet2", dataSet.Tables[0].TableName); //通过工作簿名称sheetName读取不存在的工作簿
dataSet = ExcelHelper.Read("Read.xlsx", true, "Sheet99");
Assert.Empty(dataSet.Tables); //同时指定sheetName和sheetNumber优先使用sheetName
dataSet = ExcelHelper.Read("Read.xlsx", true, "Sheet1", 2);
Assert.Single(dataSet.Tables);
Assert.Equal("Sheet1", dataSet.Tables[0].TableName);
//通过工作簿编号sheetNumber读取不存在的工作簿 dataSet = ExcelHelper.Read("Read.xlsx", true, null, 99);
Assert.Empty(dataSet.Tables); //通过工作簿编号sheetNumber读取指定工作簿
dataSet = ExcelHelper.Read("Read.xlsx", true, null, 1);
Assert.Single(dataSet.Tables);
Assert.Equal("Sheet1", dataSet.Tables[0].TableName);
}
# ***02***、根据文件路径读取Excel到对象集合``` 在上一章中我们实现了Excel与DataSet相互转换,而在前面TableHelper实现章节中我们已经实现了对象集合与表格DataTable的相互转换,因此我们只要把这两者结合起来就可以实现Excel与对象集合的相互转换。 因为Excel中有多个工作簿Sheet,而每一个工作簿Sheet代表一个表格DataTable,一个表格DataTable关联一个对象集合,因此我们约定本方法必须指定一个工作簿Sheet用来转换对象集合,如果没有指定则默认读取第一个工作簿Sheet。 而该方法通过文件完全路径读取到Excel文件流后,调用具体实现文件流处理重载方法,具体代码如下: ```csharp
//根据文件路径读取Excel到对象集合
//指定sheetName,sheetNumber则读取相应工作簿Sheet
//如果不指定则默认读取第一个工作簿Sheet
public static IEnumerable<T> Read<T>(string path, bool isFirstRowAsColumnName = false, string? sheetName = null, int? sheetNumber = null)
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
return Read<T>(stream, IsXlsxFile(path), isFirstRowAsColumnName, sheetName, sheetNumber);
}

03、根据文件流、文件名读取Excel到对象集合

在有些场景下,我们直接得到的就是Excel文件流,因此更通用的处理方式就是处理ExceL文件流,因为无论如何最终我们都是要拿到Excel文件流的。

该方法也是一个重载方法,为了方便哪些上传文件后,有文件流,有文件名,但是不想自己处理文件后缀格式的,提供一个便捷方法,因此该方法会通过文件名识别出文件具体后缀格式,再调用下一个重载方法,具体实现如下:

//根据文件流读取Excel到对象集合
//指定sheetName,sheetNumber则读取相应工作簿Sheet
//如果不指定则默认读取第一个工作簿Sheet
public static IEnumerable<T> Read<T>(Stream stream, string fileName, bool isFirstRowAsColumnName = false, string? sheetName = null, int? sheetNumber = null)
{
return Read<T>(stream, IsXlsxFile(fileName), isFirstRowAsColumnName, sheetName, sheetNumber);
}

04、根据文件流、文件后缀读取Excel到对象集合

该方法是上面两个方法的最终实现,具体实现分为两步:

(1)读取指定工作簿Sheet到DataSet中;

(2)把DataSet中第一个表格DataTable转换为对象集合;

而这两步都是调用之前实现好的方法,具体代码如下:

//根据文件流读取Excel到对象集合
//指定sheetName,sheetNumber则读取相应工作簿Sheet
//如果不指定则默认读取第一个工作簿Sheet
public static IEnumerable<T> Read<T>(Stream stream, bool isXlsx, bool isFirstRowAsColumnName = false, string? sheetName = null, int? sheetNumber = null)
{
//读取指定工作簿Sheet至DataSet
var dataSet = CreateDataSetWithStreamOfSheet(stream, isXlsx, isFirstRowAsColumnName, sheetName, sheetNumber ?? 1);
if (dataSet == null || dataSet.Tables.Count == 0)
{
return [];
} //DataTable转对象集合
return TableHelper.ToModels<T>(dataSet.Tables[0]);
}

下面我们针对上面三个方法做个简单的单元测试,代码如下:

public class Student
{
public string A { get; set; }
[Description("B")]
public string Name { get; set; }
[Description("1")]
public DateTime Age { get; set; }
} [Fact]
public void Read_FileName_T()
{
//表格数据格式无法转为对象数据类型,则抛异常
Assert.Throws<FormatException>(() => ExcelHelper.Read<Student>("Read.xlsx", true, "Sheet1")); //表格成功转为对象集合
var models = ExcelHelper.Read<Student>("Read.xlsx", true, "Sheet3");
Assert.Single(models);
var model = models.First();
Assert.Equal("C", model.A);
Assert.Equal("D", model.Name);
Assert.Equal(new DateTime(2024, 11, 29), model.Age);
}

05、把表格数组写入Excel文件流

该方法是先把表格数组生成Excel的IWorkbook,然后再写入内存流MemoryStream。

而表格数组转换为IWorkbook也很简单,在IWorkbook中创建工作簿Sheet,然后把每个表格数据填充至相应的工作簿Sheet中即可,具体代码如下:

//把表格数组写入Excel文件流
public static MemoryStream Write(DataTable[] dataTables, bool isXlsx, bool isColumnNameAsData)
{
//表格数组写入Excel对象
using var workbook = CreateWorkbook(dataTables, isXlsx, isColumnNameAsData);
var stream = new MemoryStream();
workbook.Write(stream, true);
stream.Flush();
return stream;
} //表格数组转为IWorkbook
private static IWorkbook CreateWorkbook(DataTable[] dataTables, bool isXlsx, bool isColumnNameAsData)
{
//根据Excel文件后缀创建IWorkbook
var workbook = CreateWorkbook(isXlsx);
foreach (var dt in dataTables)
{
//根据表格填充Sheet
FillSheetByDataTable(workbook, dt, isColumnNameAsData);
} return workbook;
}

而根据表格填充工作簿Sheet实现也非常简单,只需遍历表格中每个单元格,把其值填充至对应工作簿Sheet中相同的位置即可,当然其中表格列名是否要作为数据,需要单独处理,具体代码如下:

//根据表格填充工作簿Sheet
private static void FillSheetByDataTable(IWorkbook workbook, DataTable dataTable, bool isColumnNameAsData)
{
var sheet = string.IsNullOrWhiteSpace(dataTable.TableName) ? workbook.CreateSheet() : workbook.CreateSheet(dataTable.TableName);
if (isColumnNameAsData)
{
//把列名加入数据第一行
var dataRow = sheet.CreateRow(0);
foreach (DataColumn column in dataTable.Columns)
{
dataRow.CreateCell(column.Ordinal).SetCellValue(column.ColumnName);
}
} //循环处理表格的所有行数据
for (var i = 0; i < dataTable.Rows.Count; i++)
{
var dataRow = sheet.CreateRow(i + (isColumnNameAsData ? 1 : 0));
for (var j = 0; j < dataTable.Columns.Count; j++)
{
dataRow.CreateCell(j).SetCellValue(dataTable.Rows[i][j].ToString());
}
}
}

06、把表格数组写入Excel文件

该方法需要注意的是对于Excel文件路径的处理,如果给定的Excel文件路径不存在,则本方法会自动创建相应的文件夹,如果给定的Excel文件路径中不包括文件名称,则本方法会自动根据当前时间+4位随机数的方式+.xlsx的命名方式自动生成文件名。

处理好这些则只需要调用根据表格数组生成Excel对象方法,最后写入Excel文件中,具体代码如下:

//把表格数组写入Excel文件
public static void Write(DataTable[] dataTables, string path, bool isColumnNameAsData)
{
//检查文件夹是否存在,不存在则创建
var directoryName = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName))
{
Directory.CreateDirectory(directoryName);
} //检查是否指定文件名,没有则默认以“时间+随机数.xlsx”作为文件名
var fileName = Path.GetFileName(path);
if (string.IsNullOrEmpty(fileName))
{
directoryName = Path.GetFullPath(path);
fileName = DateTime.Now.ToString("yyyyMMdd-hhmmss-") + new Random().Next(0000, 9999).ToString("D4") + ".xlsx";
path = Path.Combine(directoryName, fileName);
} //表格数组写入Excel对象
using var workbook = CreateWorkbook(dataTables, IsXlsxFile(path), isColumnNameAsData);
using var fs = new FileStream(path, FileMode.Create, FileAccess.Write);
workbook.Write(fs, true);
}

下面我们对上面两个写入方法进行详细的单元测试,具体如下:

[Fact]
public void Write_Table()
{
var table = TableHelper.Create<Student>();
var row1 = table.NewRow();
row1[0] = "Id-11";
row1[1] = "名称-12";
row1[2] = new DateTime(2024, 11, 28);
table.Rows.Add(row1); var row2 = table.NewRow();
row2[0] = "Id-21";
row2[1] = "名称-22";
row2[2] = new DateTime(2024, 11, 29);
table.Rows.Add(row2); var message = "The column name of the table cannot be mapped to an object property, and the conversion cannot be completed."; //把表格写入Excel,并且列名不作为数据行,结果重新读取Excel无法和对象完成转换
ExcelHelper.Write([table], "Write.xls", false);
var exception1 = Assert.Throws<NotSupportedException>(() => ExcelHelper.Read<Student>("Write.xls", true, "Sheet0"));
Assert.Equal(message, exception1.Message); //把表格写入Excel,并且列名作为数据行,但是重新读取Excel时第一行没有作为列名,结果还是无法和对象完成转换
ExcelHelper.Write([table], "Write.xls", true);
var exception2 = Assert.Throws<NotSupportedException>(() => ExcelHelper.Read<Student>("Write.xls", false, "Sheet0"));
Assert.Equal(message, exception2.Message); //重新读取Excel时第一行作为列名
var models = ExcelHelper.Read<Student>("Write.xls", true, "Sheet0");
Assert.Equal(2, models.Count());
var model = models.First();
Assert.Equal("Id-11", model.A);
Assert.Equal("名称-12", model.Name);
Assert.Equal(new DateTime(2024, 11, 28), model.Age);
File.Delete("Write.xls");
}

07、把对象集合写入Excel文件流或Excel文件

到这里这两个方法就很好实现了,因为这两个方法需要的所有基础方法都已经实现,核心思路就是先把对象集合转换为表格DataTable,然后再通过调用相关把表格数组写入Excel的扩展方法实现即可,具体代码如下:

//把对象集合写入Excel文件流
public static MemoryStream Write<T>(IEnumerable<T> models, bool isXlsx, bool isColumnNameAsData, string? sheetName = null)
{
//对象集合转为表格
var table = TableHelper.ToDataTable<T>(models, sheetName);
//表格数组写入Excel文件流
return Write([table], isXlsx, isColumnNameAsData);
} //把对象集合写入Excel文件
public static void Write<T>(IEnumerable<T> models, string path, bool isColumnNameAsData, string? sheetName = null)
{
//对象集合转为表格
var table = TableHelper.ToDataTable<T>(models, sheetName);
//表格数组写入Excel文件
Write([table], path, isColumnNameAsData);
}

最后我们再进行一次详细的单元测试,代码如下:

[Fact]
public void Write_T()
{
//验证正常情况
var students = new List<Student>();
var student1 = new Student
{
A = "Id-11",
Name = "名称-12",
Age = new DateTime(2024, 11, 28)
};
students.Add(student1); var student2 = new Student
{
A = "Id-21",
Name = "名称-22",
Age = new DateTime(2024, 11, 29)
};
students.Add(student2); var message = "The column name of the table cannot be mapped to an object property, and the conversion cannot be completed."; //把对象集合写入Excel,并且列名不作为数据行,结果重新读取Excel无法和对象完成转换
ExcelHelper.Write<Student>(students, "Write_T.xls", false);
var exception1 = Assert.Throws<NotSupportedException>(() => ExcelHelper.Read<Student>("Write_T.xls", true, "Sheet0"));
Assert.Equal(message, exception1.Message); //把对象集合写入Excel,并且列名作为数据行,但是重新读取Excel时第一行没有作为列名,结果还是无法和对象完成转换
ExcelHelper.Write<Student>(students, "Write_T.xls", true);
var exception2 = Assert.Throws<NotSupportedException>(() => ExcelHelper.Read<Student>("Write_T.xls", false, "Sheet0"));
Assert.Equal(message, exception2.Message); //重新读取Excel时第一行作为列名
var models = ExcelHelper.Read<Student>("Write_T.xls", true, "Sheet0");
Assert.Equal(2, models.Count());
var model = models.First();
Assert.Equal("Id-11", model.A);
Assert.Equal("名称-12", model.Name);
Assert.Equal(new DateTime(2024, 11, 28), model.Age);
File.Delete("Write_T.xls");
}

到这里我们整个Excel封装就完成了,相信通过对象集合完成Excel导入导出能满足大多数业务开发需求。当然如果有更复杂的业务需求,还需要我们自己去研究相应的第三方库。

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Ideal

开源 - Ideal库 - Excel帮助类,ExcelHelper实现(五)的更多相关文章

  1. C# Excel操作类 ExcelHelper

    实现C#与Excel文件的交互操作,实现以下功能: 1.DataTable 导出到 Excel文件 2.Model数据实体导出到 Excel文件[List<Model>] 3.导出数据到模 ...

  2. [.Net] C# Excel操作类 ExcelHelper

    实现C#与Excel文件的交互操作,实现以下功能: 1.DataTable 导出到 Excel文件 2.Model数据实体导出到 Excel文件[List<Model>] 3.导出数据到模 ...

  3. C#EXCEL 操作类--C#ExcelHelper操作类

    主要功能如下1.导出Excel文件,自动返回可下载的文件流 2.导出Excel文件,转换为可读模式3.导出Excel文件,并自定义文件名4.将数据导出至Excel文件5.将指定的集合数据导出至Exce ...

  4. Python Excel工具类封装, 给excel表头搞点颜色

    封装Excel工具类 我们常用的excel工具类,读有xlrd,写有xlwt.有读有写,新一代库有pandas,openpyxl等等. 大家用法都差不多,今天博主就介绍新手最爱,我也爱的xlrd和xl ...

  5. Pugixml一种快速解析XML文件的开源解析库

    Pugixml是一个轻量级的C++ XML开源解析库,DOM形式的解析器.接口和丰富的遍历和修改操作,快速的解析,此外支持XPath1.0实现数据查询,支持unicode编码: 使用Pugixml可通 ...

  6. 导入导出Excel工具类ExcelUtil

    前言 前段时间做的分布式集成平台项目中,许多模块都用到了导入导出Excel的功能,于是决定封装一个ExcelUtil类,专门用来处理Excel的导入和导出 本项目的持久化层用的是JPA(底层用hibe ...

  7. MVC NPOI Linq导出Excel通用类

    之前写了一个模型导出Excel通用类,但是在实际应用中,可能不是直接导出模型,而是通过Linq查询后获取到最终结果再导出 通用类: public enum DataTypeEnum { Int = , ...

  8. asp.net(C#) Excel导出类 导出.xls文件

    ---恢复内容开始--- using Microsoft.Office.Interop.Excel; 针对office 2003需添加引用Microsoft   Excel   11.0   Obje ...

  9. 从Google开源RE2库学习到的C++测试方案

    最近因为科研需求,一直在研究Google的开源RE2库(正则表达式识别库),库源码体积庞大,用C++写的,对于我这个以前专供Java的人来说真的是一件很痛苦的事,每天只能啃一点点.今天研究了下里面用到 ...

  10. 爆料喽!!!开源日志库Logger的剖析分析

    导读 Logger类提供了多种方法来处理日志活动.上一篇介绍了开源日志库Logger的使用,今天我主要来分析Logger实现的原理. 库的整体架构图 详细剖析 我们从使用的角度来对Logger库抽茧剥 ...

随机推荐

  1. UWP 通过 .NET 9 和Native AOT 的支持实现 UWP 应用的现代化

    微软(9 月 11 日)发布博文,微软正在预览对 .NET 9 的 UWP(通用 Windows 平台)支持,为现有 UWP 开发人员提供一条使用最新的 .NET 和本机 AOT 实现其应用程序现代化 ...

  2. ASP.NET Core – Try Preview

    前言 .NET 7 已经来到 RC 阶段了. 通常 RC 就是我们 (写库的人) 要入场的时候了. 有发现 Bug 要尽可能在这段期间提交. 不然后患无穷. 这篇主要就是来讲讲如果测试 RC 版本的 ...

  3. 2024 IDEA开发者部署lilishop的manager模块(详尽版)

    一.环境整合 构建工具(参考工具部署方式) 软件名称 版本 相关文章推荐 Git 13.5.0 https://www.cnblogs.com/liuyangfirst/p/15996063.html ...

  4. SimpleRAG-v1.0.3:增加文件对话功能

    Kimi上有一个功能,就是增加文件之后对话,比如我有如下一个私有文档: 会议主题:<如何使用C#提升工作效率> 参会人员:张三.李四.王五 时间:2024.9.26 14:00-16:00 ...

  5. SuperMap流数据应用技术方案

    流数据应用技术方案针对流数据应用场景,针对流数据的海量.多源.持续等特征,进行持续地获取相关的动态位置,以及持续地分析.处理和挖掘. 本章沿用基于单机SuperMap iServer环境,介绍流数据处 ...

  6. composer 执行脚本时报错:Error: You made a reference to a non-existent script @php artisan package:discover

    composer 执行脚本时报错:Error: You made a reference to a non-existent script @php artisan package:discover. ...

  7. 数据库运维实操优质文章分享(含Oracle、MySQL等) | 2023年5月刊

    本文为大家整理了墨天轮数据社区2023年5月发布的优质技术文章,主题涵盖Oracle.MySQL.PostgreSQL等数据库的安装配置.故障处理.性能优化等日常实践操作,以及常用脚本.注意事项等总结 ...

  8. Spark任务OOM问题如何解决?

    大家好,我是 V 哥.在实际的业务场景中,Spark任务出现OOM(Out of Memory) 问题通常是由于任务处理的数据量过大.资源分配不合理或者代码存在性能瓶颈等原因造成的.针对不同的业务场景 ...

  9. 在华为云上安装高可用 KubeSphere

    随着多云多集群的场景越来越丰富,在各个云厂商环境部署 KubeSphere 的需求随之升高.由于各云厂商的云资源使用规则和菜单导航栏各不相同,会使用户花大量时间去排错.为降低部署过程错误率,本教程使用 ...

  10. Taylor series Explicit Euler Implicit Euler

    1 Taylor series \[\begin{gathered}\sum_{n=0}^{\infty}\frac{f^{(n)}\left(a\right)}{n!}\left(x-a\right ...