- 后端
- 数据工程
【免费下载链接】CsvHelper
Library to help reading and writing CSV files
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重载:
GetConverter(MemberInfo)(TypeConverterCache.cs):先检查成员上是否标注了TypeConverterAttribute——属性注册优先级最高,命中后直接返回该特性指定的转换器;- 若成员上没有属性,则退而调用
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
相关推荐
AutoMapper 自定义类型转换器(Custom Type Converters)完全指南:ITypeConverter 与 ConvertUsing 深度解析
AutoMapper 自定义类型转换器(Custom Type Converters)完全指南:ITypeConverter 与 ConvertUsing 深度
后端CsvHelper 类型转换(Type Conversion)完全指南:内置转换器、TypeConverterOptions 与自定义 TypeConverter
CsvHelper 类型转换(Type Conversion)完全指南:内置转换器、TypeConverterOptions 与自定义 TypeConverte
后端数据工程MikroORM 自定义类型(Custom Types)完全指南:从 Type 抽象类到 SQL 级转换的实战详解
MikroORM 自定义类型(Custom Types)完全指南:从 Type 抽象类到 SQL 级转换的实战详解 本文以 MikroORM 的 Type 抽象
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考