1. 背景介绍
在项目中,我们经常需要查询数据库中的数据并导出为 Excel 进行分析或存档。本篇文章介绍如何在 C# 中使用 Dapper 查询 SQL Server 数据,并通过 EPPlus 生成 Excel 文件,支持 动态对象 (ExpandoObject
) 和静态实体对象 的数据导出。
2. 代码结构概览
该实现包括以下部分:
- 数据库查询 (
DapperHelper.Query
) :执行 SQL 查询并返回DynamicParameters
形式的结果。 - 数据转换 (
ExpandoObject
):查询结果转换为动态对象。 - 静态对象列表 (
Person
):示例数据。 - Excel 导出 (
ExcelHelper.ExportToExcel
) :将数据转换为 Excel 文件,支持 动态对象和静态对象,自动适配嵌套属性。
3. 代码实现
cs
Dictionary<string, object> keyValuePairs = new();
- 为了方便我们进行多个Sheet和多个数据查询,我这边会把多个数据存储到一个键值对里面,键值对的Key是Sheet的名字,Value是我们查出来的静态或者动态实体内容的集合
3.1 使用 Dapper 查询数据库
cs
public static async Task<IEnumerable<DynamicParameters>> Query(string sql, object? @params = null, int? commandTimeout = null, CommandType? commandType = null)
{
string ConnStr = AppConfig.SqlLinks;
using (var con = new SqlConnection(ConnStr))
{
var results = await con.QueryAsync(sql, @params, commandTimeout: commandTimeout, commandType: commandType);
return results.Select(r => new DynamicParameters(r));
}
}
- 该方法使用 Dapper 查询数据库,并返回
DynamicParameters
。 - 连接字符串从
AppConfig.SqlLinks
获取。 - 我们在不想创建实体对象的情况下直接选择把内容转化为动态实体类型
3.2 查询 orders
表并转换为动态对象
cs
string sql = "Select * From [1booking].[dbo].[orders] With(NoLock)";
var results = await DapperHelper.Query(sql);
if (results.Count() > 0 && results != null)
{
var objects = results.Select(r =>
{
var expando = new ExpandoObject();
var dict = (IDictionary<string, object>)expando!;
foreach (var key in r.ParameterNames)
{
dict[key] = r.Get<object>(key);
}
return expando;
});
}
- 查询
orders
表,并转换查询结果为 动态对象 (ExpandoObject
)。 - 我们把他转换为动态对象之后,后续就可以使用它来直接展示
- 把他直接存储键值对中,这个肯定都会存储,我这边就少些一行代码了
3.3 示例:静态对象列表 Person
cs
var people = new List<Person>
{
new Person
{
Id = 1,
Name = "张三",
BirthDate = new DateTime(1990, 1, 1),
Salary = 5000.50m,
Address = new Address { City = "北京", State = "北京" },
Info = new Info {A = "2", B = "C"}
},
new Person
{
Id = 2,
Name = "李四",
BirthDate = new DateTime(1985, 5, 12),
Salary = 8000.75m,
Address = new Address { City = "上海", State = "上海" },
Info = new Info {A = "1", B = "B"}
}
};
- 定义 静态对象
Person,这里都是我的示例,举例说明的,可以是任何静态对象多层嵌套也是没问题的
。 - 把他直接存储键值对中,这个肯定都会存储,我这边就少些一行代码了
3.4 调用 Excel 导出方法
cs
var excelData = ExcelHelper.ExportToExcel<Task>(keyValuePairs);
- 通过
ExcelHelper.ExportToExcel<T>
方法,将keyValuePairs
导出为 Excel。 - 在这一步也就是调用我们主要的实现方法,这个方法里面主要是处理Excel和我们的实体区分处理。以实现全自动处理不区分类型
3.5 Excel 导出实现 (ExcelHelper
)
cs
public static byte[] ExportToExcel<T>(Dictionary<string, object> dataSets)
{
// 首先我们不能让我们的集合是空的
if (dataSets == null || !dataSets.Any())
{
throw new ArgumentException("数据列表不能为空");
}
// 因为这个分商用版和个人版本,一个收费一个不收费,所以我选择个人使用
ExcelPackage.LicenseContext = OfficeOpenXml.LicenseContext.NonCommercial;
// 创建一个处理Excel的生命周期
using (var package = new ExcelPackage())
{
// 这一步也就是我上面所说的,如果有多个集合进行区分Sheet处理不同Sheet存放不同集合数据
foreach (var dataSet in dataSets)
{
var sheetName = dataSet.Key;
var data = dataSet.Value;
var worksheet = package.Workbook.Worksheets.Add(sheetName);
if (data is IEnumerable<ExpandoObject> dynamicList)
{// 这一步主要是处理动态集合元素
// 获取动态集合的属性,这里一般动态元素的类型我们是获取不到的都是null
var properties = GetAllProperties(dynamicList.FirstOrDefault(), typeof(T));
for (int i = 0; i < properties.Count; i++)
{
// 写入第一行属性名称
worksheet.Cells[1, i + 1].Value = properties[i].DisplayName ?? properties[i].PropertyName;
}
for (int row = 0; row < dynamicList.Count(); row++)
{
var item = dynamicList.ElementAt(row);
for (int col = 0; col < properties.Count; col++)
{
// 这一步用来处理嵌套属性
worksheet.Cells[row + 2, col + 1].Value = GetNestedPropertyValue(item, properties[col].PropertyName);
}
}
}
else if (data is IEnumerable<object> list)
{// 这一步主要是处理静态集合元素
// 获取静态集合的属性,像是静态属性,这里是可以获取到的
var properties = GetAllProperties(list.FirstOrDefault(), typeof(T));
for (int i = 0; i < properties.Count; i++)
{
// 写入第一行属性名称
worksheet.Cells[1, i + 1].Value = properties[i].DisplayName ?? properties[i].PropertyName;
}
int row = 2;
foreach (var item in list)
{
for (int col = 0; col < properties.Count; col++)
{
// 写入内容
worksheet.Cells[row, col + 1].Value = GetNestedPropertyValue(item, properties[col].PropertyName);
}
row++;
}
}
// 自动调整列宽
worksheet.Cells.AutoFitColumns();
}
// 返回Excel文件的字节数组
return package.GetAsByteArray();
}
}
cs
private static List<PropertyMetadata> GetAllProperties(object obj, Type type = null, string prefix = "")
{
// 处理 动态属性
if (obj is IDictionary<string, object> dict)
{
// 遍历 ExpandoObject 的键值对
foreach (var key in dict.Keys)
{
// 添加属性元数据(ExpandoObject 没有 PropertyInfo)
properties.Add(new PropertyMetadata(null, key, key));
}
}
// 处理静态属性
else
{
// 获取类型的公共实例属性
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
// 构造属性名称(考虑前缀)
var propertyName = string.IsNullOrEmpty(prefix) ? prop.Name : $"{prefix}.{prop.Name}";
// 处理基本类型属性
if (prop.PropertyType.IsPrimitive || prop.PropertyType == typeof(string) || prop.PropertyType == typeof(DateTime) || prop.PropertyType == typeof(decimal))
{
// 获取显示名称(DisplayNameAttribute)
var displayName = prop.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName;
// 添加属性元数据
properties.Add(new PropertyMetadata(prop, propertyName, displayName));
}
// 处理嵌套对象属性
else if (prop.PropertyType.IsClass)
{
// 递归获取嵌套属性
properties.AddRange(GetAllProperties(null, prop.PropertyType, propertyName));
}
}
}
// 返回属性元数据列表
return properties;
}
cs
/// <summary>
/// 属性名称(包括前缀)
/// </summary>
public string PropertyName { get; }
/// <summary>
/// 属性显示名称(DisplayNameAttribute)
/// </summary>
public string DisplayName { get; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="propertyInfo">PropertyInfo 对象(如果是基本类型,则为 null)</param>
/// <param name="propertyName">属性名称(包括前缀)</param>
/// <param name="displayName">属性显示名称(DisplayNameAttribute)</param>
public PropertyMetadata(PropertyInfo propertyInfo, string propertyName, string displayName)
{
PropertyInfo = propertyInfo;
PropertyName = propertyName;
DisplayName = displayName;
}
cs
private static object GetNestedPropertyValue(object obj, string propertyName)
{
// 分割属性名称(支持嵌套属性)
var props = propertyName.Split('.');
// 初始化属性值
object value = obj;
// 遍历属性名称
foreach (var prop in props)
{
// 检查属性值是否为空
if (value == null)
{
return null; // 如果属性值为空,则返回 null
}
// 处理 动态属性,一般这个区分不了类型,后续可以在写入文档的时候区分类型
if (value is IDictionary<string, object> dict)
{
// 尝试获取属性值
dict.TryGetValue(prop, out value);
}
else // 处理静态属性
{
// 获取属性信息
var propertyInfo = value.GetType().GetProperty(prop);
// 检查属性是否存在
if (propertyInfo == null)
{
return null; // 如果属性不存在,则返回 null
}
// 获取属性值
value = propertyInfo.GetValue(value);
}
}
// 返回属性值
return value;
}
- 自动解析动态对象和静态对象
- 支持嵌套对象属性导出
- 格式化 Excel,自动调整列宽
3.6 最后输出结果展示
4. 总结
📌 完整流程
- 查询数据库 (
DapperHelper.Query
)。 - 处理动态对象 (
ExpandoObject
) ,并存入keyValuePairs
。 - 存储静态对象 (
Person
) ,并存入keyValuePairs
。 - 调用
ExcelHelper.ExportToExcel
生成 Excel。 - 支持静态 & 动态数据导出,自动解析嵌套对象。
🎯 适用场景
✅ 大批量数据导出
✅ 动态数据结构(ExpandoObject
)
✅ 支持嵌套对象
希望这篇文章能帮助你在 C# 项目中高效导出 Excel 数据!🚀