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

WCF: Data contract serializer with multiple modules

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

我正在寻找一种解决方案,允许我在未加载(或什至不可用)完整模块集的情况下保留现有的序列化数据。我认为这是告诉反序列化器 "if you don't understand what you get, then don't try to serialize it, but please keep the data somewhere so that you can put it back when serializing the next time" 的一种方式。我认为我的问题与 round-tripping 有关,但我(还)没有很成功地找到有关如何处理在序列化操作之间可能添加或删除复杂类型的情况的提示。

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

<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

您的问题 是您有一个 polymorphic known type hierarchy, and you would like to use the round-tripping mechanism of DataContractSerializer to read and save "unknown" known types, specifically XML elements with an xsi:type 类型提示指向当前未加载到您的应用程序域中的类型。

不幸的是,这个用例根本没有通过往返机制实现。该机制旨在在 ExtensionData object, provided that the data contract object itself can be successfully deserialized and implements IExtensibleDataObject 中缓存未知数据 members。不幸的是,在您的情况下,无法准确构造数据协定对象,因为无法识别多态子类型;而是抛出以下异常:

System.Runtime.Serialization.SerializationException occurred
Message="Error in line 4 position 6. Element 'http://www.Question45412824.com:ModuleData' contains data of the 'http://www.Question45412824.com:CData' data contract. The deserializer has no knowledge of any type that maps to this contract. Add the type corresponding to 'CData' to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding it to the list of known types passed to DataContractSerializer."

即使我尝试创建一个标有 [CollectionDataContract] 并实现 IExtensibleDataObject 的自定义通用集合来缓存具有无法识别的合同的项目,也会引发相同的异常。

一个解决方案 是利用你的问题比往返问题稍微简单的事实。您(软件架构师)实际上知道所有可能的多态子类型。 您的软件 没有,因为它并不总是加载包含它们的程序集。因此,您可以做的是在不需要真实类型时加载轻量级 虚拟类型 而不是真实类型。只要虚拟类型实现 IExtensibleDataObject 并具有与真实类型相同的数据协定名称空间和名称,它们的数据协定就可以与多态集合中的 "real" 数据协定互换。

因此,如果您按如下方式定义类型,添加一个 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
    {
    }
}

您将能够使用 "real" CData 或 "dummy" 版本反序列化您的 Project 对象,如下面的测试所示:

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 并完全手动处理多态序列化,缓存 XML 用于未知元素列表中的未知多态子类型。然而,我不建议这样做,因为即使 IXmlSerializable 的直接实现也可能非常复杂,如图 here and, e.g., .

根据 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 相结合,后者处理任何未知类型并根据需要生成虚拟对象以在后续(反)序列化期间保留其数据。

为了完整起见,我将最近迭代的示例代码放在这里:

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