开源 - Ideal库 - Excel帮助类,ExcelHelper实现(五)

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

01、读取Excel到DataSet单元测试

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

csharp 复制代码
[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文件流的。

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

csharp 复制代码
//根据文件流读取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转换为对象集合;

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

csharp 复制代码
//根据文件流读取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]);
}

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

csharp 复制代码
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中即可,具体代码如下:

csharp 复制代码
//把表格数组写入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中相同的位置即可,当然其中表格列名是否要作为数据,需要单独处理,具体代码如下:

csharp 复制代码
//根据表格填充工作簿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文件中,具体代码如下:

csharp 复制代码
//把表格数组写入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);
}

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

csharp 复制代码
[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的扩展方法实现即可,具体代码如下:

csharp 复制代码
//把对象集合写入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);
}

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

csharp 复制代码
[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

相关推荐
cyhysr5 分钟前
oracle的model子句让sql像excel一样灵活2
sql·oracle·excel
yumgpkpm14 分钟前
(简略)AI 大模型 手机的“简单替换陷阱”与Hadoop、Cloudera CDP 7大数据底座的关系探析
人工智能·hive·zookeeper·flink·spark·kafka·开源
yumgpkpm15 分钟前
Cloudera CDP 7.3下载地址、方式,开源适配 CMP 7.3(或类 CDP 的 CMP 7.13 平台,如华为鲲鹏 ARM 版)值得推荐
大数据·hive·hadoop·分布式·华为·开源·cloudera
周杰伦_Jay12 小时前
【大模型数据标注】核心技术与优秀开源框架
人工智能·机器学习·eureka·开源·github
玄魂13 小时前
如何查看、生成 github 开源项目star 图表
前端·开源·echarts
隐语SecretFlow13 小时前
【隐语Secreflow】如何配置 Kuscia 对请求进行 Path Rewrit
架构·开源
hh.h.13 小时前
开源鸿蒙生态下Flutter的发展前景分析
flutter·开源·harmonyos
oh,huoyuyan14 小时前
【界面案例】火语言RPA读取Excel文件,循环写入界面表格
excel·rpa
一RTOS一18 小时前
光亚鸿道携手AGIROS开源社区,共筑中国具身智能机器人操作系统新生态
机器人·开源·鸿道实时操作系统·国产嵌入式操作系统选型·具身智能操作系统选型