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

时间:2017-07-31 10:01:51

标签: c# .net wcf serialization datacontractserializer

在我的一个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

1 个答案:

答案 0 :(得分:2)

您的问题是您有polymorphic known type hierarchy,并且您希望使用DataContractSerializer的{​​{3}}来阅读并保存“未知”已知类型,特别是带有round-tripping mechanism类型提示的XML元素,指的是当前未加载到您的应用域中的类型。

不幸的是,这个用例并不是由往返机制实现的。该机制旨在缓存xsi:type对象内的未知数据成员,前提是数据协定对象本身可以成功反序列化并实现ExtensionData。不幸的是,在您的情况下,无法精确构造数据协定对象,因为多态子类型无法识别;而是抛出以下异常:

  

发生了System.Runtime.Serialization.SerializationException   消息=“第4行位置6的错误。元素   'IExtensibleDataObject'包含的数据   'http://www.Question45412824.com:ModuleData'数据合同。该   反序列化器不知道映射到此合同的任何类型。   将与“CData”对应的类型添加到已知类型列表中 - for   例如,通过使用KnownTypeAttribute属性或将其添加到   传递给DataContractSerializer的已知类型列表。“

即使我尝试创建一个标有[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或“虚拟”版本反序列化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的自定义集合,并完全手动处理多态序列化,为未知的多态子类型缓存XML未知元素列表。我不建议这样做,因为即使是IXmlSerializable的直接实现也可能非常复杂,如http://www.Question45412824.com:CData所示,例如here