.NET 4.8反序列化含init-only setter的泛型类抛出MissingMethodException
尝试反序列化包含init-only setter的泛型类时,抛出以下异常:
System.InvalidOperationException: 'XML文档(2, 2)中存在错误。'
内部异常:
MissingMethodException: 未找到方法:'Void Example.BrokenClass`1.set_MyString(System.String)'
但反序列化包含init-only setter的非泛型类却能正常执行。运行.NET 4.8最小化项目时,WorkingClass1和WorkingClass2可正常反序列化,BrokenClass会触发上述异常。
原因分析
.NET Framework 4.8的XmlSerializer对泛型类的init-only setter存在兼容性bug:
- init-only setter在编译时会生成带有特殊标记的set方法,而非普通的setter;
- 非泛型类场景下,
XmlSerializer能识别并调用这个特殊的init方法完成反序列化; - 但泛型类场景下,
XmlSerializer的类型解析逻辑无法正确定位到该特殊方法,仍尝试寻找普通的set_XXX方法,最终抛出MissingMethodException。
不升级.NET 4.8的解决办法
以下是几种可行的替代方案:
方案1:实现IXmlSerializable接口手动控制序列化逻辑
通过手动实现序列化和反序列化过程,绕开XmlSerializer对init-only setter的识别问题:
public class BrokenClass<T> : IXmlSerializable { public string MyString { get; init; } public XmlSchema GetSchema() => null; public void ReadXml(XmlReader reader) { // 读取XML节点值,通过私有构造函数+字段赋值完成初始化 reader.ReadStartElement(); var value = reader.ReadContentAsString(); reader.ReadEndElement(); // 假设类中有私有字段和构造函数 // 示例:替换为实际的私有字段和构造函数实现 typeof(BrokenClass<T>).GetField("_myString", BindingFlags.Instance | BindingFlags.NonPublic) ?.SetValue(this, value); } public void WriteXml(XmlWriter writer) { writer.WriteElementString(nameof(MyString), MyString); } }
注:实际实现时建议用私有字段配合构造函数完成初始化,避免反射开销。
方案2:替换init-only setter为私有setter
保留属性的公开get访问器,将setter改为私有,让XmlSerializer能通过反射访问私有setter:
public class BrokenClass<T> { public string MyString { get; private set; } }
这种方式牺牲了init-only的不可变性,但能直接兼容XmlSerializer。
方案3:使用DataContractSerializer替代XmlSerializer
DataContractSerializer对init-only setter的支持更完善,无论泛型还是非泛型类都能正常处理:
private static void TestDataContractSerializer<T>(T myClass) { DataContractSerializer serializer = new DataContractSerializer(typeof(T)); using (MemoryStream stream = new MemoryStream()) { serializer.WriteObject(stream, myClass); stream.Position = 0; var output = (T)serializer.ReadObject(stream); } }
需要给类添加[DataContract]特性,属性添加[DataMember]特性:
[DataContract] public class BrokenClass<T> { [DataMember] public string MyString { get; init; } }
方案4:添加辅助私有属性供XmlSerializer使用
保留init-only属性,同时添加一个私有辅助属性,让XmlSerializer序列化/反序列化这个辅助属性,再同步到init-only属性:
public class BrokenClass<T> { public string MyString { get; init; } [XmlElement("MyString")] private string MyStringHelper { get => MyString; set => typeof(BrokenClass<T>).GetProperty(nameof(MyString)) .SetValue(this, value); } }
利用XmlElement特性让辅助属性映射到原XML节点,通过反射完成init-only属性的赋值。
完整测试代码
Program.cs
using System.ComponentModel; using System.IO; using System.Xml.Serialization; namespace Example { internal class Program { static void Main(string[] args) { WorkingClass1<string> workingClass1 = new WorkingClass1<string>() { MyString = "Hello World" }; TestXmlSerializer(workingClass1); WorkingClass2 workingClass2 = new WorkingClass2() { MyString = "Hello World" }; TestXmlSerializer(workingClass2); BrokenClass<string> brokenClass = new BrokenClass<string>() { MyString = "Hello World" }; TestXmlSerializer(brokenClass); } private static void TestXmlSerializer<T>(T myClass) { XmlSerializer serializer = new(typeof(T)); StringWriter writer = new(); serializer.Serialize(writer, myClass); StringReader reader = new(writer.ToString()); var output = (T)serializer.Deserialize(reader); } } public class BrokenClass<T> { public string MyString { get; init; } } public class WorkingClass1<T> { public string MyString { get; set; } } public class WorkingClass2 { public string MyString { get; init; } } } namespace System.Runtime.CompilerServices { [EditorBrowsable(EditorBrowsableState.Never)] public static class IsExternalInit { } }
Example.csproj
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net48</TargetFramework> <LangVersion>latest</LangVersion> </PropertyGroup> </Project>
内容的提问来源于stack exchange,提问作者Ryan W

