☰
CsvHelper 自定义类型转换器(Custom Type Converters)完整实战指南
2026/10/6 2:29:45 网站建设 项目流程
  • 后端
  • 数据工程

【免费下载链接】CsvHelper

Library to help reading and writing CSV files

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

CsvHelper 内置的类型转换器已经覆盖了绝大多数 .NET 基础类型与集合类型,但当你需要处理JsonNode、自定义值对象等「内置转换器不认识」的类型时,就需要自己编写并注册一个自定义类型转换器。本文以 JSON 字段反序列化为例,完整讲解自定义转换器的写法、全局 / 属性 / 类映射三种注册方式,并结合 ITypeConverter.cs、DefaultTypeConverter.cs 与 TypeConverterCache.cs 等源码,说明转换器被选中的底层机制与失败处理策略。读完后你将能独立实现、注册并调试任意业务类型与 CSV 字段之间的双向转换。

为什么需要自定义类型转换器

在读取和写入 CSV 时,CSV 的每一个字段本质上都是字符串,而类的每个属性都可能是任意 CLR 类型。CsvHelper 正是通过**类型转换器(Type Converter)**来完成「字符串 ↔ 属性值」的转换:读取时调用ConvertFromString把字段文本转成对象,写入时调用ConvertToString把对象序列化回文本。

CsvHelper 已经内置了大量转换器,覆盖绝大多数常见场景,完整清单可见 type-conversion/index.md,例如:

| CsvHelper 转换器 | C# 类型关键字 | .NET 类型 | | - | - | - | | ArrayConverter | [ ] | System.Array | | BigIntegerConverter | | System.Numerics.BigInteger | | BooleanConverter | bool | System.Boolean | | ByteArrayConverter | byte[ ] | System.Array | | ByteConverter | byte | System.Byte | | CharConverter | char | System.Char | | CollectionGenericConverter | | System.Collections.Generic.Collection<T>、List<T> | | DateOnlyConverter | | System.DateOnly | | DateTimeConverter | | System.DateTime | | DateTimeOffsetConverter | | System.DateTimeOffset | | DecimalConverter | decimal | System.Decimal | | DoubleConverter | double | System.Double | | EnumConverter | enum | System.Enum | | GuidConverter | | System.Guid | | IDictionaryConverter | | Dictionary<string, string> | | IDictionaryGenericConverter | | Dictionary<TKey, TValue> | | IEnumerableConverter | | ICollection、IEnumerable、IList | | IEnumerableGenericConverter | | ICollection<T>、IEnumerable<T>、IList<T> | | Int16Converter | short | System.Int16 | | Int32Converter | int | System.Int32 | | Int64Converter | long | System.Int64 | | NullableConverter | | System.Nullable<T> | | SByteConverter | sbyte | System.SByte | | SingleConverter | float | System.Single | | StringConverter | string | System.String | | TimeOnlyConverter | | System.TimeOnly | | UInt16Converter | ushort | System.UInt16 | | UInt32Converter | uint | System.UInt32 | | UInt64Converter | ulong | System.UInt64 | | UriConverter | | System.Uri |

这些内置转换器由 TypeConverterCache.cs 中的CreateDefaultConverters()在构造缓存时自动注册。但一旦遇到内置转换器无法处理的类型——例如本文示例中的System.Text.Json.Nodes.JsonNode——就需要自己实现转换器并注册。你可以按需选择三种注册方式之一:全局注册、成员属性注册、类映射注册;官方文档建议「三种方式只需要用其中一种」,示例代码中为了演示则全部用上了。

自定义转换器的基础:接口与基类

在动手写转换器之前,先了解 CsvHelper 定义的最小契约。所有类型转换器都实现 ITypeConverter.cs 接口,它只声明两个方法:

public interface ITypeConverter { // 读取时调用:把 CSV 字段文本转换为对象 object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData); // 写入时调用:把对象转换回字符串 string? ConvertToString(object? value, IWriterRow row, MemberMapData memberMapData); }

实际开发中通常不需要从零实现该接口,而是继承以下两个基类之一:

  • DefaultTypeConverter:实现了接口的全部方法,但ConvertFromString默认直接失败(详见下文「转换失败的处理」)。自定义转换器通常只重写ConvertFromString,即可获得完整的默认写入能力。官方示例正是这种写法。
  • TypeConverter<T>(见 TypeConverter.cs):强类型的泛型抽象基类,需要同时实现abstract T? ConvertFromString(...)与abstract string? ConvertToString(T? value, ...),两个方向都必须自己写。

编写自定义转换器:JsonNode 示例

官方文档给出的典型场景是:CSV 的某一列存放 JSON 字符串,希望读取后直接映射为System.Text.Json.Nodes.JsonNode对象。示例数据如下:

Id,Name,Json 1,one,"{""foo"": ""bar""}"

注意 CSV 中 JSON 字符串内的双引号需要写成两个""进行转义,解析后得到的Json字段文本是{"foo": "bar"}。

转换器本身非常简单——继承DefaultTypeConverter,只重写ConvertFromString这一个方法,用JsonSerializer.Deserialize<JsonNode>完成解析:

public class JsonNodeConverter : DefaultTypeConverter { public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData) { return JsonSerializer.Deserialize<JsonNode>(text); } }

由于继承了DefaultTypeConverter,写入方向无需任何代码:ConvertToString的默认实现会先判断value == null(此时若配置了NullValues则返回首个空值标记,否则返回空字符串),再判断值是否实现IFormattable(按TypeConverterOptions.Formats与CultureInfo格式化),最后回退到ToString()——JSON 节点序列化后的字符串会被原样写回 CSV,完全符合预期(详见 DefaultTypeConverter.cs)。

三种注册方式详解

方式一:全局注册(TypeConverterCache.AddConverter)

在创建CsvReader之后,通过csv.Context.TypeConverterCache把转换器与目标类型JsonNode绑定。此后整个上下文范围内所有JsonNode类型的成员都会使用该转换器:

using (var reader = new StreamReader("path\\to\\file.csv")) using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { // 全局注册:所有 JsonNode 类型的字段都走 JsonNodeConverter csv.Context.TypeConverterCache.AddConverter<JsonNode>(new JsonNodeConverter()); csv.Context.RegisterClassMap<FooMap>(); csv.GetRecords<Foo>().ToList().Dump(); }

从 TypeConverterCache.cs 可以看到,AddConverter<T>(ITypeConverter)本质是向内部字典写入typeConverters[typeof(T)] = typeConverter,覆盖同类型的默认转换器。除泛型重载外,还提供AddConverter(Type, ITypeConverter)、AddConverter(ITypeConverter)(为所有已注册类型统一替换)以及对应的RemoveConverter方法。

方式二:成员属性注册([TypeConverter])

直接在属性上标注[TypeConverter(typeof(JsonNodeConverter))]特性,仅对这一个成员生效:

public class Foo { public int Id { get; set; } public string Name { get; set; } // 注册 via attribute:仅 Json 属性使用该转换器 [TypeConverter(typeof(JsonNodeConverter))] public JsonNode Json { get; set; } }

这里的TypeConverterAttribute位于 Attributes/TypeConverterAttribute.cs。它同时实现了IMemberMapper与IParameterMapper,因此不仅可用于属性 / 字段,还可以用于构造函数参数;构造时通过ObjectResolver.Current.Resolve(typeConverterType)解析转换器实例,如果传入的类型没有实现ITypeConverter会抛出ArgumentException。

方式三:类映射注册(Map + TypeConverter )

在ClassMap<Foo>中为指定成员调用TypeConverter<JsonNodeConverter>():

public class FooMap : ClassMap<Foo> { public FooMap() { Map(m => m.Id); Map(m => m.Name); // 注册 via map Map(m => m.Json).TypeConverter<JsonNodeConverter>(); } }

MemberMap<TClass, TMember>.TypeConverter<TConverter>()的实现(见 [MemberMap1.cs](https://link.gitcode.com/i/20e929ab11203d3b41ebc18892d7a19f))通过ObjectResolver.Current.Resolve ()实例化转换器,再写入映射数据MemberMapData.TypeConverter`。

三种方式如何选择

  • 全局注册:目标类型在项目中各处含义一致(如统一反序列化为JsonNode),使用频率最高、最省事;
  • 属性注册:目标类型本身不确定转换规则,但某个成员有特殊需求,声明式、零配置代码;
  • 类映射注册:已经使用ClassMap管理映射关系时最自然,把所有映射规则集中在一处,便于查看和维护。

官方文档明确指出「只需要使用其中一种」,三种方式并用只是为了演示各自的写法。

底层原理:CsvHelper 如何选中你的转换器

理解注册背后的选择逻辑,有助于排查「为什么我的转换器没生效」这类问题。核心是 TypeConverterCache.cs 中两个GetConverter重载:

  1. GetConverter(MemberInfo)(TypeConverterCache.cs):先检查成员上是否标注了TypeConverterAttribute——属性注册优先级最高,命中后直接返回该特性指定的转换器;
  2. 若成员上没有属性,则退而调用GetConverter(Type)(TypeConverterCache.cs):先在字典缓存中查找(即全局注册的转换器、以及内置默认转换器);未命中时,依次询问用户注册的ITypeConverterFactory和内置工厂(EnumConverterFactory、NullableConverterFactory、CollectionConverterFactory)能否创建该类型;工厂创建成功后还会把结果写回缓存以便复用;最终仍无法匹配时,返回一个DefaultTypeConverter实例作为兜底。

由此可见完整的优先级链路是:成员属性 > 全局缓存注册 > 类型转换器工厂 > DefaultTypeConverter 兜底。这也是为什么属性注册的效果最局部也最优先。

此外,ITypeConverterFactory(见 ITypeConverterFactory.cs)是面向一族类型的扩展点:内置工厂正是靠它让枚举、可空类型、集合类型共享一套转换逻辑。如果你的自定义转换器需要批量服务于某类类型(而不是单一类型),实现CanCreate/Create两个方法并通过TypeConverterCache.AddConverterFactory注册即可,工厂会「按注册顺序最先匹配者生效」。

转换失败怎么办:默认基类的失败处理

DefaultTypeConverter.ConvertFromString的默认实现并不是「尝试转换」,而是直接抛出TypeConverterException——除非映射数据中配置了默认值。具体逻辑(见 DefaultTypeConverter.cs):

  • 若memberMapData.UseDefaultOnConversionFailure为false或未设置Default值,直接抛出异常;
  • 若设置了Default,则优先返回配置的默认值(对允许为null的类型,默认值本身可为null);
  • 若默认值类型与成员类型不兼容,仍会抛出TypeConverterException。

抛出的TypeConverterException消息中包含字段文本、成员名、成员类型与转换器类型等信息;并且当ExceptionMessagesContainRawData配置为false时,出于安全考虑字段原文会被替换为"Hidden because ExceptionMessagesContainRawData is false."(见 DefaultTypeConverter.cs)。

因此在实现自己的ConvertFromString时,如果解析可能失败,请明确选择策略:要么直接抛出异常(由ReadingExceptionOccurred等回调处理),要么返回默认值,要么配合UseDefaultOnConversionFailure/Default配置让基类逻辑接管。

完整的可运行示例

把文档示例合并成一段完整代码(略去了Dump()这类 LINQPad 特有调用):

using System.Text.Json.Nodes; using CsvHelper; using CsvHelper.Configuration; using CsvHelper.TypeConversion; using System.Globalization; // 1. 自定义转换器:继承 DefaultTypeConverter,只重写读取方向 public class JsonNodeConverter : DefaultTypeConverter { public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData) { return JsonSerializer.Deserialize<JsonNode>(text); } } public class Foo { public int Id { get; set; } public string Name { get; set; } // 注册方式二:属性 [TypeConverter(typeof(JsonNodeConverter))] public JsonNode Json { get; set; } } public class FooMap : ClassMap<Foo> { public FooMap() { Map(m => m.Id); Map(m => m.Name); // 注册方式三:类映射 Map(m => m.Json).TypeConverter<JsonNodeConverter>(); } } public class Program { public static void Main() { using var reader = new StreamReader("path\\to\\file.csv"); using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); { // 注册方式一:全局 csv.Context.TypeConverterCache.AddConverter<JsonNode>(new JsonNodeConverter()); csv.Context.RegisterClassMap<FooMap>(); var records = csv.GetRecords<Foo>().ToList(); foreach (var foo in records) { Console.WriteLine($"{foo.Id}, {foo.Name}, {foo.Json["foo"]}"); } } } }

对应的测试与验证可以参考仓库中的 TypeConverterTests.cs 与 TypeConverter`1Tests.cs,这些测试覆盖了基类行为与默认转换器的各类边界情况;若需为自定义转换器补充单元测试,直接以这两个文件为模板即可。

小结

自定义类型转换器是 CsvHelper 类型转换体系中面向扩展的最后一环:先确认 内置转换器清单 无法覆盖需求,再继承DefaultTypeConverter(或TypeConverter<T>)实现核心的ConvertFromString,最后从「全局注册 / 属性注册 / 类映射注册」三种方式中任选一种接入。理解 TypeConverterCache.cs 的查询链路(属性优先、缓存其次、工厂兜底)之后,你不仅能写出正确的自定义转换器,也能快速定位转换器未生效的原因。

  • 后端
  • 数据工程

【免费下载链接】CsvHelper

Library to help reading and writing CSV files

项目地址:https://gitcode.com/gh_mirrors/cs/CsvHelper
点击查看免费下载
上一篇:Playnite游戏库管理器:一站式整合你的所有PC和模拟器游戏
下一篇:Smithbox终极指南:三步打造你的专属魂系游戏体验

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

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

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

立即咨询