使用 DataContractSerializer 和 XmlDictionaryWriter 序列化 JObject 后崩溃

Posted

技术标签:

【中文标题】使用 DataContractSerializer 和 XmlDictionaryWriter 序列化 JObject 后崩溃【英文标题】:Crash after serializing JObject with DataContractSerializer and XmlDictionaryWriter 【发布时间】:2021-08-04 08:21:01 【问题描述】:

我必须使用 DataContractSerializer 序列化 Newtonsoft JObject,它会因堆栈溢出而崩溃。 如何让它发挥作用? 我的代码是。

var serializer = new DataContractSerializer(typeof(JObject));
MemoryStream stream1 = new MemoryStream();
var writer = XmlDictionaryWriter.CreateBinaryWriter(stream1);
var obj = new JObject();
serializer.WriteObject(writer, obj);
writer.Flush();

以下示例使用ISerializationSurrogateProvider 功能将JObject 转换为通用类型。它会因堆栈溢出而崩溃。


using System;
using System.IO;
using Newtonsoft.Json.Linq;
using System.Runtime.Serialization;
using System.Xml;

class Program

    [DataContract(Name = "JTokenReference", Namespace = "urn:actors")]
    [Serializable]
    public sealed class JTokenReference
    
        public JTokenReference()
        
        

        [DataMember(Name = "JType", Order = 0, IsRequired = true)]
        public JTokenType JType  get; set; 

        [DataMember(Name = "Value", Order = 1, IsRequired = true)]
        public string Value  get; set; 

        public static JTokenReference From(JToken jt)
        
            if (jt == null)
            
                return null;
            
            return new JTokenReference()
            
                Value = jt.ToString(),
                JType = jt.Type
            ;
        
        public object To()
        
            switch (JType)
            
                case JTokenType.Object:
                    
                        return JObject.Parse(Value);
                    
                case JTokenType.Array:
                    
                        return JArray.Parse(Value);
                    
                default:
                    
                        return JToken.Parse(Value);
                    
            
        
    

    internal class ActorDataContractSurrogate : ISerializationSurrogateProvider
    
        public static readonly ISerializationSurrogateProvider Instance = new ActorDataContractSurrogate();

        public Type GetSurrogateType(Type type)
        
            if (typeof(JToken).IsAssignableFrom(type))
            
                return typeof(JTokenReference);
            

            return type;
        

        public object GetObjectToSerialize(object obj, Type targetType)
        
            if (obj == null)
            
                return null;
            
            else if (obj is JToken jt)
            
                return JTokenReference.From(jt);
            

            return obj;
        

        public object GetDeserializedObject(object obj, Type targetType)
        
            if (obj == null)
            
                return null;
            
            else if (obj is JTokenReference reference &&
                    typeof(JToken).IsAssignableFrom(targetType))
            
                return reference.To();
            
            return obj;
        
    

    [DataContract(Name = "Test", Namespace = "urn:actors")]
    [Serializable]
    public class Test
    
        [DataMember(Name = "obj", Order = 0, IsRequired = false)]
        public JObject obj;
    

    static void Main(string[] args)
    
        var serializer = new DataContractSerializer(typeof(Test),
        new DataContractSerializerSettings()
        
            MaxItemsInObjectGraph = int.MaxValue,
            KnownTypes = new Type[]  typeof(JTokenReference), typeof(JObject), typeof(JToken) ,
        );

        serializer.SetSerializationSurrogateProvider(ActorDataContractSurrogate.Instance);

        MemoryStream stream1 = new MemoryStream();
        var writer = XmlDictionaryWriter.CreateBinaryWriter(stream1);
        var obj = new JObject();
        var test = new Test()
        
            obj = obj,
        ;
        serializer.WriteObject(writer, test);
        writer.Flush();
        Console.WriteLine(System.Text.Encoding.UTF8.GetString(stream1.GetBuffer(), 0, checked((int)stream1.Length)));
    

我正在尝试定义一个新类型 JTokenReference 来在序列化时替换 JObject/JToken,但它在替换发生之前就崩溃了。似乎无法解析类型。

【问题讨论】:

我建议您首先使用this answer 将JObject 转换为标准的.NET 类型,然后您可以使用DataContractSerializer 序列化该结果。 你为什么要用DataContractSerializer序列化JObjectJObject 是 Json.NET 的 JSON 文档对象模型,没有理由想象它可以被 DataContractSerializer 序列化为 XML。如果您只需要将 JSON 转换为 XML,请参阅 How to convert JSON to XML or XML to JSON?。如果您已经解析为 JObject,您可以使用 JsonExtension.ToXElement() from How to generate XML from JSON with parent node of array items 将其转换为 XML。 我使用 ISerializationSurrogateProvider 将其转换为 .NET 类型,但由于同样的原因它也崩溃了。 第3方框架将每个参数都转换为XML,不幸的是我的参数是一个json.net对象。我别无选择,那么如何解决这个问题? ISerializationSurrogateProvider 将其转换为 .NET 类型,但由于同样的原因它也崩溃了。 - 那么您能否分享带有 JSON 和重现代码的 minimal reproducible example问题?另外,它是崩溃,还是异常?如果是后者,还请分享异常的完整ToString() 输出,包括异常类型、消息、回溯和内部异常(如果有)。 【参考方案1】:

TL;DR

您的方法是合理的,应该有效,但由于递归集合类型的 ISerializationSurrogateProvider 功能中似乎存在错误而失败。每当您需要序列化 ​​JToken 时,您都需要更改设计以使用代理属性,例如如下:

[IgnoreDataMember]
public JObject obj  get; set; 

[DataMember(Name = "obj", Order = 0, IsRequired = false)]
string objSurrogate  get  return obj?.ToString(Newtonsoft.Json.Formatting.None);  set  obj = (value == null ? null : JObject.Parse(value));  

说明

您遇到的崩溃是堆栈溢出,可以更简单地重现如下。当数据协定序列化程序编写一个泛型如List<string> 时,它通过组合泛型类和参数名称来构造一个data contract name,如下所示:

List<string>: ArrayOfstring List<List<string>: ArrayOfArrayOfstring List<List<List<string>>>: ArrayOfArrayOfArrayOfstring

等等。随着通用嵌套越来越深,名称越来越长。那么,如果我们像下面这样定义一个自递归集合类型会发生什么?

public class RecursiveList<T> : List<RecursiveList<T>>


好吧,如果我们尝试使用数据合约序列化程序序列化这些列表之一,它会因堆栈溢出异常而崩溃,试图找出合约名称。演示小提琴 #1 here -- 你需要取消注释 //Test(new RecursiveList&lt;string&gt;()); 才能看到崩溃:

Stack overflow.
   at System.ModuleHandle.ResolveType(System.Runtime.CompilerServices.QCallModule, Int32, IntPtr*, Int32, IntPtr*, Int32, System.Runtime.CompilerServices.ObjectHandleOnStack)
   at System.ModuleHandle.ResolveTypeHandleInternal(System.Reflection.RuntimeModule, Int32, System.RuntimeTypeHandle[], System.RuntimeTypeHandle[])
   at System.Reflection.RuntimeModule.ResolveType(Int32, System.Type[], System.Type[])
   at System.Reflection.CustomAttribute.FilterCustomAttributeRecord(System.Reflection.MetadataToken, System.Reflection.MetadataImport ByRef, System.Reflection.RuntimeModule, System.Reflection.MetadataToken, System.RuntimeType, Boolean, ListBuilder`1<System.Object> ByRef, System.RuntimeType ByRef, System.IRuntimeMethodInfo ByRef, Boolean ByRef)
   at System.Reflection.CustomAttribute.IsCustomAttributeDefined(System.Reflection.RuntimeModule, Int32, System.RuntimeType, Int32, Boolean)
   at System.Reflection.CustomAttribute.IsDefined(System.RuntimeType, System.RuntimeType, Boolean)
   at System.Runtime.Serialization.CollectionDataContract.IsCollectionOrTryCreate(System.Type, Boolean, System.Runtime.Serialization.DataContract ByRef, System.Type ByRef, Boolean)
   at System.Runtime.Serialization.CollectionDataContract.IsCollectionHelper(System.Type, System.Type ByRef, Boolean)
   at System.Runtime.Serialization.DataContract.GetNonDCTypeStableName(System.Type)
   at System.Runtime.Serialization.DataContract.GetStableName(System.Type, Boolean ByRef)
   at System.Runtime.Serialization.DataContract.GetCollectionStableName(System.Type, System.Type, System.Runtime.Serialization.CollectionDataContractAttribute ByRef)
   at System.Runtime.Serialization.DataContract.GetNonDCTypeStableName(System.Type)
   at System.Runtime.Serialization.DataContract.GetStableName(System.Type, Boolean ByRef)
   at System.Runtime.Serialization.DataContract.GetCollectionStableName(System.Type, System.Type, System.Runtime.Serialization.CollectionDataContractAttribute ByRef)
   at System.Runtime.Serialization.DataContract.GetNonDCTypeStableName(System.Type)
   at System.Runtime.Serialization.DataContract.GetStableName(System.Type, Boolean ByRef)

哎呀。好吧,如果我们为RecursiveList&lt;string&gt; 创建一个序列化代理项,例如下面的虚拟代理项会怎样?

public class RecursiveListStringSurrogate

    // A dummy surrogate that serializes nothing, for testing purposes.


public class RecursiveListStringSurrogateSelector : ISerializationSurrogateProvider

    public object GetDeserializedObject(object obj, Type targetType)
    
        if (obj is RecursiveListStringSurrogate)
            return new RecursiveList<string>();
        return obj;
    

    public object GetObjectToSerialize(object obj, Type targetType)
    
        if (obj is RecursiveList<string>)
            return new RecursiveListStringSurrogate();
        return obj;
    

    public Type GetSurrogateType(Type type) 
    
        if (type == typeof(RecursiveList<string>))
            return typeof(RecursiveListStringSurrogate);
        return type;
    

使用那个代理,一个空的new RecursiveList&lt;string&gt;()确实可以成功序列化,因为

<RecursiveListStringSurrogate xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/" />

演示小提琴 #2 here.

好的,现在让我们尝试在 RecursiveList&lt;string&gt; 嵌入到模型中时使用代理,例如:

public class Model

    public RecursiveList<string> List  get; set; 

当我尝试用一​​个空列表序列化这个模型的一个实例时,崩溃又回来了。演示小提琴 #3 here - 您需要取消注释 //Test(new Model List = new RecursiveList&lt;string&gt;() ); 行才能看到崩溃。

再次糟糕。目前尚不完全清楚为什么会失败。我只能推测,微软在某处保留了一个字典,将原始数据合约名称映射到代理数据合约名称——这会导致堆栈溢出,只需生成一个字典键即可。

现在这与JObject 和您的Test 班级有什么关系?好吧,事实证明JObject 是递归集合类型的另一个例子。它实现了IDictionary&lt;string, JToken?&gt;JToken 又实现了IEnumerable&lt;JToken&gt; 从而触发了我们在包含RecursiveList&lt;string&gt; 的简单模型中看到的相同的堆栈溢出。

您甚至可能想就此向 Microsoft 发送 report an issue(尽管我不知道他们是否正在修复数据协定序列化程序的错误。)

解决方法

为避免此问题,您需要修改模型以使用 JToken 成员的代理属性,如本答案开头所示:

[DataContract(Name = "Test", Namespace = "urn:actors")]
public class Test

    [IgnoreDataMember]
    public JObject obj  get; set; 
    
    [DataMember(Name = "obj", Order = 0, IsRequired = false)]
    string objSurrogate  get  return obj?.ToString(Newtonsoft.Json.Formatting.None);  set  obj = (value == null ? null : JObject.Parse(value));  

如下可以序列化成功:

var obj = new JObject();
var test = new Test()

    obj = obj,
;

var serializer = new DataContractSerializer(test.GetType());

MemoryStream stream1 = new MemoryStream();
var writer = XmlDictionaryWriter.CreateBinaryWriter(stream1);
serializer.WriteObject(writer, test);
writer.Flush();
Console.WriteLine(System.Text.Encoding.UTF8.GetString(stream1.GetBuffer(), 0, checked((int)stream1.Length)));

注意事项:

如果您需要将JToken 序列化为根对象,您可以将其包装在某个容器对象中,或者使用您问题中的ActorDataContractSurrogate。正如我们所见,当递归集合类型是根对象时,序列化功能似乎确实适用于它们。

由于您要序列化为二进制,为了提高效率,我建议将JObject 格式化为Formatting.None

代理属性可以是私有的,只要用[DataMember]标记即可。

演示小提琴 #4 here.

【讨论】:

感谢您提供非常聪明的解决方案!

以上是关于使用 DataContractSerializer 和 XmlDictionaryWriter 序列化 JObject 后崩溃的主要内容,如果未能解决你的问题,请参考以下文章

使用 DataContractSerializer 时设置属性的初始值

使用 DataContractSerializer 和 XmlDictionaryWriter 序列化 JObject 后崩溃

在原语列表上使用 DataContractSerializer 的自定义元素名称

使用 DataContractSerializer 的接口中的显式类型

使用 DataContractSerializer 序列化没有命名空间的对象

如何使用 DataContractSerializer 从 XMLDocument 的单个节点反序列化?