- 后端
- 数据工程
【免费下载链接】CsvHelper
Library to help reading and writing CSV files
导读
当 CSV 的列结构在编译期未知、或者你希望跳过为每张表定义 POCO 类的步骤时,CsvHelper 提供了GetRecords<dynamic>()这一动态记录读取方案:它把每一行 CSV 数据转换为运行时动态对象,属性名直接取自表头。读完本文,你将掌握动态记录的基本用法、属性值的类型规则(全部为字符串)、无表头文件的字段命名规则,以及如何通过GetDynamicPropertyName、PrepareHeaderForMatch等配置扩展动态对象的命名行为,并理解其底层基于FastDynamicObject的实现原理。
本文关联的原文档为 get-dynamic-records,属于 CsvHelper 网站 reading 示例系列 的一部分。
一、核心概念:CSV 行与dynamic对象
CsvHelper 支持把 CSV 行转换为多种记录形式:强类型类、匿名类型、dynamic动态对象。其中动态对象适用于列结构不确定或需要通用处理的场景。
关键约定(来自原文档):
- 每一行 CSV 数据都会被转换为一个
dynamic对象; - 由于编译期无法获知属性的真实类型,动态对象上的所有属性值一律是字符串。
也就是说,即使 CSV 单元格里写的是1,通过dynamic读取得到的也是字符串"1",不会自动转换为int。如果需要类型转换,应当使用强类型类配合类型转换器(参见 type-conversion 系列)。
二、最简示例:把 CSV 行读成动态对象
原文档给出了如下最小可运行示例。假设磁盘上有文件file.csv,内容为:
Id,Name 1,one读取代码:
void Main() { using (var reader = new StreamReader("path\\to\\file.csv")) using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { var records = csv.GetRecords<dynamic>(); } }随后可以像访问普通对象那样访问每一行:
foreach (dynamic record in records) { Console.WriteLine(record.Id); // 输出: 1 Console.WriteLine(record.Name); // 输出: one }要点说明:
CultureInfo.InvariantCulture作为配置传入CsvReader,这是 CsvHelper 文档示例中的标准写法,用于避免区域性设置影响解析;using同时包裹StreamReader与CsvReader,确保资源释放;records是IEnumerable<dynamic>,支持foreach直接遍历。
三、惰性求值:GetRecords<dynamic>()的延迟执行特性
GetRecords<T>()返回的是按需生成的迭代器,方法体只有在真正访问记录(如调用.ToList()或foreach)时才执行。这一点在源码注释中明确警告(CsvReader.cs):
"
GetRecords<T>()returns an IEnumerable that yields records. This means that the method isn't actually called until you try and access the values. e.g..ToList()"
由此带来的实际影响是:如果你在using块内创建CsvReader,却在using块之外去遍历records,会抛出ObjectDisposedException——因为迭代发生时 reader 已被释放。正确做法是在using块内部完成消费:
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { var records = csv.GetRecords<dynamic>().ToList(); // 在 using 内完成求值 // 之后可以在块外使用 records }另外从源码可见(CsvReader.cs),GetRecords<T>()会在读取记录前自动消费表头:当配置开启表头记录(hasHeaderRecord且尚未读取)时,先调用Read()与ReadHeader()并执行ValidateHeader<T>()。因此使用GetRecords<dynamic>()时无需手动先调用ReadHeader()。
四、源码视角:动态记录是如何创建的
理解底层实现有助于把握动态对象的行为边界。CsvHelper 通过表达式工厂按记录类型选择不同的创建器(RecordCreatorFactory.cs):
- 类型为基元类型(primitive)时使用
PrimitiveRecordCreator; - 类型为
typeof(object)时使用DynamicRecordCreator——而dynamic在运行时正是以object传递的,所以GetRecords<dynamic>()走的是这一分支; - 其余类型使用
ObjectRecordCreator。
DynamicRecordCreator.CreateDynamicRecord()的具体逻辑(DynamicRecordCreator.cs):
protected virtual dynamic CreateDynamicRecord() { var obj = new FastDynamicObject(); var dict = obj as IDictionary<string, object?>; if (Reader.HeaderRecord != null) { for (var i = 0; i < Reader.HeaderRecord.Length; i++) { var args = new GetDynamicPropertyNameArgs(i, Reader.Context); var propertyName = Reader.Configuration.GetDynamicPropertyName(args); Reader.TryGetField(i, out string? field); dict[propertyName] = field; } } else { for (var i = 0; i < Reader.Parser.Count; i++) { var args = new GetDynamicPropertyNameArgs(i, Reader.Context); var propertyName = Reader.Configuration.GetDynamicPropertyName(args); var field = Reader.GetField(i); dict[propertyName] = field; } } return obj; }从中可以确认三个事实:
- 属性名来源于表头:有表头时,按表头列名(经过配置函数处理后)作为动态属性名,字段值直接取自当前行对应索引;
- 值始终为字符串:代码中使用
out string? field与GetField(i),没有任何类型转换,印证了原文档"所有属性都是字符串"的约定; - 无表头时按序号命名:没有表头记录时,使用
GetDynamicPropertyName的默认实现生成Field1、Field2、Field3…… 依次递增(见下文)。
动态对象本体是FastDynamicObject(FastDynamicObject.cs),它同时实现了IDynamicMetaObjectProvider与IDictionary<string, object?>:前者通过DynamicMetaObject绑定实现record.Id这样的动态成员访问,后者使动态对象可以被当作字典遍历。二者组合保证了"既能按属性访问、也能按集合处理"的灵活性。
五、属性命名规则的定制
动态属性的命名不是写死的,而是由配置委托GetDynamicPropertyName控制(CsvConfiguration.cs,委托签名见 GetDynamicPropertyName.cs)。默认实现位于 ConfigurationFunctions.cs:
public static string GetDynamicPropertyName(GetDynamicPropertyNameArgs args) { if (args.Context.Reader?.HeaderRecord == null) { return $"Field{args.FieldIndex + 1}"; } var header = args.Context.Reader.HeaderRecord[args.FieldIndex]; var prepareHeaderForMatchArgs = new PrepareHeaderForMatchArgs(header, args.FieldIndex); header = args.Context.Reader.Configuration.PrepareHeaderForMatch(prepareHeaderForMatchArgs); return header; }即默认规则是:有表头时,属性名 = 表头经PrepareHeaderForMatch处理后的结果;无表头时,属性名 =Field1、Field2……(索引从 1 开始)。
利用这一点可以衍生出三种实用定制:
5.1 通过PrepareHeaderForMatch清理表头字符
表头中若含空格等不便作为属性名的字符,可统一清洗。测试用例 DynamicTests.cs 展示了将"O ne,Tw o,Thr ee"去掉空格后得到records[0].One、.Two、.Three的验证:
var config = new CsvConfiguration(CultureInfo.InvariantCulture) { PrepareHeaderForMatch = args => args.Header.Replace(" ", string.Empty), }; var records = csv.GetRecords<dynamic>().ToList(); Assert.Equal("1", records[0].One); Assert.Equal("2", records[0].Two); Assert.Equal("3", records[0].Three);5.2 为空白表头生成占位属性名
表头可能为空列,默认实现下属性名会变成空字符串,访问不便。可通过PrepareHeaderForMatch为空白表头回退生成Blank{index}(见 DynamicTests.cs):
PrepareHeaderForMatch = args => { if (string.IsNullOrWhiteSpace(args.Header)) { return $"Blank{args.FieldIndex}"; } return args.Header; },5.3 通过GetDynamicPropertyName处理重复列名
当 CSV 中存在重复表头(如两个Name列)时,默认属性名会冲突。测试用例 DynamicTests.cs 展示了结合GetDynamicPropertyName与列统计,把第二个重复列重命名为Name1、Name2的做法:
var headerNameCounts = new Dictionary<string, int>(); var config = new CsvConfiguration(CultureInfo.InvariantCulture) { GetDynamicPropertyName = args => { var header = args.Context.Reader?.HeaderRecord?[args.FieldIndex] ?? string.Empty; // ...先经 PrepareHeaderForMatch 处理... var name = headerNameCounts[header] > 1 ? $"{header}{args.FieldIndex}" : header; return name; }, };GetDynamicPropertyNameArgs提供FieldIndex(当前列索引)与Context(读取上下文),足以支撑以上各种按列位置定制的命名逻辑。
六、适用场景与注意事项小结
适合使用GetRecords<dynamic>()的场景:
- CSV 列结构不确定、或同一批数据存在多种表头变体;
- 只想快速预览/探查文件内容,不想为每张表定义类;
- 需要把表头与值以键值对形式统一处理(因为
FastDynamicObject同时实现IDictionary<string, object?>,可强制转换为字典遍历)。
需要注意的边界:
- 属性值全部是字符串,数字、日期等需要自行转换;
- 属性名来源于表头,若表头不规范(含空格、重复、为空),应按上文方式配置命名委托;
GetRecords<dynamic>()是惰性迭代,务必在using块内完成求值,避免ObjectDisposedException;- 需要类型转换、默认值、索引映射等强类型能力时,应改用 get-class-records 或 mapping-by-name 等方案。
七、延伸阅读
- 原文档:Get Dynamic Records
- 同类示例:读取强类型记录 get-class-records、读取匿名类型记录 get-anonymous-type-records
- 核心实现:DynamicRecordCreator.cs、RecordCreatorFactory.cs、FastDynamicObject.cs
- 命名配置:CsvConfiguration.cs、ConfigurationFunctions.cs、GetDynamicPropertyName.cs
- 测试验证:DynamicTests.cs
- 后端
- 数据工程
【免费下载链接】CsvHelper
Library to help reading and writing CSV files
相关推荐
Wand-Enhancer 本地补丁实测:不花一分钱激活 WeMod 专业版的完整上手指南
Wand Enhancer 本地补丁实测:不花一分钱激活 WeMod 专业版的完整上手指南 WeMod 专业版必须年费订阅吗?不一定。Wand Enhancer
后端数据工程CsvHelper 使用 GetRecords<T> 将 CSV 行转换为类对象:完整实战指南
CsvHelper 使用 GetRecords<T 将 CSV 行转换为类对象:完整实战指南 导读 本文围绕 CsvHelper 的 GetRecords<T
后端数据工程CsvHelper 写入动态对象:使用 ExpandoObject 与 IDynamicMetaObjectProvider 输出 CSV
CsvHelper 写入动态对象:使用 ExpandoObject 与 IDynamicMetaObjectProvider 输出 CSV 本指南聚焦 CsvH
后端数据工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考