C# 10 .NET6 XML反序列化如何控制构造或复用预初始化对象
.NET 6 XmlSerializer 反序列化保留预初始化字段值方案
基础环境
- 语言版本:C# 10
- 运行时:.NET 6
- 序列化组件:
System.Xml.Serialization.XmlSerializer
问题复现代码
public class Test<T> where T : struct, IComparable<T> { [XmlIgnore()] public T SomeOtherValue { get; private set; } public T Value { get; set; } protected Test() { // 仅为序列化流程预留的无参构造 } public Test(int someOtherValue_) { SomeOtherValue = someOtherValue_; } } public class Data { public Test<int> IntValue { get; set; } = new() { SomeOtherValue = 42, }; public Test<int> IntValue2 { get; set; } = new() { SomeOtherValue = 43, }; }
问题表现与约束
- 当XML流中缺失
IntValue这类Test<T>属性节点时,反序列化结果会保留类定义中预初始化的实例,标记[XmlIgnore]的业务阈值字段SomeOtherValue能正常保留预设值(如42、43)。 - 走完整序列化+反序列化流程时,XmlSerializer会默认调用类型无参构造生成全新实例,覆盖预初始化对象,导致
SomeOtherValue被赋值为值类型默认值0,直接导致Value属性setter中依赖阈值的校验逻辑失效。 - 业务规则限制:
SomeOtherValue属于内部业务约束,禁止写入输出XML- 不同实例的
SomeOtherValue取值独立,无法定义为静态字段,也不能在无参构造中写死固定值 - 业务类嵌套层级深,后续迭代会持续增加嵌套的
Test<T>类型成员,手动递归遍历所有成员补赋值的方式维护成本极高,代码冗余易出错
- 预期目标:低侵入实现对
Test<T>类型实例构造过程的控制,优先复用类定义中预初始化的已有实例,避免无参构造重置业务字段。
实现方案
编码成本最低、适配任意嵌套层级的方案是利用XmlSerializer的反序列化事件回调,配合反射自动替换序列化器新建的实例为预初始化对象,不需要修改现有业务类的任何逻辑,也不需要新增特性或调整构造函数访问权限。
实现原理
XmlSerializer在反序列化每个引用类型节点、准备填充属性前会触发UnknownNode事件,此时序列化器刚通过无参构造创建完空实例,还未对属性赋值。我们在这个时机将空实例替换为预初始化好的、带正确SomeOtherValue的已有实例,后续序列化器就会把XML中读取到的Value等值填充到我们提供的实例上,完全不会覆盖被[XmlIgnore]标记的业务字段。
可直接复用的工具代码
using System.Collections; using System.Reflection; using System.Xml.Serialization; public static class XmlSerializationUtil { /// <summary> /// 反序列化XML,自动复用类型定义中预初始化的引用类型实例,避免无参构造重置非序列化字段 /// </summary> public static T DeserializePreserveInitValues<T>(Stream xmlStream) where T : class, new() { T result = new T(); XmlSerializer serializer = new XmlSerializer(typeof(T)); HashSet<object> processedInstances = new HashSet<object>(ReferenceEqualityComparer.Instance); serializer.UnknownNode += (sender, args) => { if (args.ObjectBeingDeserialized == null || args.NodeType != System.Xml.XmlNodeType.Element) return; Type currentType = args.ObjectBeingDeserialized.GetType(); PropertyInfo? targetProp = currentType.GetProperty(args.Name, BindingFlags.Public | BindingFlags.Instance); if (targetProp == null || targetProp.GetIndexParameters().Length > 0) return; // 值类型、string由序列化器正常处理即可 if (targetProp.PropertyType.IsValueType || targetProp.PropertyType == typeof(string)) return; // 取预初始化好的实例 object? presetInstance = targetProp.GetValue(result) ?? targetProp.GetValue(args.ObjectBeingDeserialized); if (presetInstance == null) return; // 替换序列化器新建的空实例 targetProp.SetValue(args.ObjectBeingDeserialized, presetInstance); // 递归绑定嵌套类型的预初始化实例 if (processedInstances.Add(presetInstance)) BindNestedPresetValues(presetInstance, processedInstances); }; serializer.Deserialize(xmlStream); return result; } private static void BindNestedPresetValues(object instance, HashSet<object> processedInstances) { Type type = instance.GetType(); // 跳过框架内置类型、集合类型,仅处理自定义业务类 if (typeof(IEnumerable).IsAssignableFrom(type) || type.Namespace?.StartsWith("System") == true) return; foreach (PropertyInfo prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (!prop.CanRead || !prop.CanWrite || prop.GetIndexParameters().Length > 0) continue; if (prop.PropertyType.IsValueType || prop.PropertyType == typeof(string)) continue; object? propValue = prop.GetValue(instance); if (propValue == null || !processedInstances.Add(propValue)) continue; BindNestedPresetValues(propValue, processedInstances); } } }
使用方式
直接替换原有反序列化调用即可,业务代码零改动:
// 原有写法:会覆盖预初始化值,导致SomeOtherValue变为默认值 // using FileStream fs = File.OpenRead("data.xml"); // Data data = (Data)new XmlSerializer(typeof(Data)).Deserialize(fs); // 新写法:保留预初始化值,校验逻辑正常执行 using FileStream fs = File.OpenRead("data.xml"); Data data = XmlSerializationUtil.DeserializePreserveInitValues<Data>(fs);
方案优势
- 零业务侵入:不需要修改现有业务类的特性、构造函数、属性逻辑,
[XmlIgnore]标记的字段完全不会参与序列化/反序列化流程 - 自动适配嵌套:无论业务类嵌套多少层、包含多少个
Test<T>类型成员,都会自动复用预初始化实例,不需要手动编写遍历赋值逻辑 - 行为完全符合预期:XML中存在的属性值(如
Value)会正常反序列化覆盖,XML中缺失的节点保留预初始化值,Value的校验逻辑执行时SomeOtherValue已经是正确的预设阈值,不会报错
如果业务类结构后续不会频繁变动,也可以选择实现IXmlSerializable接口手动控制Test<T>的序列化/反序列化逻辑,但这种方式需要给每个涉及的类型实现接口,编码和维护成本远高于上述通用工具类方案。
内容的提问来源于stack exchange,提问作者nlaak
相关产品推荐
相关产品推荐

