ef core 值转换器是处理枚举转字符串、json 字段、timeonly/dateonly 等场景的强制推荐路径;必须显式指定底层类型、复用 jsonserializeroptions、确保纯函数无副作用、精度适配,并在多处复用时抽为语义化独立类。

EF Core 值转换器不是“可选优化”,而是处理枚举存字符串、JSON 存字段、TimeOnly/DateOnly 映射等场景的**强制推荐路径**——绕开它,就得在每个 SaveChanges 前手动序列化、每次查询后手动反序列化,代码污染严重且极易出错。
枚举转字符串:别用 HasConversion<string>()</string> 直接拍脑门
很多人写 .HasConversion<string>()</string> 就以为完事了,结果数据库里存的是 "0" 或空字符串,查不出来。这是因为 EF Core 默认按底层类型(比如 byte)推断,没走字符串逻辑。
- 必须显式指定底层类型为
int或string,例如:public enum Status : int { Pending, Active } - 优先用内置
EnumToStringConverter,而不是匿名函数:.HasConversion<enumtostringconverter>>()</enumtostringconverter> - 如果枚举带
[Description]或自定义属性(如[Display(Name = "已提交")]),内置转换器不识别,得自己写:v => v.GetCustomAttribute<displayattribute>()?.GetName() ?? v.ToString()</displayattribute> - 注意 null 安全:
Enum.Parse遇到空字符串会抛ArgumentException,务必加TryParse或空值判断
JSON 字段:别用 HasConversion 手动序列化
把 Dictionary<string object></string> 或自定义类存 JSON,最常见错误是用 JsonSerializer.Serialize + JsonSerializer.Deserialize 写在 HasConversion 里——这会导致序列化选项不统一、日期格式错乱、甚至循环引用崩溃。
- EF Core 5+ 推荐直接用
.HasColumnType("json")(PostgreSQL/MySQL 8.0+/SQL Server 2016+ 支持原生 JSON 类型) - 若数据库不支持 JSON 类型,才退而求其次用字符串列 +
HasConversion,但必须复用同一套JsonSerializerOptions实例,避免每次新建导致性能和行为不一致 - 不要在转换器里做业务逻辑(如自动补默认值、过滤敏感字段),那属于领域层职责,不是值转换器该干的
自定义结构体(如 Money、PhoneNumber):转换必须无副作用
值转换器被 EF Core 多次调用(跟踪、变更检测、生成 SQL),如果转换逻辑里调用了 DateTime.Now、读配置、查数据库,就会出不可预测的问题。
- 转换函数必须是纯函数:输入相同,输出一定相同;不依赖外部状态,不修改入参
- 确保目标类型能被 EF Core 正确实例化:比如
Money结构体要有无参构造函数,或所有字段都可被反射赋值 - 精度陷阱:把
TimeOnly转TimeSpan存time列会丢微秒精度;更稳的做法是转long(ticks)或string("HH:mm:ss.fffffff") - 别为了“看起来干净”强行统一转换器类型——
Money转decimal比转string更高效,也利于数据库索引和查询
全局注册 vs 局部配置:什么时候该抽成独立类
一个转换器在 3 个以上实体里重复使用,就该立刻抽成类。否则改一处逻辑,漏改两处,上线就出数据解析失败。
- 独立类必须继承
ValueConverter<tmodel tprovider></tmodel>,不能只用匿名函数硬塞 - 类名要体现语义,比如
PhoneNumberToStringConverter,而不是MyConverter1 - 单元测试必须覆盖正向/反向转换,尤其边界值:
null、空字符串、超长字符串、非法格式 - 避免在构造函数里初始化耗时对象(如
HttpClient),值转换器生命周期由 EF Core 管理,不是你控制的
最容易被忽略的点是:值转换器只管类型转换,不管业务约束。比如把 string 转 enum 成功了,不代表这个字符串就合法——校验仍得放在模型验证或领域服务里,不能指望转换器替你挡非法输入。











