☰
CsvHelper 动态记录读取:使用 `GetRecords<dynamic>()` 将 CSV 行转换为动态对象
2026/10/6 7:51:46 网站建设 项目流程
  • 后端
  • 数据工程

【免费下载链接】CsvHelper

Library to help reading and writing CSV files

项目地址:https://gitcode.com/gh_mirrors/cs/CsvHelper
点击查看免费下载

导读

当 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; }

从中可以确认三个事实:

  1. 属性名来源于表头:有表头时,按表头列名(经过配置函数处理后)作为动态属性名,字段值直接取自当前行对应索引;
  2. 值始终为字符串:代码中使用out string? field与GetField(i),没有任何类型转换,印证了原文档"所有属性都是字符串"的约定;
  3. 无表头时按序号命名:没有表头记录时,使用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

项目地址:https://gitcode.com/gh_mirrors/cs/CsvHelper
点击查看免费下载
上一篇:绝区零自动化脚本终极指南:从"下班只想躺"到"全自动一条龙"的完整上手攻略
下一篇:网盘直链下载也能这么简单?一个免费脚本,八大网盘真实链接直接拿

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询