代码之家  ›  专栏  ›  技术社区  ›  Christian Waluga

WCF:具有多个模块的数据协定序列化程序

  •  1
  • Christian Waluga  · 技术社区  · 7 年前

    在我的一个C#项目中,我使用WCF数据约定序列化器将其序列化为XML。然而,该框架由多个扩展模块组成,这些模块可以加载也可以不加载,这取决于一些启动配置(如果有必要,我使用MEF)。未来,模块列表可能会增加,我担心这种情况有一天可能会对模块特定的数据造成问题。据我所知,我可以实现一个数据协定解析器来双向帮助序列化程序定位类型,但是如果项目包含它无法解释的数据,那么会发生什么情况,因为相关模块没有加载?

    我正在寻找一种解决方案,允许我在未加载完整模块集(甚至不可用)的情况下保留现有的序列化数据。我认为这是一种告诉反序列化程序“如果你不理解你得到了什么,那么不要尝试序列化它,但请将数据保存在某个地方,以便下次序列化时可以将其放回”。我认为我的问题与 round-tripping

    最小示例 假设我用可选模块A、B和C启动应用程序,并生成以下XML(AData、BData和CData在一个集合中,可能都派生自一个公共基类):

    <Project xmlns="http://schemas.datacontract.org/2004/07/TestApplication" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
        <Data>
            <ModuleData i:type="AData">
                <A>A</A>
            </ModuleData>
            <ModuleData i:type="BData">
                <B>B</B>
            </ModuleData>
            <ModuleData i:type="CData">
                <C>C</C>
            </ModuleData>
        </Data>
    </Project>
    

    如果我跳过模块C(包含CData的定义)并加载同一个项目,那么序列化程序将失败,因为它不知道如何处理CData。如果我能设法说服框架保留数据,并在有人用模块C再次打开项目之前保持不变,那么我就赢了。当然,我可以实现动态数据结构来存储扩展数据,例如键值树,但在扩展模块中也可以使用现有的序列化框架。任何关于如何实现这一点的提示都将受到高度赞赏!

    using System;
    using System.IO;
    using System.Collections.Generic;
    using System.Runtime.Serialization;
    
    namespace TestApplication
    {
        // common base class
        [DataContract]
        public class ModuleData : IExtensibleDataObject
        {
            public virtual ExtensionDataObject ExtensionData { get; set; }
        }
    
        [DataContract]
        public class AData : ModuleData
        {
            [DataMember]
            public string A { get; set; }
        }
    
        [DataContract]
        public class BData : ModuleData
        {
            [DataMember]
            public string B { get; set; }
        }
    
        [DataContract]
        public class CData : ModuleData
        {
            [DataMember]
            public string C { get; set; }
        }
    
        [DataContract]
        [KnownType(typeof(AData))]
        [KnownType(typeof(BData))]
        public class Project
        {
            [DataMember]
            public List<ModuleData> Data { get; set; }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                // new project object
                var project1 = new Project()
                {
                    Data = new List<ModuleData>()
                    {
                        new AData() { A = "A" },
                        new BData() { B = "B" },
                        new CData() { C = "C" }
                    }
                };
    
                // serialization; make CData explicitly known to simulate presence of "module C"
                var stream = new MemoryStream();
                var serializer1 = new DataContractSerializer(typeof(Project), new[] { typeof(CData) });
                serializer1.WriteObject(stream, project1);
    
                stream.Position = 0;
                var reader = new StreamReader(stream);
                Console.WriteLine(reader.ReadToEnd());
    
                // deserialization; skip "module C"
                stream.Position = 0;
                var serializer2 = new DataContractSerializer(typeof(Project));
                var project2 = serializer2.ReadObject(stream) as Project;
            }
        }
    }
    

    我还上传了VS2015解决方案 here

    2 回复  |  直到 7 年前
        1
  •  2
  •   dbc    7 年前

    你的问题 polymorphic known type hierarchy ,并且您希望使用 round-tripping mechanism 属于 DataContractSerializer xsi:type 类型提示指的是当前未加载到应用程序域中的类型。

    不幸的是,这个用例并没有通过往返机制来实现。该机制旨在缓存未知数据 ExtensionData 对象,前提是数据协定对象本身可以成功反序列化并实现 IExtensibleDataObject 不幸的是,在您的情况下,由于多态子类型无法识别,无法精确构造数据协定对象;相反,会引发以下异常:


    Message=“第4行位置6的错误。元素 http://www.Question45412824.com:ModuleData ' http://www.Question45412824.com:CData 反序列化程序不知道映射到此合同的任何类型。 将“CData”对应的类型添加到已知类型列表中-对于

    即使我尝试创建一个标记为 [CollectionDataContract] IExtensibleDataObject 为了缓存具有无法识别的契约的项,会引发相同的异常。

    一种解决方案 不会,因为它并不总是加载包含它们的程序集。因此,您可以做的是轻量级加载 而不是真正的类型时,真正的类型是不需要的。只要虚拟类型实现 IExtensibleDataObject 并且具有相同的数据契约名称空间和名称以及实类型,它们的数据契约将与多态集合中的“实”数据契约可互换。

    Dummies.CData 虚拟占位符:

    public static class Namespaces
    {
        // The data contract namespace for your project.
        public const string ProjectNamespace = "http://www.Question45412824.com"; 
    }
    
    // common base class
    [DataContract(Namespace = Namespaces.ProjectNamespace)]
    public class ModuleData : IExtensibleDataObject
    {
        public ExtensionDataObject ExtensionData { get; set; }
    }
    
    [DataContract(Namespace = Namespaces.ProjectNamespace)]
    public class AData : ModuleData
    {
        [DataMember]
        public string A { get; set; }
    }
    
    [DataContract(Namespace = Namespaces.ProjectNamespace)]
    public class BData : ModuleData
    {
        [DataMember]
        public string B { get; set; }
    }
    
    [DataContract(Namespace = Namespaces.ProjectNamespace)]
    [KnownType(typeof(AData))]
    [KnownType(typeof(BData))]
    public class Project
    {
        [DataMember]
        public List<ModuleData> Data { get; set; }
    }
    
    [DataContract(Namespace = Namespaces.ProjectNamespace)]
    public class CData : ModuleData
    {
        [DataMember]
        public string C { get; set; }
    }
    
    namespace Dummies
    {
        [DataContract(Namespace = Namespaces.ProjectNamespace)]
        public class CData : ModuleData
        {
        }
    }
    

    您将能够反序列化您的 Project 对象使用“real” CData

    class Program
    {
        static void Main(string[] args)
        {
            new TestClass().Test();
        }
    }
    
    class TestClass
    {
        public virtual void Test()
        {
            // new project object
            var project1 = new Project()
            {
                Data = new List<ModuleData>()
                {
                    new AData() { A = "A" },
                    new BData() { B = "B" },
                    new CData() { C = "C" }
                }
            };
    
            // serialization; make CData explicitly known to simulate presence of "module C"
            var extraTypes = new[] { typeof(CData) };
            var extraTypesDummy = new[] { typeof(Dummies.CData) };
    
            var xml = project1.SerializeXml(extraTypes);
    
            ConsoleAndDebug.WriteLine(xml);
    
            // Demonstrate that the XML can be deserialized with the dummy CData type.
            TestDeserialize(project1, xml, extraTypesDummy);
    
            // Demonstrate that the XML can be deserialized with the real CData type.
            TestDeserialize(project1, xml, extraTypes);
    
            try
            {
                // Demonstrate that the XML cannot be deserialized without either the dummy or real type.
                TestDeserialize(project1, xml, new Type[0]);
                Assert.IsTrue(false);
            }
            catch (AssertionFailedException ex)
            {
                Console.WriteLine("Caught unexpected exception: ");
                Console.WriteLine(ex);
                throw;
            }
            catch (Exception ex)
            {
                ConsoleAndDebug.WriteLine(string.Format("Caught expected exception: {0}", ex.Message));
            }
        }
    
        public void TestDeserialize<TProject>(TProject project, string xml, Type[] extraTypes)
        {
            TestDeserialize<TProject>(xml, extraTypes);
        }
    
        public void TestDeserialize<TProject>(string xml, Type[] extraTypes)
        {
            var project2 = xml.DeserializeXml<TProject>(extraTypes);
    
            var xml2 = project2.SerializeXml(extraTypes);
    
            ConsoleAndDebug.WriteLine(xml2);
    
            // Assert that the incoming and re-serialized XML are equivalent (no data was lost).
            Assert.IsTrue(XNode.DeepEquals(XElement.Parse(xml), XElement.Parse(xml2)));
        }
    }
    
    public static partial class DataContractSerializerHelper
    {
        public static string SerializeXml<T>(this T obj, Type [] extraTypes)
        {
            return obj.SerializeXml(new DataContractSerializer(obj == null ? typeof(T) : obj.GetType(), extraTypes));
        }
    
        public static string SerializeXml<T>(this T obj, DataContractSerializer serializer)
        {
            serializer = serializer ?? new DataContractSerializer(obj == null ? typeof(T) : obj.GetType());
            using (var textWriter = new StringWriter())
            {
                var settings = new XmlWriterSettings { Indent = true };
                using (var xmlWriter = XmlWriter.Create(textWriter, settings))
                {
                    serializer.WriteObject(xmlWriter, obj);
                }
                return textWriter.ToString();
            }
        }
    
        public static T DeserializeXml<T>(this string xml, Type[] extraTypes)
        {
            return xml.DeserializeXml<T>(new DataContractSerializer(typeof(T), extraTypes));
        }
    
        public static T DeserializeXml<T>(this string xml, DataContractSerializer serializer)
        {
            using (var textReader = new StringReader(xml ?? ""))
            using (var xmlReader = XmlReader.Create(textReader))
            {
                return (T)(serializer ?? new DataContractSerializer(typeof(T))).ReadObject(xmlReader);
            }
        }
    }
    
    public static class ConsoleAndDebug
    {
        public static void WriteLine(object s)
        {
            Console.WriteLine(s);
            Debug.WriteLine(s);
        }
    }
    
    public class AssertionFailedException : System.Exception
    {
        public AssertionFailedException() : base() { }
    
        public AssertionFailedException(string s) : base(s) { }
    }
    
    public static class Assert
    {
        public static void IsTrue(bool value)
        {
            if (value == false)
                throw new AssertionFailedException("failed");
        }
    }
    

    另一种解决方案 List<ModuleData> 使用实现的自定义集合 IXmlSerializable IXML可序列化 可能相当复杂,如图所示 here 以及,例如。, here

        2
  •  1
  •   Christian Waluga    7 年前

    dbc提出了一个很好的建议,即使用虚拟机来利用往返机制来完成这项工作,我通过根据需要动态生成虚拟类型,使解决方案更加通用。

    该解决方案的核心是以下在内部调用C#编译器的简单函数:

    private Type CreateDummyType(string typeName, string typeNamespace)
    {
        var className = $"DummyClass_{random_.Next()}";
        var code = $"[System.Runtime.Serialization.DataContract(Name=\"{typeName}\", Namespace=\"{typeNamespace}\")] public class {className} : ModuleData {{}}";
    
        using (var provider = new CSharpCodeProvider())
        {
            var parameters = new CompilerParameters();
            parameters.ReferencedAssemblies.Add("System.Runtime.Serialization.dll");
            parameters.ReferencedAssemblies.Add(GetType().Assembly.Location); // this assembly (for ModuleData)
    
            var results = provider.CompileAssemblyFromSource(parameters, code);
            return results.CompiledAssembly.GetType(className);
        }
    }
    

    我将其与DataContractResolver相结合,DataContractResolver负责处理任何未知类型,并根据需要生成假人,以在后续(反)序列化期间保留其数据。

    为了完整性,我将样本代码的最新迭代放在这里:

    using System;
    using System.IO;
    using System.Collections.Generic;
    using System.Runtime.Serialization;
    using System.Diagnostics;
    using System.Xml;
    using System.Xml.Linq;
    using Microsoft.CSharp;
    using System.CodeDom.Compiler;
    
    public static class Namespaces
    {
        public const string BaseNamespace = "http://www.Question45412824.com";
        public const string ProjectNamespace = BaseNamespace + "/Project";
        public const string ExtensionNamespace = BaseNamespace + "/Extension";
    }
    
    // common base class
    [DataContract(Namespace = Namespaces.ProjectNamespace)]
    public class ModuleData : IExtensibleDataObject
    {
        public ExtensionDataObject ExtensionData { get; set; }
    }
    
    [DataContract(Namespace = Namespaces.ProjectNamespace)]
    public class AData : ModuleData
    {
        [DataMember]
        public string A { get; set; }
    }
    
    [DataContract(Namespace = Namespaces.ProjectNamespace)]
    public class BData : ModuleData
    {
        [DataMember]
        public string B { get; set; }
    }
    
    [DataContract(Namespace = Namespaces.ProjectNamespace)]
    [KnownType(typeof(AData))]
    [KnownType(typeof(BData))]
    public class Project
    {
        [DataMember]
        public List<ModuleData> Data { get; set; }
    }
    
    [DataContract(Namespace = Namespaces.ProjectNamespace)]
    internal class CSubData : ModuleData
    {
        [DataMember]
        public string Name { get; set; }
    }
    
    
    [DataContract(Namespace = Namespaces.ExtensionNamespace)]
    public class CData : ModuleData
    {
        [DataMember]
        public ModuleData C { get; set; }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            new TestClass().Test();
        }
    }
    
    class TestClass
    {
        public virtual void Test()
        {
            // new project object
            var project1 = new Project()
            {
                Data = new List<ModuleData>()
                    {
                         new AData() { A = "A" },
                         new BData() { B = "B" },
                         new CData() { C = new CSubData() { Name = "C" } }
                    }
            };
    
            // serialization; make CData explicitly known to simulate presence of "module C"
            var extraTypes = new[] { typeof(CData), typeof(CSubData) };
    
            ConsoleAndDebug.WriteLine("\n== Serialization with all types known ==");
            var xml = project1.SerializeXml(extraTypes);
            ConsoleAndDebug.WriteLine(xml);
    
            ConsoleAndDebug.WriteLine("\n== Deserialization and subsequent serialization WITH generic resolver and unknown types ==");
            TestDeserialize(project1, xml, new GenericDataContractResolver());
    
            ConsoleAndDebug.WriteLine("\n== Deserialization and subsequent serialization WITHOUT generic resolver and unknown types ==");
            try
            {
                // Demonstrate that the XML cannot be deserialized without the generic resolver.
                TestDeserialize(project1, xml, new Type[0]);
                Assert.IsTrue(false);
            }
            catch (AssertionFailedException ex)
            {
                Console.WriteLine("Caught unexpected exception: ");
                Console.WriteLine(ex);
                throw;
            }
            catch (Exception ex)
            {
                ConsoleAndDebug.WriteLine(string.Format("Caught expected exception: {0}", ex.Message));
            }
        }
    
        public void TestDeserialize<TProject>(TProject project, string xml, Type[] extraTypes)
        {
            TestDeserialize<TProject>(xml, extraTypes);
        }
    
        public void TestDeserialize<TProject>(string xml, Type[] extraTypes)
        {
            var project2 = xml.DeserializeXml<TProject>(extraTypes);
    
            var xml2 = project2.SerializeXml(extraTypes);
    
            ConsoleAndDebug.WriteLine(xml2);
    
            // Assert that the incoming and re-serialized XML are equivalent (no data was lost).
            Assert.IsTrue(XNode.DeepEquals(XElement.Parse(xml), XElement.Parse(xml2)));
        }
    
        public void TestDeserialize<TProject>(TProject project, string xml, DataContractResolver resolver)
        {
            TestDeserialize<TProject>(xml, resolver);
        }
    
        public void TestDeserialize<TProject>(string xml, DataContractResolver resolver)
        {
            var project2 = xml.DeserializeXml<TProject>(resolver);
    
            var xml2 = project2.SerializeXml(resolver);
    
            ConsoleAndDebug.WriteLine(xml2);
    
            // Assert that the incoming and re-serialized XML are equivalent (no data was lost).
            Assert.IsTrue(XNode.DeepEquals(XElement.Parse(xml), XElement.Parse(xml2)));
        }
    }
    
    public static partial class DataContractSerializerHelper
    {
        public static string SerializeXml<T>(this T obj, Type[] extraTypes)
        {
            return obj.SerializeXml(new DataContractSerializer(obj == null ? typeof(T) : obj.GetType(), extraTypes));
        }
    
        public static string SerializeXml<T>(this T obj, DataContractResolver resolver)
        {
            return obj.SerializeXml(new DataContractSerializer(obj == null ? typeof(T) : obj.GetType(), null, int.MaxValue, false, false, null, resolver));
        }
    
        public static string SerializeXml<T>(this T obj, DataContractSerializer serializer)
        {
            serializer = serializer ?? new DataContractSerializer(obj == null ? typeof(T) : obj.GetType());
            using (var textWriter = new StringWriter())
            {
                var settings = new XmlWriterSettings { Indent = true };
                using (var xmlWriter = XmlWriter.Create(textWriter, settings))
                {
                    serializer.WriteObject(xmlWriter, obj);
                }
                return textWriter.ToString();
            }
        }
    
        public static T DeserializeXml<T>(this string xml, DataContractResolver resolver)
        {
            return xml.DeserializeXml<T>(new DataContractSerializer(typeof(T), null, int.MaxValue, false, false, null, resolver));
        }
    
        public static T DeserializeXml<T>(this string xml, Type[] extraTypes)
        {
            return xml.DeserializeXml<T>(new DataContractSerializer(typeof(T), extraTypes));
        }
    
        public static T DeserializeXml<T>(this string xml, DataContractSerializer serializer)
        {
            using (var textReader = new StringReader(xml ?? ""))
            using (var xmlReader = XmlReader.Create(textReader))
            {
                return (T)(serializer ?? new DataContractSerializer(typeof(T))).ReadObject(xmlReader);
            }
        }
    }
    
    public static class ConsoleAndDebug
    {
        public static void WriteLine(object s)
        {
            Console.WriteLine(s);
            Debug.WriteLine(s);
        }
    }
    
    public class AssertionFailedException : System.Exception
    {
        public AssertionFailedException() : base() { }
    
        public AssertionFailedException(string s) : base(s) { }
    }
    
    public static class Assert
    {
        public static void IsTrue(bool value)
        {
            if (value == false)
                throw new AssertionFailedException("failed");
        }
    }
    
    class GenericDataContractResolver : DataContractResolver
    {
        private static readonly Random random_ = new Random();
        private static readonly Dictionary<Tuple<string, string>, Type> toType_ = new Dictionary<Tuple<string, string>, Type>();
        private static readonly Dictionary<Type, Tuple<string, string>> fromType_ = new Dictionary<Type, Tuple<string, string>>();
    
        private Type CreateDummyType(string typeName, string typeNamespace)
        {
            var className = $"DummyClass_{random_.Next()}";
            var code = $"[System.Runtime.Serialization.DataContract(Name=\"{typeName}\", Namespace=\"{typeNamespace}\")] public class {className} : ModuleData {{}}";
    
            using (var provider = new CSharpCodeProvider())
            {
                var parameters = new CompilerParameters();
                parameters.ReferencedAssemblies.Add("System.Runtime.Serialization.dll");
                parameters.ReferencedAssemblies.Add(GetType().Assembly.Location); // this assembly (for ModuleData)
    
                var results = provider.CompileAssemblyFromSource(parameters, code);
                return results.CompiledAssembly.GetType(className);
            }
        }
    
        // Used at deserialization; allows users to map xsi:type name to any Type 
        public override Type ResolveName(string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver)
        {
            var type = knownTypeResolver.ResolveName(typeName, typeNamespace, declaredType, null);
    
            // resolve all unknown extension datasets; all other should be explicitly known.
            if (type == null && declaredType == typeof(ModuleData) && typeNamespace == Namespaces.ExtensionNamespace)
            {
                // if we already have this type cached, then return the cached one
                var typeNameAndNamespace = new Tuple<string, string>(typeName, typeNamespace);
                if (toType_.TryGetValue(typeNameAndNamespace, out type))
                    return type;
    
                // else compile the dummy type and remember it in the cache
                type = CreateDummyType(typeName, typeNamespace);
                toType_.Add(typeNameAndNamespace, type);
                fromType_.Add(type, typeNameAndNamespace);
            }
    
            return type;
        }
    
        // Used at serialization; maps any Type to a new xsi:type representation
        public override bool TryResolveType(Type type, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace)
        {
            if (knownTypeResolver.TryResolveType(type, declaredType, null, out typeName, out typeNamespace))
                return true; // known type
    
            // is the type one of our cached dummies?
            var typeNameAndNamespace = default(Tuple<string, string>);
            if (declaredType == typeof(ModuleData) && fromType_.TryGetValue(type, out typeNameAndNamespace))
            {
                typeName = new XmlDictionaryString(XmlDictionary.Empty, typeNameAndNamespace.Item1, 0);
                typeNamespace = new XmlDictionaryString(XmlDictionary.Empty, typeNameAndNamespace.Item2, 0);
                return true; // dummy type
            }
    
            return false; // unknown type
        }
    }