The simplest way to get the XML you desire would be to serialize a "surrogate" class as follows:
[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
. Or you can use XmlAttributeOverrides
along with XmlAttributes.XmlElements
to specify the XML name for EmployeeList
dynamically.
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.
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:
public static XmlQualifiedName GetSchemaMethod(XmlSchemaSet xs)
{
}
The purpose of this method is twofold:
It should fill in the XmlSchemaSet
with the expected schema when serializing instances of the type. 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
.)
It should return the XML type name for the type.
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.
Thus if I modify your Person
class as follows:
[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:
<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.
However, you want to dynamically override the XML type name for Person
via XmlAttributeOverrides
rather than set it at compile type. To do this, it would seem necessary to specify a XmlSchemaProviderAttribute
inside XmlAttributes
. Unfortunately, there is no XmlSchemaProvider
property to be found inside XmlAttributes
. 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. Two things to keep in mind:
Under the hood, XmlSerializer
works by creating a dynamic assembly. 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.
Thus, attempting to temporarily override the return of GetSchemaMethod()
when constructing a serializer using either of these will fail or produce unexpected results.
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.
In these cases temporarily overriding the return of GetSchemaMethod()
could work.
All this being said, the following produces the XML you require:
[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.