2017-07-31 53 views
1

在我使用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

感謝您的回覆。我的問題是不要讓這些東西序列化,只要所有的模塊都存在。當類型不知道時我有一個問題(例如,如果我只加載一個子集),因爲序列化器會引發異常。我會花時間把這個簡化爲一個簡單的例子,並相應地編輯我的問題。 –

回答

1

您的問題是你有一個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

1

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