【问题标题】:How to serialize / deserialize immutable list type in c#如何在 C# 中序列化/反序列化不可变列表类型
【发布时间】:2013-09-28 02:59:17
【问题描述】:

如果我定义了一个类

[DataContract()]
class MyObject {
    [DataMember()]
    ImmutableList<string> Strings { get; private set}
}

ImmutableList&lt;T&gt; 类型来自不可变库https://www.nuget.org/packages/Microsoft.Bcl.Immutable。请注意,类 ImmutableList 没有默认构造函数或可变的 Add 方法。将事物添加到列表中采用表单。

myList = myList.Add("new string");

我可以为 .NET 序列化机制添加一些自定义支持来支持这种类型并展示如何反序列化它吗?

目前该集合只是在反序列化时被跳过,尽管可以序列化它。

【问题讨论】:

标签: c# .net serialization immutability datacontractserializer


【解决方案1】:

通过IDataContractSurrogate 接口还有另一种干净的方法可以做到这一点。 DataContractSerializer 允许您为不可序列化的对象提供代理。下面是ImmutableList&lt;T&gt; 的示例和测试用例。它使用反射,可能会由比我更聪明的人进行优化,但就是这样。

测试用例

using FluentAssertions;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Xml;
using Xunit;

namespace ReactiveUI.Ext.Spec
{
    [DataContract(Name="Node", Namespace="http://foo.com/")]
    class Node
    {
        [DataMember()]
        public string Name;
    }

    [DataContract(Name="Fixture", Namespace="http://foo.com/")]
    class FixtureType
    {
        [DataMember()]
        public ImmutableList<Node> Nodes;

        public FixtureType(){
            Nodes = ImmutableList<Node>.Empty.AddRange( new []
            { new Node(){Name="A"}
            , new Node(){Name="B"}
            , new Node(){Name="C"}
            });
        }
    }


    public class ImmutableSurrogateSpec
    {  
        public static string ToXML(object obj)
            {
                var settings = new XmlWriterSettings { Indent = true };

                using (MemoryStream memoryStream = new MemoryStream())
                using (StreamReader reader = new StreamReader(memoryStream))
                using (XmlWriter writer = XmlWriter.Create(memoryStream, settings))
                {
                    DataContractSerializer serializer =
                      new DataContractSerializer
                          ( obj.GetType()
                          , new DataContractSerializerSettings() { DataContractSurrogate = new ImmutableSurrogateSerializer() }
                          );
                    serializer.WriteObject(writer, obj);
                    writer.Flush();
                    memoryStream.Position = 0;
                    return reader.ReadToEnd();
                }
            }

        public static T Load<T>(Stream data)
        {
            DataContractSerializer ser = new DataContractSerializer
                  ( typeof(T)
                  , new DataContractSerializerSettings() { DataContractSurrogate = new ImmutableSurrogateSerializer() }
                  );
            return (T)ser.ReadObject(data);
        }

        public static T Load<T>(string data)
        {
            using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(data)))
            {
                return Load<T>(stream);
            }
        }

        [Fact]
        public void ShouldWork()
        {
            var o = new FixtureType();

            var s = ToXML(o);

            var oo = Load<FixtureType>(s);

            oo.Nodes.Count().Should().Be(3);
            var names = oo.Nodes.Select(n => n.Name).ToList();
            names.ShouldAllBeEquivalentTo(new[]{"A", "B", "C"});

        }

    }
}

实施

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Serialization;

namespace ReactiveUI.Ext
{
    class ImmutableListListConverter<T>
    {
        public static ImmutableList<T> ToImmutable( List<T> list )
        {
            return ImmutableList<T>.Empty.AddRange(list);
        }

        public static List<T> ToList(ImmutableList<T> list){
            return list.ToList();
        }

        public static object ToImmutable( object list )
        {
            return ToImmutable(( List<T> ) list);
        }

        public static object ToList(object list){
            return ToList(( ImmutableList<T> ) list);
        }

    }

    static class ImmutableListListConverter {


        static ConcurrentDictionary<Tuple<string, Type>, Func<object,object>> _MethodCache 
            = new ConcurrentDictionary<Tuple<string, Type>, Func<object,object>>();

        public static Func<object,object> CreateMethod( string name, Type genericType )
        {
            var key = Tuple.Create(name, genericType);
            if ( !_MethodCache.ContainsKey(key) )
            {
                _MethodCache[key] = typeof(ImmutableListListConverter<>)
                    .MakeGenericType(new []{genericType})
                    .GetMethod(name, new []{typeof(object)})
                    .MakeLambda();
            }
            return _MethodCache[key];
        }
        public static Func<object,object> ToImmutableMethod( Type targetType )
        {
            return ImmutableListListConverter.CreateMethod("ToImmutable", targetType.GenericTypeArguments[0]);
        }

        public static Func<object,object> ToListMethod( Type targetType )
        {
            return ImmutableListListConverter.CreateMethod("ToList", targetType.GenericTypeArguments[0]);
        }

        private static Func<object,object> MakeLambda(this MethodInfo method )
        {
            return (Func<object,object>) method.CreateDelegate(Expression.GetDelegateType(
            (from parameter in method.GetParameters() select parameter.ParameterType)
            .Concat(new[] { method.ReturnType })
            .ToArray()));
        }

    }

    public class ImmutableSurrogateSerializer : IDataContractSurrogate
    {
        static ConcurrentDictionary<Type, Type> _TypeCache = new ConcurrentDictionary<Type, Type>();

        public Type GetDataContractType( Type targetType )
        {
            if ( _TypeCache.ContainsKey(targetType) )
            {
                return _TypeCache[targetType];
            }

            if(targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(ImmutableList<>)) 
            {
                return _TypeCache[targetType] 
                    = typeof(List<>).MakeGenericType(targetType.GetGenericArguments());
            }
            else
            {
                return targetType;
            }
        }

        public object GetDeserializedObject( object obj, Type targetType )
        {
            if ( _TypeCache.ContainsKey(targetType) )
            {
               return ImmutableListListConverter.ToImmutableMethod(targetType)(obj);
            }
            return obj;
        }

        public object GetObjectToSerialize( object obj, Type targetType )
        {
            if ( targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(ImmutableList<>) )
            {
               return ImmutableListListConverter.ToListMethod(targetType)(obj);
            }
            return obj;
        }

        public object GetCustomDataToExport( Type clrType, Type dataContractType )
        {
            throw new NotImplementedException();
        }

        public object GetCustomDataToExport( System.Reflection.MemberInfo memberInfo, Type dataContractType )
        {
            throw new NotImplementedException();
        }


        public void GetKnownCustomDataTypes( System.Collections.ObjectModel.Collection<Type> customDataTypes )
        {
            throw new NotImplementedException();
        }


        public Type GetReferencedTypeOnImport( string typeName, string typeNamespace, object customData )
        {
            throw new NotImplementedException();
        }

        public System.CodeDom.CodeTypeDeclaration ProcessImportedType( System.CodeDom.CodeTypeDeclaration typeDeclaration, System.CodeDom.CodeCompileUnit compileUnit )
        {
            throw new NotImplementedException();
        }

        public ImmutableSurrogateSerializer() { }
    }
}

【讨论】:

  • 通过注释掉将代理项添加到序列化程序并观察测试失败的行很容易证明它正在工作。
  • 注意我已经停止使用 Datacontract 序列化器,现在使用 json.net 序列化器,它对不可变集合“正常工作”。
  • 有点晚了,但你确定GetObjectToSerializer 不应该同时考虑实际类型和目标类型吗?我必须调整该方法以使 ImmutableSurrogate 与 ImmutableHashSet 一起使用。
  • 基于这个答案,我已经为 .NET Standard 库实现了 ISerializationSurrogateProvider 接口。它与 .Net 4.5 的 IDataContractSurrogate 接口基本相同。它也应该表现得更好一点,因为我们避免了处理整个集合进行序列化。 github.com/bp74/System.Collections.Immutable.Surrogate
【解决方案2】:

一种方法是使用代理可变列表并使用 OnSerializing 和 OnDeserialized 挂钩

[DataContract()]
class MyObject {

    public ImmutableList<string> Strings { get; private set}

    [DataMember(Name="Strings")]
    private List<String> _Strings;

    [OnSerializing()]
    public void OnSerializing(StreamingContext ctxt){
        _Strings = Strings.ToList();
    }

    [OnDeserialized()]
    public void OnDeserialized(StreamingContext ctxt){
        Strings = ImmutableList<string>.Empty.AddRange(_Strings);
    }
}

它不是超级漂亮,但正如 Marc Gravell 在他的回答中指出的那样,DataContract 序列化程序在不可变集合方面被破坏了,并且没有简单的钩子可以教它如何在没有上述类型的黑客攻击的情况下表现。

更新

DataContract 序列化程序没有损坏。有一种方法可以连接代理。请参阅这个单独的答案,显示另一种技术。

https://stackoverflow.com/a/18957739/158285

【讨论】:

  • 附带说明 - 我通常的指导是:如果序列化步骤超出了使用所选序列化程序琐碎可以完成的工作,那么与其抗争,不如创建一个单独的序列化程序DTO 模型(MyObjectDTO,例如)确实与序列化程序很好地配合,并在两个模型之间添加了转换方法。所以MyObjectDTO 将有一个简单的List&lt;string&gt;MyObject 保留ImmutableList&lt;string&gt;,并且转换代码具有在它们之间切换的魔力。
  • 好建议,但有一个 API 可以处理这个问题。 IDataContractSurrogate。我只需要弄清楚如何编写将 ImmutableList 映射到 List 的代码,它应该可以正常工作。
【解决方案3】:

嘿;我可以想象这里发生了什么......生成的代码可能正在做(释义):

var list = obj.Strings;
while(CanReadNextItem()) {
    list.Add(ReadNextItem());
}

问题在于 BCL 不可变 API 会要求您每次都捕获结果,即

var list = obj.Strings;
while(CanReadNextItem()) {
    list = list.Add(ReadNextItem());
}
obj.Strings = list; // the private set is not a problem for this

预先存在的列表反序列化代码不能以这种方式工作,因为它从来不需要 - 事实上,Add 有许多不同的实现,其中一些返回非空结果需要忽略。

缺少非公共构造函数也可能会让人有些不安,但如果这是主要问题,我会在它尝试创建非空列表时出现异常。

当然,就性能而言,list = list.Add(...) API 可能并不是最适合使用的 API无论如何(尽管它应该可以工作)。

我最近写了一篇关于这个主题的博客(在 protobuf-net 的上下文中,它现在已经更新为与这些集合类型一起工作):http://marcgravell.blogspot.co.uk/2013/09/fun-with-immutable-collections.html希望这篇博客文章能解释为什么这些差异意味着它不能播放与现有的序列化技术完美结合,以及如何更新序列化库以适应这种情况。

要直接回答这个问题,我想说的答案很简单:因为尚未对DataContractSerializer 进行支持不可变集合所需的更改。我不知道是否有计划解决这个问题。但是:我很高兴地声明:“在 protobuf-net 中工作”;p

【讨论】:

  • BCL 库具有构建器模式,您可以在其中在事务中变得可变。那将是正确的做法,但 DataContractSerializer 中没有挂钩可以将其添加到我自己:( ?
  • @bradgonesurfing 是的,我在博客中全面讨论了这个(构建器 API)。坦率地说,如果 any 序列化库有一个直接适用于这种情况的扩展点,我会感到惊讶。我必须对 protobuf-net 所做的更改超出了通过代理等可以实现的范围。而且构建器 API 依赖于与通用具体列表匹配的非通用实用程序类的知识这一事实意味着它不能合理预计将仅在尚不了解此模式的序列化程序中工作
  • 我的解决方案是使用隐藏的私有字段作为实际的DataMember,并使用 OnSerializing 和 OnDeserialized 挂钩通过可变列表代理不可变集合。它可以工作,从公共接口的角度来看,它仍然是不可变的集合。
  • @bradgonesurfing 它把对象弄得一团糟;根据我的其他评论 - 我希望在这种情况下看到专用的 DTO 模型
  • 我同意。我刚刚找到了一个用于 datacontract 序列化程序代理 msdn.microsoft.com/en-us/library/… 的钩子,这是实现我手动执行的 hack 模式的官方方法。不确定我能否让它适用于泛型,但我会试一试。
猜你喜欢
  • 1970-01-01
  • 2013-10-12
  • 2016-06-15
  • 2022-12-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-13
  • 1970-01-01
相关资源
最近更新 更多