返回

C# ClosedXML导出Excel数据错误排查与解决方案

mysql

C# 使用 ClosedXML 导出 Excel 文件:数据错误排查

将数据导出到 Excel 是常见需求,C# 和 ClosedXML.Excel 是很好的工具组合。 但有时,导出结果并非预期,出现数据不一致的情况。本文将深入探讨一个典型场景,并给出解决方案。

问题

在进行 C# 开发时,使用 ClosedXML.Excel 导出 MySQL 数据到 XLSX 文件。数据库中通过存储过程获取的数据,在导出的 Excel 文件中却显示为数字,而非原本的文本或日期数据。实际数据库数据包含'_Date_Hour', '_ID', 和 '_Area' 等字段,但 Excel 文件却显示类似“44335”,“4833”,“5281”,无法识别的内容。

问题分析

问题的核心在于 ClosedXML 如何处理 DataSet 中的数据。MySqlDataAdapterFill 方法将数据填充到 DataSetDataTable 中。而问题在于,存储过程中使用 UNION ALL 连接多个 SELECT 语句时,SELECT 返回字段使用了 相同的别名 (Alias) 。比如, col1, col2, 以及后面的 _ID, _Area 在不同SELECT语句里有不同的意思,而所有列都设置成固定的别名了,导致返回的结果可能并非程序所预期的。

更深入地说 ,数据表内的每一列都通过序号被处理,col1, col2在所有结果集中都有索引0,1, 但是每次循环得到结果的数据类型,可能各不相同,造成 ClosedXML 将 DataTable 的第一列, 都默认理解成日期或者数值类型了。因此导出的结果,也就出现了乱码。ClosedXML 基于数据的类型进行转换,无法正确处理混合类型的列,进而产生格式上的错误。

解决方案

方法一:避免别名重复和混用,让每一列都有独特的意义

  1. 修改存储过程 :调整 SQL 语句,给每一个查询的列添加一个有意义且独一无二的别名。
  2. 保证数据类型一致性 :确保存储过程返回的数据类型与 C# 代码中期望的数据类型一致。这样才能在Excel中正确显示。

存储过程修改示例:

SELECT
    src.col1_date AS col1,
    src.col2_date AS col2
FROM
    (
    SELECT
        '_Date_Hour' as col1_date,
        _Date_Hour AS col2_date
    FROM
        check_list
    WHERE
        _ID = @ID 
    UNION ALL
    SELECT
        '_ID' AS col1_id,
         _ID AS col2_id
    FROM
        check_list
    WHERE
        _ID = @ID 
    UNION ALL
    SELECT
        '_Area' AS col1_area,
         _Area as col2_area
    FROM
        check_list
    WHERE
        _ID = @ID
    ) AS src;

通过修改别名,避免混淆和数据类型判断的错误。这样做之后, ClosedXML 就可以按照实际的数据类型导出正确结果。

方法二:处理DataTable中列的类型

  1. 强制转换数据类型 :遍历 DataTable 中的每一列,并根据实际情况将其类型显式设置为字符串类型或其它适合类型。通过明确列的类型,可以让 ClosedXML 正确处理。
 public static void ExportDataSetToExcel(DataSet ds)
  {
    string AppLocation = @"D:\inetpub\wwwroot\PUBLIC\ExcelFiles\";
    string guid = Guid.NewGuid().ToString().ToUpper().Replace("-", "_");

    HttpContext.Current.Response.Cookies["guid"].Value = guid.ToString();
    HttpContext.Current.Response.Cookies["guid"].Expires = DateTime.Now.AddMinutes(3);

    string filepath = AppLocation + "Export_" +
          DateTime.Now.ToString("ddMMyyyyHHmm") + "_" +
          HttpContext.Current.Response.Cookies["guid"].Value + ".xlsx";

      using (XLWorkbook wb = new XLWorkbook())
      {
        for (int i = 0; i < ds.Tables.Count; i++)
        {
          DataTable dt = ds.Tables[i];
          // 遍历所有列,把数据转换成string
            foreach (DataColumn column in dt.Columns) {
              if (column.DataType != typeof(String)) {
               column.DataType= typeof(String);
              }
            }
          wb.Worksheets.Add(dt, dt.TableName);
        }
          wb.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
          wb.Style.Font.Bold = true;
          wb.SaveAs(filepath);
          wb.Dispose();
    }

      Thread.Sleep(3000);
      HttpContext.Current.Response.Clear();
      HttpContext.Current.Response.Buffer = true;
      HttpContext.Current.Response.ContentType = "application/force-download";
      HttpContext.Current.Response.AppendHeader("Content-Disposition", "attachment; " +
            "filename=Export_" +
          DateTime.Now.ToString("ddMMyyyyHHmm") + "_" +
          HttpContext.Current.Response.Cookies["guid"].Value + ".xlsx");
    
      HttpContext.Current.Response.TransmitFile(@"D:\inetpub\wwwroot\PUBLIC\ExcelFiles\Export_" +
              DateTime.Now.ToString("ddMMyyyyHHmm") + "_" +
          HttpContext.Current.Response.Cookies["guid"].Value + ".xlsx");

      HttpCookie cookie = new HttpCookie("ExcelDownloadFlag")
    {
      Value = "Flag",
      Expires = DateTime.Now.AddDays(1)
    };

    HttpContext.Current.Response.AppendCookie(cookie);
      HttpContext.Current.Response.End();

  }

通过遍历 DataTable 并将所有列转换为 String 类型,保证数据能正常显示,避免由于 ClosedXML 类型推断错误而导致的数据显示问题。

额外的安全建议

  • 避免硬编码文件路径 : 将文件保存路径存储在配置文件中,提高灵活性和安全性。
  • 增加错误处理 : 在代码中加入 try-catch 语句,捕获可能出现的异常情况,例如文件写入失败等。 这样做可以提升代码健壮性。
  • 限制权限 :确保导出文件的存储位置仅限于授权的用户或进程访问。这可以降低潜在的安全风险。

正确处理数据库数据和 Excel 数据类型是导出正确结果的关键。使用这两种方法都可以有效地解决导出数据出现错误的问题。根据实际场景选择适合的方法,才能高效地使用 C# 和 ClosedXML 生成准确的 Excel 文件。