简体   繁体   English

C#XML序列化覆盖IXmlSerializable类的类型

[英]C# XML serialization override Type of IXmlSerializable Class

Initial question 最初的问题

I want to serialize a List<IXmlSerializable> changing the XmlType of the IXmlSerializable class dynamically (so I can't use attributes tags to do that) 我想serialize一个List<IXmlSerializable>动态更改IXmlSerializable类的XmlType (所以我不能使用属性标签来做到这一点)

I' ve tried to use XmlAttributeOverrides to do that with no success so far. 到目前为止,我已经尝试使用XmlAttributeOverrides来做到这一点并没有成功。

Here is a sample code illustrating the issue : 以下是说明问题的示例代码:
IXmlSerializable class (from MSDN ) : IXmlSerializable类(来自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);
    }
}

Test class (using console for output) : 测试类(使用控制台输出):

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();
    }
}

Here is what I want to get : 这是我想要的:

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

But I get this error on runtime : 但是我在运行时遇到这个错误:

System.InvalidOperationException: Only XmlRoot attribute may be specified for the type Person. System.InvalidOperationException:可以仅为Person类型指定XmlRoot属性。 Please use XmlSchemaProviderAttribute to specify schema type. 请使用XmlSchemaProviderAttribute指定架构类型。

Note When my Person class is not implementing IXmlSerializable , everything works well... 注意当我的Person类没有实现IXmlSerializable ,一切运行良好......

Could someone help me on that ? 有人可以帮助我吗?


Choosen solution (based on @dbc answer ) 选择解决方案(基于@dbc答案

As @dbc pointed out, using a "surrogate" class is the easiest way of doing what I want. 正如@dbc指出的那样,使用“代理”类是做我想要的最简单的方法。 But as I've said, I need to change the Person type dynamically, which means that I can't use attributes tags. 但正如我所说,我需要动态更改Person类型,这意味着我不能使用属性标签。
So I still use XmlAttributeOverrides in my final design, here it is : 所以我仍然在我的最终设计中使用XmlAttributeOverrides ,这里是:

Surrogate List<Person> Class (same as @dbc without attributes tags) : 代理List<Person> (与没有属性标记的@dbc相同):

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

Test class using surrogate : 使用代理人的测试类:

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

I want to end this with a big thanks to @dbc, your answer was very helpfull and informative, I've learned a lot. 我想以@dbc对此表示感谢,您的答案非常有帮助且内容丰富,我学到了很多东西。 I can't upvote you but I hope the community will do ! 我不能赞成你,但我希望社区能做到!

The simplest way to get the XML you desire would be to serialize a "surrogate" class as follows: 获取所需XML的最简单方法是序列化“代理”类,如下所示:

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

This completely eliminates the need for XmlAttributeOverrides . 这完全消除了对XmlAttributeOverrides的需要。 Or you can use XmlAttributeOverrides along with XmlAttributes.XmlElements to specify the XML name for EmployeeList dynamically. 或者,您可以使用XmlAttributeOverridesXmlAttributes.XmlElements动态指定EmployeeList的XML名称。

That being said, the reason the InvalidOperationException is thrown when you try to apply [XmlType] to a type that also implements IXmlSerializable is that XmlSerializer requires the type name to be returned via an entirely different mechanism, namely the XmlSchemaProviderAttribute.MethodName method specified in an [XmlSchemaProvider] attribute. 也就是说,当您尝试将[XmlType]应用于同时实现IXmlSerializable的类型时抛出InvalidOperationException的原因是XmlSerializer需要通过完全不同的机制返回类型名称,即在一个中指定的XmlSchemaProviderAttribute.MethodName方法。 [XmlSchemaProvider]属性。

When [XmlSchemaProvider] is applied to an IXmlSerializable type, XmlSerializer will look for a public static method of the type whose name is specified in the attribute constructor and has the following signature: [XmlSchemaProvider]应用于IXmlSerializable类型时, XmlSerializer将查找该类型的公共静态方法,该方法的名称在属性构造函数中指定,并具有以下签名:

    public static XmlQualifiedName GetSchemaMethod(XmlSchemaSet xs)
    {
    }

The purpose of this method is twofold: 这种方法的目的有两个:

  1. It should fill in the XmlSchemaSet with the expected schema when serializing instances of the type. 在序列化类型的实例时,它应该使用期望的模式填充XmlSchemaSet By testing, I found that it has to be filled with something valid . 通过测试,我发现它必须填充有效的东西 It cannot just be left empty, or an exception will be thrown. 它不能只是留空,否则会抛出异常。

    (I don't know to what extent XmlSerializer actually validates against the schema when serializing. The method also gets invoked when exporting schema information via xsd.exe .) (我不知道XmlSerializer在序列化时实际验证模式的程度。通过xsd.exe导出模式信息时也会调用该方法。)

  2. It should return the XML type name for the type. 它应该返回该类型的XML类型名称。

    This seems to be why Microsoft throws the exception you are seeing: since the schema attribute provider should return the type name, an XmlType attribute would be in conflict. 这似乎是微软抛出您所看到的异常的原因:由于架构属性提供程序应该返回类型名称,因此XmlType属性会发生冲突。

Thus if I modify your Person class as follows: 因此,如果我按如下方式修改Person类:

[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);
    }
}

And serialize your List<Person> to XML, I get the following result: 并将List<Person>序列化为XML,我得到以下结果:

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

As you can see, the XML type name for Person has been successfully specified. 如您所见,已成功指定Person的XML类型名称。

However, you want to dynamically override the XML type name for Person via XmlAttributeOverrides rather than set it at compile type. 但是,您希望通过XmlAttributeOverrides 动态覆盖 Person的XML类型名称,而不是将其设置为编译类型。 To do this, it would seem necessary to specify a XmlSchemaProviderAttribute inside XmlAttributes . 为此,似乎有必要在XmlAttributes指定XmlSchemaProviderAttribute Unfortunately, there is no XmlSchemaProvider property to be found inside XmlAttributes . 不幸的是, XmlAttributes找不到XmlSchemaProvider属性。 It appears Microsoft never implemented such functionality. 看来微软从未实现过这样的功能。 Thus, if you want to pursue this design further, you're going to need to do something kludgy: temporarily override the return of GetSchemaMethod() when creating the override serializer. 因此,如果你想进一步追求这个设计,你需要做一些kludgy:在创建覆盖序列化器时暂时覆盖GetSchemaMethod()的返回。 Two things to keep in mind: 要记住两件事:

  1. Under the hood, XmlSerializer works by creating a dynamic assembly. 在引擎盖下, XmlSerializer通过创建动态程序集来工作。 If you construct an XmlSerializer with new XmlSerializer(Type) or new XmlSerializer(Type, String) , then .Net will cache the assembly and reuse it when a serializer is constructed a second time. 如果使用new XmlSerializer(Type)new XmlSerializer(Type, String)构造XmlSerializer ,则.Net将缓存程序集并在第二次构造序列化程序时重用它。

    Thus, attempting to temporarily override the return of GetSchemaMethod() when constructing a serializer using either of these will fail or produce unexpected results. 因此,在使用其中任何一个构造序列化程序时尝试临时覆盖GetSchemaMethod()的返回将失败或产生意外结果。

  2. Otherwise the dynamic assemblies are not cached and so your code must cache the serializer manually or have a severe resource leak. 否则,动态程序集不会被缓存,因此您的代码必须手动缓存序列化程序或导致严重的资源泄漏。 See Memory Leak using StreamReader and XmlSerializer . 请参阅使用StreamReader和XmlSerializer的内存泄漏

    In these cases temporarily overriding the return of GetSchemaMethod() could work. 在这些情况下,暂时覆盖GetSchemaMethod()的返回可能会起作用。

All this being said, the following produces the XML you require: 所有这些,以下产生您需要的XML:

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

Then do 然后做

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);

But as you can see this is much more complicated than using the surrogate shown initially. 但正如您所看到的,这比使用最初显示的代理更复杂。

Sample fiddle showing both options. 示例小提琴显示两个选项。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM