微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

如何解决使用 DataContractSerializer 和 XmlDictionaryWriter 序列化 JObject 后崩溃

我必须使用 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",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(),checked((int)stream1.Length)));
    }
}

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

解决方法

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>>
{
}

好吧,如果我们尝试使用数据契约序列化程序序列化这些列表之一,它会因堆栈溢出异常而崩溃,试图找出契约名称。 Demo fiddle #1 here -- 您需要取消注释行 //Test(new RecursiveList<string>()); 才能看到崩溃:

Stack overflow.
   at System.ModuleHandle.ResolveType(System.Runtime.CompilerServices.QCallModule,Int32,IntPtr*,System.Runtime.CompilerServices.ObjectHandleOnStack)
   at System.ModuleHandle.ResolveTypeHandleInternal(System.Reflection.RuntimeModule,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,Boolean)
   at System.Reflection.CustomAttribute.IsDefined(System.RuntimeType,Boolean)
   at System.Runtime.Serialization.CollectionDataContract.IsCollectionOrTryCreate(System.Type,System.Runtime.Serialization.DataContract ByRef,System.Type ByRef,Boolean)
   at System.Runtime.Serialization.CollectionDataContract.IsCollectionHelper(System.Type,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)

糟糕。好吧,如果我们为 RecursiveList<string>

创建一个序列化代理,例如以下虚拟代理
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<string>(),如

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

演示小提琴 #2 here

好的,现在让我们尝试在 RecursiveList<string> 嵌入模型中时使用代理,例如:

public class Model
{
    public RecursiveList<string> List { get; set; }
}

好吧,当我尝试使用空列表序列化此模型的实例时,崩溃又回来了。演示小提琴 #3 here - 您需要取消注释行 //Test(new Model { List = new RecursiveList<string>() }); 才能看到崩溃。

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

现在这与 JObject 和您的 Test 类有什么关系?事实证明,JObject 是递归集合类型的另一个示例。它实现了 IDictionary<string,JToken?>JToken 依次实现了 IEnumerable<JToken>,从而触发了我们在包含 RecursiveList<string> 的简单模型中看到的相同堆栈溢出。

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

解决方法

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

[DataContract(Name = "Test",Namespace = "urn:actors")]
public class Test
{
    [IgnoreDataMember]
    public JObject obj { get; set; }
    
    [DataMember(Name = "obj",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(),checked((int)stream1.Length)));

注意事项:

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

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

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

演示小提琴 #4 here

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。