需求
把 IEnumerable<T> 转成 DataTable 常见于老项目对接:
- 用
SqlBulkCopy把 DTO 列表批量写入数据库 - 用
DataSet喂给报表控件(旧 RDLC、FastReport) - 把对象导出为 CSV / Excel(NPOI)
完整实现
using System.Data;
using System.Reflection;
public static class EnumerableToDataTable
{
public static DataTable ToDataTable<T>(this IEnumerable<T> source)
{
var dt = new DataTable(typeof(T).Name);
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
// 第一步:根据属性创建列
foreach (var p in props)
{
var colType = p.PropertyType;
// Nullable<T> 取底层类型
if (colType.IsGenericType && colType.GetGenericTypeDefinition() == typeof(Nullable<>))
colType = Nullable.GetUnderlyingType(colType)!;
dt.Columns.Add(p.Name, colType);
}
// 第二步:填充数据
foreach (var item in source)
{
var row = dt.NewRow();
foreach (var p in props)
{
var v = p.GetValue(item);
row[p.Name] = v ?? DBNull.Value; // null → DBNull
}
dt.Rows.Add(row);
}
return dt;
}
}
使用:
var people = new[]
{
new { Id = 1, Name = "Alice", Birth = new DateTime(1990, 1, 1) },
new { Id = 2, Name = "Bob", Birth = (DateTime?)null },
};
var dt = people.ToDataTable();
容易踩的三个坑
1. Nullable<T> 没取底层类型
如果直接用 p.PropertyType 作为列类型,DateTime? 字段在 DataTable 里会变成 Nullable<DateTime>(依然 OK)。但混用:类里有 DateTime? 又有 int 时:
- 用底层类型
DateTime列,可以存DBNull与非空值; - 用
Nullable<DateTime>列也行——DataTable内部对Nullable<T>是支持的。
关键是要显式取底层类型,否则对接 SqlBulkCopy 时某些数据库驱动会拒绝 Nullable<T> 列。
2. null 没转 DBNull.Value
DataRow 的列不接受 C# 的 null,必须用 DBNull.Value。否则:
row[p.Name] = v ?? DBNull.Value; // ✅ 正确
row[p.Name] = v; // ❌ "The value for Column ... is DBNull."
3. 用匿名类型时反射拿不到属性
匿名类型 new { Id = 1, ... } 在同一程序集内反射 GetProperties 是拿得到的(编译器把它们生成成 internal 类型 + 属性)。但跨程序集或动态加载场景会失败——这时改用具名类。
性能优化
反射取属性列表每次都走 GetProperties,大数据量(百万级)会变慢。两种优化:
缓存属性
private static readonly ConcurrentDictionary<Type, PropertyInfo[]> _cache = new();
static PropertyInfo[] GetProps(Type t) =>
_cache.GetOrAdd(t, type => type.GetProperties(BindingFlags.Public | BindingFlags.Instance));
委托代替 GetValue
// 编译期把 (T, PropertyInfo) 转成 (T) → object
static Func<T, object> MakeGetter<T>(PropertyInfo p)
{
var param = Expression.Parameter(typeof(T));
var access = Expression.Property(param, p);
return Expression.Lambda<Func<T, object>>(
Expression.Convert(access, typeof(object)), param).Compile();
}
单元测试
[Fact]
public void Should_Handle_Nullable_And_Nulls()
{
var src = new[]
{
new Person { Id = 1, Name = "A", Birth = new DateTime(2000, 1, 1) },
new Person { Id = 2, Name = "B", Birth = null },
};
var dt = src.ToDataTable();
Assert.Equal(2, dt.Rows.Count);
Assert.Equal(typeof(int), dt.Columns["Id"].DataType);
Assert.Equal(typeof(DateTime), dt.Columns["Birth"].DataType);
Assert.Equal(DBNull.Value, dt.Rows[1]["Birth"]);
}
record Person { public int Id; public string Name; public DateTime? Birth; }
使用准则
- 用
Nullable.GetUnderlyingType取底层类型,列类型统一一致。 - 始终
v ?? DBNull.Value,不要相信调用方会过滤 null。 - 大数据量场景:缓存属性 + 用表达式树生成 getter。
- 导出
SqlBulkCopy前关闭DataTable的约束 / 索引,避免写库时再做无意义的校验。