WCF:数据合同串行器与多个模块

问题描述:

在我使用WCF数据契约序列化序列化到XML我的C#项目之一。但是,该框架由多个扩展模块组成,可以加载或不加载,具体取决于某些启动配置(如果有必要,我使用MEF)。未来,模块列表可能会增长,我担心这种情况有一天可能会导致模块特定数据出现问题。据我所知,我可以实现一个数据合约解析器来双向帮助序列化程序查找类型,但是如果项目包含无法解释的数据,会发生什么情况,因为相关模块未加载?WCF:数据合同串行器与多个模块

我要寻找一个解决方案,允许我保留在没有全套的模块被加载(或甚至可用的)情况下,现有的串行数据。我认为这是一种告诉反序列化器的方法,“如果你不明白你得到了什么,那么不要尝试序列化它,但是请保存数据到某个地方,以便在序列化下一个序列时将它放回原处时间”。我认为我的问题与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

+0

你能不能给一些细节你如何生成的XML开始?从多态列表创建XML时,“DataContractSerializer”使用[已知类型](https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/data-contract-known-types)机制并生成看起来像“' – dbc

+0

注意'i:type =“AData”'这是一个使用标准[xsi:type](https://www.w3.org/TR/xmlschema-1/#xsi_type)属性的类型提示。 ,相反,你的收藏品的元素名称正在改变,这表明你真的在使用'XmlSerializer'。你能确认吗?你可以在你的问题中添加一些代码,以便我们了解你正在做什么? – dbc

+0

感谢您的回复。我的问题是不要让这些东西序列化,只要所有的模块都存在。当类型不知道时我有一个问题(例如,如果我只加载一个子集),因为序列化器会引发异常。我会花时间把这个简化为一个简单的例子,并相应地编辑我的问题。 –

您的问题是你有一个polymorphic known type hierarchy,你想用的DataContractSerializerround-tripping mechanism读取和保存“未知”已知类型,与xsi:type类型提示具体的XML元素指的是一种目前尚未加载进入您的应用程序域。

不幸的是,这种使用情况根本不是由往返机制来实现。该机制被设计以缓存ExtensionData对象内部未知数据成员,条件是该数据契约对象本身可以成功地解串行化,并实现IExtensibleDataObject。不幸的是,在你的情况下,数据契约对象不能被精确地构造,因为多态的子类型是无法识别的;代替以下异常获取引发:

System.Runtime.Serialization.SerializationException发生
消息=“在第4行的位置误差6.元 ‘http://www.Question45412824.com:ModuleData’包含 ‘http://www.Question45412824.com:CData’数据合同的数据的 反序列化程序不知道映射到此合同的任何类型。 添加对应于“CData的”到已知类型的列表中的类型 - 用于 例如,通过使用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 
    { 
    } 
} 

您将能够使用任何“真正的” CData或“虚拟”的版本,以反序列化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>并且完全手动处理多态序列化,为未知元素列表中的未知多态子类型缓存XML。然而,我不会推荐,因为即使是直接实现的IXmlSerializable也可能非常复杂,如和here所示。

+0

+1:这真的帮助我前进!然而,我担心如果我在各个模块中使用多态结构,我可能会遇到问题。对于*别,我可以简单地为模块“保留”足够的虚拟类型,并对此感到满意,但是如果模块使用多态数据,我想我也必须具有所有这些子类型的虚拟变量。我将因此评估第二种可能的解决方案。 –

+0

@ChristianWaluga - 你只需要在本地程序集外部使用的多态类型的虚拟变量。它很重视将类型声明为“内部”,我们并不总是这么做。对于每个可选模块,一种构造代码的方法可能是创建两个可能的DLL--一个是真正的外部类型,另一个是虚拟外部类型。 – dbc

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