C#带外键约束的强类型DataSet JSON反序列化问题
问题背景
使用C#和Newtonsoft.Json对带外键约束的强类型DataSet做序列化与反序列化时,若父表在设计器中晚于子表添加,会触发异常:自动生成的Designer.cs文件中,父表会在子表之后被加入DataTableCollection,导致序列化JSON时子表数据先被写入;反序列化回强类型DataSet实例时,会抛出InvalidConstraintException,错误提示为“ForeignKeyConstraint要求子键值必须存在于父表中”。
测试复现代码
ChildParentDataSet ds = new ChildParentDataSet(); ds.Parent.Rows.Add(1); ds.Child.Rows.Add(1); string json = Newtonsoft.Json.JsonConvert.SerializeObject(ds); // 执行此行抛出InvalidConstraintException ChildParentDataSet? ds2 = Newtonsoft.Json.JsonConvert.DeserializeObject<ChildParentDataSet>(json);
注:ChildParentDataSet是设计器生成的强类型DataSet,包含Child表(含ParentId列)和Parent表(含Id列),已建立Parent.Id与Child.ParentId的外键约束,且Parent表晚于Child表添加。
尝试的解决方案及遇到的问题
1. 自定义转换器关闭约束(首选思路)
尝试通过实现CustomCreationConverter,在反序列化前将DataSet.EnforceConstraints设为false,完成后恢复为true,代码实现如下:
public class DataSetDeserializer { public static T DeserializeDataSet<T>(string json) { var ds = Newtonsoft.Json.JsonConvert.DeserializeObject<T>(json, new DataSetConverter<T>()); System.Data.DataSet? dataSet = (System.Data.DataSet?)(Object?)ds; dataSet.EnforceConstraints = true; return ds; } private class DataSetConverter<T> : Newtonsoft.Json.Converters.CustomCreationConverter<T> { public override T Create(Type objectType) { var ds = Activator.CreateInstance(objectType); System.Data.DataSet? dataSet = (System.Data.DataSet?)ds; dataSet.EnforceConstraints = false; return (T)ds; } } }
修改测试代码最后一行调用自定义转换器后:
// 执行此行抛出Newtonsoft.Json.JsonSerializationException ChildParentDataSet? ds2 = DataSetDeserializer.DeserializeDataSet<ChildParentDataSet>(json);
异常信息为:Cannot populate JSON object onto type 'ChildParentDataSet'. Path 'Child', line 1, position 9.
2. 序列化阶段调整表顺序
思路是序列化前对DataSet中的表排序,让父表在前、子表在后,逐个序列化后拼接JSON。但该方案存在明显缺陷:无法处理自引用外键场景,且效率低于直接序列化整个DataSet,因此放弃。
3. 改用XML序列化
DataSet自带GetXml方法支持XML序列化,但XML格式过于冗长,且当前业务使用gRPC传输数据,改用XML会涉及客户端与服务端大量的重新部署工作,因此不愿采用。
问题排查与最终处理思路
后续补充测试与分析:
- 手动创建等效的
ChildParentDataSet2类,使用相同的自定义转换器可成功反序列化,确认问题根源在设计器生成的强类型DataSet上; - 设计器生成的DataSet与手动创建的类序列化得到的JSON完全一致:
{"Child":[{"ParentId":1}],"Parent":[{"Id":1}]}; - 最终定位原因:自定义的
CustomCreationConverter没有复用Newtonsoft自带的DataSetConverter功能,导致无法正确填充设计器生成的DataSet结构。
基于此,fork了官方的DataSetConverter类,修改逻辑为反序列化前关闭约束、完成后重新开启,并提交了PR,但可能因性能等问题无法被官方合并。
内容的提问来源于stack exchange,提问作者u9bg214

