2016-06-30 3 views
3

question initiale

Je veux serialize un List<IXmlSerializable> changer le XmlType de la dynamique de classe IXmlSerializable (donc je ne peux pas utiliser les attributs des balises pour le faire)C# XML sérialisation remplacement Type de IXmlSerializable Classe

J'ai essayé d'utiliser XmlAttributeOverrides pour ce faire sans succès jusqu'à présent.

Voici un exemple de code illustrant la question:
classe IXmlSerializable (de MSDN):

public class Person : IXmlSerializable 
{ 
    // Private state 
    private string personName; 


    // Constructors 
    public Person(string name) 
    { 
     personName = name; 
    } 

    public Person() 
    { 
     personName = null; 
    } 


    // Xml Serialization Infrastructure 
    public void WriteXml(XmlWriter writer) 
    { 
     writer.WriteString(personName); 
    } 

    public void ReadXml(XmlReader reader) 
    { 
     personName = reader.ReadString(); 
    } 

    public XmlSchema GetSchema() 
    { 
     return (null); 
    } 


    // Print 
    public override string ToString() 
    { 
     return (personName); 
    } 
} 

classe de test (en utilisant la console pour la sortie):

class Program 
{ 
    static void Main(string[] args) 
    { 
     List<Person> lPersonList = new List<Person> { 
      new Person("First"), 
      new Person("Second"), 
      new Person("Third") 
     }; 
     XmlAttributeOverrides lOverrides = new XmlAttributeOverrides(); 
     XmlAttributes lAttributes = new XmlAttributes { XmlType = new XmlTypeAttribute("Employee") }; 
     lOverrides.Add(typeof(Person), lAttributes); 

     XmlSerializer lSerialiser = new XmlSerializer(typeof(List<Person>), lOverrides, null, new XmlRootAttribute("Employees"), null); 
     XmlSerializerNamespaces lNamespaces = new XmlSerializerNamespaces(); 
     lNamespaces.Add("", ""); 
     lSerialiser.Serialize(Console.Out, lPersonList, lNamespaces); 

     System.Console.WriteLine("Enter any key to close."); 
     System.Console.ReadKey(); 
    } 
} 

ici est ce que je veux obtenir:

<Employees> 
<Employee>First</Employee> 
<Employee>Second</Employee> 
<Employee>Third</Employee> 
</Employees> 

Mais je reçois cette erreur d'exécution:

System.InvalidOperationException: Seul attribut XmlRoot peut être spécifié pour la personne de type. Veuillez utiliser XmlSchemaProviderAttribute pour spécifier le type de schéma.

Remarque Quand ma personne classe n'applique pas IXmlSerializable, tout fonctionne bien ...

Quelqu'un pourrait-il me aider à ce sujet?


solution choisie (basée sur @dbc answer)

Comme @dbc a souligné, en utilisant une classe "substitution" est la meilleure façon de faire ce que je veux. Mais comme je l'ai dit, j'ai besoin de changer le type de personne dynamiquement, ce qui signifie que je ne peux pas utiliser les balises attributs.
J'utilise encore XmlAttributeOverrides dans ma conception finale, la voici:

Surrogate List<Person> classe (comme @dbc sans attributs tags):

public class EmployeesListSurrogate 
{ 
    public List<Person> EmployeeList { get; set; } 

    public static implicit operator List<Person>(EmployeesListSurrogate surrogate) 
    { 
     return surrogate == null ? null : surrogate.EmployeeList; 
    } 

    public static implicit operator EmployeesListSurrogate(List<Person> employees) 
    { 
     return new EmployeesListSurrogate { EmployeeList = employees }; 
    } 
} 

classe de test à l'aide de substitution:

class Program 
{ 
    static void Main(string[] args) 
    { 
     List<Person> lPersonList = new List<Person> { 
      new Person("First"), 
      new Person("Second"), 
      new Person("Third") 
     }; 

     XmlAttributeOverrides lOverrides = new XmlAttributeOverrides(); 
     XmlAttributes lEmployeesListAttributes = new XmlAttributes { XmlRoot = new XmlRootAttribute("Employees") }; 
     lOverrides.Add(typeof(EmployeesListSurrogate), lEmployeesListAttributes); 
     XmlAttributes lEmployeeAttributes = new XmlAttributes { XmlElements = { new XmlElementAttribute("Employee") } }; 
     lOverrides.Add(typeof(EmployeesListSurrogate), "EmployeeList", lEmployeeAttributes); 

     XmlSerializer lSerializer = new XmlSerializer(typeof(EmployeesListSurrogate), lOverrides); 
     XmlSerializerNamespaces lNamespaces = new XmlSerializerNamespaces(); 
     lNamespaces.Add("", ""); 
     lSerializer.Serialize(Console.Out, (EmployeesListSurrogate)lPersonList, lNamespaces); 
    } 
} 

Je veux terminer ceci avec un grand merci à @dbc, votre réponse a été très utile et info rmatif, j'ai beaucoup appris. Je ne peux pas vous mettre en colère mais j'espère que la communauté le fera!

Répondre

2

La façon la plus simple pour obtenir le XML que vous désirez serait sérialiser une classe « de substitution » comme suit:

[XmlRoot("Employees")] 
public class EmployeesListSurrogate 
{ 
    [XmlElement("Employee")] 
    public List<Person> EmployeeList { get; set; } 

    public static implicit operator List<Person>(EmployeesListSurrogate surrogate) 
    { 
     return surrogate == null ? null : surrogate.EmployeeList; 
    } 

    public static implicit operator EmployeesListSurrogate(List<Person> employees) 
    { 
     return new EmployeesListSurrogate { EmployeeList = employees }; 
    } 
} 

Cela élimine complètement le besoin de XmlAttributeOverrides. Ou vous pouvez utiliser XmlAttributeOverrides avec XmlAttributes.XmlElements pour spécifier le nom XML pour EmployeeList dynamiquement.

Cela étant dit, la raison pour laquelle la InvalidOperationException est levée lorsque vous essayez d'appliquer [XmlType] à un type qui implémente également IXmlSerializable est que XmlSerializer nécessite le nom du type doit être retourné par un mécanisme tout à fait différent, à savoir la méthode XmlSchemaProviderAttribute.MethodName spécifiée dans un [XmlSchemaProvider] attribut.

Lorsque [XmlSchemaProvider] est appliqué à un type IXmlSerializable, XmlSerializer recherchera une méthode statique publique du type dont le nom est spécifié dans le constructeur d'attribut et a la signature suivante:

public static XmlQualifiedName GetSchemaMethod(XmlSchemaSet xs) 
    { 
    } 

Le but de cette méthode est double:

  1. Il doit remplir le XmlSchemaSet avec le schéma prévu lors de la sérialisation instances du type. En testant, j'ai trouvé qu'il doit être rempli avec quelque chose de valide. Il ne peut pas être laissé vide, ou une exception sera levée.

    (je ne sais pas dans quelle mesure XmlSerializer valident effectivement contre le schéma lors de la sérialisation. La méthode est également invoquée lors de l'exportation des informations de schéma via xsd.exe.)

  2. Il doit retourner le nom du type XML pour le type .

    Cela semble être pourquoi Microsoft lève l'exception que vous voyez: puisque le fournisseur d'attribut de schéma doit retourner le nom de type, un attribut XmlType serait en conflit.

Ainsi, si je modifie votre Person classe comme suit:

[XmlSchemaProvider("GetSchemaMethod")] 
public class Person : IXmlSerializable 
{ 
    // Private state 
    private string personName; 

    // Constructors 
    public Person(string name) 
    { 
     personName = name; 
    } 

    public Person() 
    { 
     personName = null; 
    } 

    // This is the method named by the XmlSchemaProviderAttribute applied to the type. 
    public static XmlQualifiedName GetSchemaMethod(XmlSchemaSet xs) 
    { 
     string EmployeeSchema = @"<?xml version=""1.0"" encoding=""utf-16""?> 
<xs:schema elementFormDefault=""qualified"" xmlns:xs=""http://www.w3.org/2001/XMLSchema""> 
    <xs:import namespace=""http://www.w3.org/2001/XMLSchema"" /> 
    <xs:element name=""Employee"" nillable=""true"" type=""Employee"" /> 
    <xs:complexType name=""Employee"" mixed=""true""> 
    <xs:sequence> 
    <xs:any /> 
    </xs:sequence> 
    </xs:complexType> 
</xs:schema>"; 

     using (var textReader = new StringReader(EmployeeSchema)) 
     using (var schemaSetReader = System.Xml.XmlReader.Create(textReader)) 
     { 
      xs.Add("", schemaSetReader); 
     } 
     return new XmlQualifiedName("Employee"); 
    } 

    // Xml Serialization Infrastructure 
    public void WriteXml(XmlWriter writer) 
    { 
     writer.WriteString(personName); 
    } 

    public void ReadXml(XmlReader reader) 
    { 
     reader.MoveToContent(); 
     var isEmpty = reader.IsEmptyElement; 
     reader.ReadStartElement(); 
     if (!isEmpty) 
     { 
      personName = reader.ReadContentAsString(); 
      reader.ReadEndElement(); 
     } 
    } 

    public XmlSchema GetSchema() 
    { 
     return (null); 
    } 

    // Print 
    public override string ToString() 
    { 
     return (personName); 
    } 
} 

Et sérialiser votre List<Person> XML, je reçois le résultat suivant:

<ArrayOfEmployee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> 
    <Employee>First</Employee> 
    <Employee>Second</Employee> 
    <Employee>Third</Employee> 
</ArrayOfEmployee> 

Comme vous pouvez le voir, la Le nom de type XML pour Person a été spécifié avec succès.

Cependant, vous voulez remplacer dynamiquement le nom du type XML pour Person via XmlAttributeOverrides plutôt que de le mettre au type de compilation. Pour ce faire, il semblerait nécessaire de spécifier un XmlSchemaProviderAttribute à l'intérieur de XmlAttributes. Il n'est malheureusement pas possible de retrouver les pour les d'autres propriétés de XmlAttributes. Il semble que Microsoft n'a jamais implémenté une telle fonctionnalité. Ainsi, si vous voulez poursuivre cette conception, vous devrez faire quelque chose kludgy: remplacer temporairement le retour de GetSchemaMethod() lors de la création du sérialiseur de remplacement.Deux choses à garder à l'esprit:

  1. Sous le capot, XmlSerializer fonctionne en créant un ensemble dynamique. Si vous construisez un XmlSerializer avec new XmlSerializer(Type) ou new XmlSerializer(Type, String), alors .Net mettra en cache l'assemblage et le réutilisera lorsqu'un sérialiseur est construit une seconde fois. Par conséquent, essayer de surcharger temporairement le retour de GetSchemaMethod() lors de la construction d'un sérialiseur utilisant l'un de ces deux éléments échouera ou produira des résultats inattendus.

  2. Sinon, les assemblages dynamiques ne sont pas mises en cache et donc votre code doit cache sérialiseur manuellement ou une fuite grave de ressources. Voir Memory Leak using StreamReader and XmlSerializer.

    Dans ces cas, le remplacement temporaire du retour de GetSchemaMethod() pourrait fonctionner.

Tout cela étant dit, ce qui suit produit le XML dont vous avez besoin:

[XmlSchemaProvider("GetSchemaMethod")] 
public class Person : IXmlSerializable 
{ 
    // Private state 
    private string personName; 

    // Constructors 
    public Person(string name) 
    { 
     personName = name; 
    } 

    public Person() 
    { 
     personName = null; 
    } 

    [ThreadStatic] 
    static string personXmlTypeName; 

    const string defaultXmlTypeName = "Person"; 

    static string PersonXmlTypeName 
    { 
     get 
     { 
      if (personXmlTypeName == null) 
       personXmlTypeName = defaultXmlTypeName; 
      return personXmlTypeName; 
     } 
     set 
     { 
      personXmlTypeName = value; 
     } 
    } 

    public static IDisposable PushXmlTypeName(string xmlTypeName) 
    { 
     return new PushValue<string>(xmlTypeName,() => PersonXmlTypeName, val => PersonXmlTypeName = val); 
    } 

    // This is the method named by the XmlSchemaProviderAttribute applied to the type. 
    public static XmlQualifiedName GetSchemaMethod(XmlSchemaSet xs) 
    { 
     string EmployeeSchemaFormat = @"<?xml version=""1.0"" encoding=""utf-16""?> 
      <xs:schema elementFormDefault=""qualified"" xmlns:xs=""http://www.w3.org/2001/XMLSchema""> 
       <xs:import namespace=""http://www.w3.org/2001/XMLSchema"" /> 
       <xs:element name=""{0}"" nillable=""true"" type=""{0}"" /> 
       <xs:complexType name=""{0}"" mixed=""true""> 
       <xs:sequence> 
       <xs:any /> 
       </xs:sequence> 
       </xs:complexType> 
      </xs:schema>"; 
     var EmployeeSchema = string.Format(EmployeeSchemaFormat, PersonXmlTypeName); 

     using (var textReader = new StringReader(EmployeeSchema)) 
     using (var schemaSetReader = System.Xml.XmlReader.Create(textReader)) 
     { 
      xs.Add("", schemaSetReader); 
     } 
     return new XmlQualifiedName(PersonXmlTypeName); 
    } 

    // Xml Serialization Infrastructure 
    public void WriteXml(XmlWriter writer) 
    { 
     writer.WriteString(personName); 
    } 

    public void ReadXml(XmlReader reader) 
    { 
     reader.MoveToContent(); 
     var isEmpty = reader.IsEmptyElement; 
     reader.ReadStartElement(); 
     if (!isEmpty) 
     { 
      personName = reader.ReadContentAsString(); 
      reader.ReadEndElement(); 
     } 
    } 

    public XmlSchema GetSchema() 
    { 
     return (null); 
    } 

    // Print 
    public override string ToString() 
    { 
     return (personName); 
    } 
} 

public struct PushValue<T> : IDisposable 
{ 
    Action<T> setValue; 
    T oldValue; 

    public PushValue(T value, Func<T> getValue, Action<T> setValue) 
    { 
     if (getValue == null || setValue == null) 
      throw new ArgumentNullException(); 
     this.setValue = setValue; 
     this.oldValue = getValue(); 
     setValue(value); 
    } 

    #region IDisposable Members 

    // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class. 
    public void Dispose() 
    { 
     if (setValue != null) 
      setValue(oldValue); 
    } 

    #endregion 
} 

public static class PersonEmployeeListSerializerFactory 
{ 
    static Dictionary<Tuple<string, string>, XmlSerializer> serializers; 
    static object padlock = new object(); 

    static PersonEmployeeListSerializerFactory() 
    { 
     serializers = new Dictionary<Tuple<string, string>, XmlSerializer>(); 
    } 

    public static XmlSerializer GetSerializer(string rootName, string personName) 
    { 
     lock (padlock) 
     { 
      XmlSerializer serializer; 
      var key = Tuple.Create(rootName, personName); 
      if (!serializers.TryGetValue(key, out serializer)) 
      { 
       using (Person.PushXmlTypeName(personName)) 
       { 
        var lOverrides = new XmlAttributeOverrides(); 
        //var lAttributes = new XmlAttributes(); 
        //lOverrides.Add(typeof(Person), lAttributes); 

        serializers[key] = serializer = new XmlSerializer(typeof(List<Person>), lOverrides, new Type[0], new XmlRootAttribute(rootName), null); 
       } 
      } 
      return serializer; 
     } 
    } 
} 

Ensuite, faites

var lSerialiser = PersonEmployeeListSerializerFactory.GetSerializer("Employees", "Employee"); 

var lNamespaces = new XmlSerializerNamespaces(); 
lNamespaces.Add("", ""); 

var sb = new StringBuilder(); 
using (var writer = new StringWriter(sb)) 
    lSerialiser.Serialize(writer, lPersonList, lNamespaces); 

Console.WriteLine(sb); 

Mais comme vous pouvez le voir est beaucoup plus compliqué que d'utiliser la mère porteuse montré initialement.

Exemple fiddle montrant les deux options.