OverrideXml
Control XML serialization of classes that you can't change
.NET framework provides an XmlSerializer
class for converting user-defined types to and from XML.
The shape of the XML produced can be highly customized using attributes.
This works very well, but it creates two problems:
- To change how the class is serialized one must change the source code and recompile. This may be not possible or not desired.
- There is no way to serialize a class to XML in more than one fashion. This issue is especially painful when two or more versions of serialized XML exist in the wild.
Microsoft provides a little known
XmlAttributeOverrides
class that allows to control XML serialization without changing source code. Unfortunately, it is quite difficult to use.
OverrideXml
provides an intuitive interface for changing
the shape of XML without touching the source code of the serialized class.
Example
Suppose you have two classes:
public class Continent { public string Name { get; set; } public List<Country> Countries { get; set;} } public class Country { public string Name { get; set; } public string Capital { get; set; } }
If you serialize them to XML as is, the result would be similar to this:
<Continent> <Name>Europe</Name> <Countries> <Country> <Name>France</Name> <Capital>Paris</Capital> </Country> <Country> <Name>Germany</Name> <Capital>Berlin</Capital> </Country> <Country> <Name>Spain</Name> <Capital>Madrid</Capital> </Country> </Countries> </Continent>
Let's say you need the XML to look like this:
<continent name="Europe"> <state name="France" capital="Paris" /> <state name="Germany" capital="Berlin" /> <state name="Spain" capital="Madrid" /> </continent>
You could do this by adding custom attributes to the class definitions and recompiling the classes:
[XmlRoot("continent")] public class Continent { [XmlAttribute("name")] public string Name { get; set; } [XmlElement("state")] public List<Country> Countries { get; set;} } public class Country { [XmlAttribute("name")] public string Name { get; set; } [XmlAttribute("capital")] public string Capital { get; set; } }
The trouble is, changing custom attributes is often not a viable option. Ability to make this change depends on many conditions, and I probably missed some:
- You must have access to full, compilable source code.
- You should be able to change it, recompile it, and deploy resulting binaries where needed.
- The change must not interfere with any backward compatibility requirements.
- If the assembly in question is strongly named, then
- Either you should be able to recompile and redeploy all the dependencies,
- Or you should have access to the key file and be willing to setup all necessary binding redirects.
Fortunately, you can still influence the way our classes are serialized without changing the class definitions. You can build
an XmlAttributeOverrides
object that contains equivalents of the required custom attributes, and passing it to the XmlSerializer
:
Continent continent = /* some continent */; var serializer = new XmlSerializer(typeof(Continent), GetOverrides()); serializer.Serialize(outputStream, continent);
Here's how you do it with OverrideXml:
XmlAttributeOverrides GetOverrides() { return new OverrideXml() .Override<Continent>() .XmlRoot("continent") .Member("Name").XmlAttribute("name") .Member("Countries").XmlElement("state") .Override<Country>() .Member("Name").XmlAttribute("name") .Member("Capital").XmlAttribute("capital") .Commit(); }
Here's equivalent code that uses XmlAttributeOverrides
directly. I honestly tried to make it as concise as possible:
XmlAttributeOverrides GetOverridesRaw() { var overrides = new XmlAttributeOverrides(); overrides.Add(typeof(Continent), new XmlAttributes { XmlRoot = new XmlRootAttribute("continent") }); overrides.Add(typeof(Continent), "Name", new XmlAttributes { XmlAttribute = new XmlAttributeAttribute("name")}); overrides.Add(typeof(Continent), "Countries", new XmlAttributes { XmlElements = { new XmlElementAttribute("state") } }); overrides.Add(typeof(Country), "Name", new XmlAttributes { XmlAttribute = new XmlAttributeAttribute("name")}); overrides.Add(typeof(Country), "Capital", new XmlAttributes { XmlAttribute = new XmlAttributeAttribute("capital")}); return overrides; }
As you can see, the code using OverrideXml is less verbose, and easier to use and maintain. OverrideXml was designed to avoid verbosity
and ceremonial code, as much as C# syntax allows. It uses "fluent" approach in which each operation returns the original object,
so the operations can be chained. Attribute overrides are built incrementally, and final Commit()
creates and returns an instance of XmlAttributeOverrides
.
If you like to think in terms of design patterns, you may consider OverrideXml
an application of the
builder pattern.
Full source code of the example can be found at https://github.com/ikriv/OverrideXml/blob/master/src/Program.cs.
Complete Visual Studio solution is available at https://github.com/ikriv/OverrideXml/archive/master.zip: look inside the "src" directory.
How to use OverrideXml
OverrideXml
is just one source file. It is therefore not packaged as an assembly. To add the file to your project,
in Visual Studio menu go to Tools→Library Package Manager→Manage NuGet Packages for Solution
and search for OverrideXml
in online packages.
Alternatively, you can download the source from https://github.com/ikriv/OverrideXml/blob/master/src/OverrideXml.cs and add it to your project manually.
OverrideXml
is implemented by a single class Ikriv.Xml.OverrideXml
. It allows you to override XML attributes
for one or more types as follows:
XmlAttributeOverrides overrides = new OverrideXml() .overrides for type1 .overrides for type2(optional) ... .overrides for typeN (optional) .Commit();
Overrides for each type look like this:
.Override<SomeClass>() // or Override(typeof(SomeClass)) .First attribute override for SomeClass .Second attribute override for SomeClass ... .Member("Member1"). First attribute override for SomeClass.Member1 Second attribute override for SomeClass.Member1 ... .Member("Member2"). First attribute override for SomeClass.Member2 Second attribute override for SomeClass.Member2 ... ...
Calls to Override<T>
and Member("Name")
don't add any attributes by themselves: they
only change the context for the subsequent attribute calls. Call to Member("Name")
also verifies that
given member exists and is public.
Attribute override calls correspond to members defined in XmlAttributes
class. The most important or
ones are summarized below. OverrideXml
has individual calls for most frequent cases, and a generic
Attr()
call that allows to specify any applicable attribute. Attr()
shall be used when
none of the specific calls applies.
Call | Meaning |
---|---|
.Attr(customAttribute) | Simulates specified custom attribute on type or member |
.XmlAttribute() | Simulates [XmlAttribute] on a member |
.XmlAttribute("name") | Simulates [XmlAttribute("name")] on a member |
.XmlElement() | Simulates [XmlElement] on a member |
.XmlElement("name") | Simulates [XmlElement("name")] on a member |
.XmlIgnore() | Simulates [XmlIgnore] on a member |
.XmlIgnore(false) | Cancels existing [XmlIgnore] attribute on a member |
.XmlRoot("element") | Simulates [XmlRoot("element")] attribute on a type |
An override will cancel all conflicting attributes that may exist in the class definition.
E.g. .XmlAttribute()
call will make given member serialize as an attribute, even if it was decorated with
[XmlElement]
in the class definition.
Here's an example of overrides:
var overrides = new OverrideXml() .Override<Continent>() .Attr(new XmlRootAttribute("continent") { Namespace="http://mycompany.com/schema1" }) .Member("Name").XmlIgnore() .Member("Countries") .XmlArray() .XmlArrayItem("foo") .Commit();
Use cases for OverrideXml
Third Party Components
Changing and recompiling third party components is usually either impossible or undesired. Also, if you do succeed in changing it, it may break the code that relies on the original way the component was serialized. Similar considerations apply to your own code whose usage you do not fully control. You cannot be sure that everyone uses the new version, and that this new version does not break any assumptions our users have made.
OverrideXml
allows you to change the way third party components are serialized
without changing their source. Project ThirdPartySample
in the demo solution
demonstrates how to customize serialization of a built-in .NET framework class AppDomainSetup
.
Here's a default serialization:
<AppDomainSetup> <ApplicationBase>C:\dev\ThirdPartySample\bin\Debug\</ApplicationBase> <ConfigurationFile>C:\dev\ThirdPartySample\bin\Debug\ThirdPartySample.exe.Config</ConfigurationFile> <DisallowPublisherPolicy>false</DisallowPublisherPolicy> <DisallowBindingRedirects>false</DisallowBindingRedirects> <DisallowCodeDownload>false</DisallowCodeDownload> <DisallowApplicationBaseProbing>false</DisallowApplicationBaseProbing> <ApplicationName>ThirdPartySample.exe</ApplicationName> <LoaderOptimization>NotSpecified</LoaderOptimization> <SandboxInterop>false</SandboxInterop> </AppDomainSetup>
After we apply these overides...
var overrides = new OverrideXml() .Override<AppDomainSetup>() .Member("ApplicationBase").XmlAttribute() .Member("ConfigurationFile").XmlAttribute() .Member("DisallowPublisherPolicy").XmlAttribute().XmlDefaultValue(false) .Member("DisallowBindingRedirects").XmlAttribute().XmlDefaultValue(false) .Member("DisallowCodeDownload").XmlAttribute().XmlDefaultValue(false) .Member("DisallowApplicationBaseProbing").XmlAttribute().XmlDefaultValue(false) .Member("ApplicationName").XmlAttribute() .Member("LoaderOptimization").XmlAttribute().XmlDefaultValue(LoaderOptimization.NotSpecified) .Member("SandboxInterop").XmlAttribute().XmlDefaultValue(false) .Commit();
... most of the default values become not necessary to serialize and others are converted to attributes. Resulting custom XML looks like this:
<AppDomainSetup ApplicationBase="C:\dev\ThirdPartySample\bin\Debug\" ConfigurationFile="C:\dev\ThirdPartySample\bin\Debug\ThirdPartySample.exe.Config" ApplicationName="ThirdPartySample.exe" />
Full source code of this example is in the ThirdPartySample
project of the solution.
Versioning
A typical versioning scenario unfolds as follows. You build version one of some class (e.g. MyData
)
that is serialized to externally stored XML documents. Then you build version two of MyData
that is
serialized to a new XML schema, but you want it to be able to read old files as well.
Of course, if you have drastically altered the structure of MyData
, you would need to create a migration
class that reads old files and manually converts them to instances of MyData
version 2. On the other
hand, if you only added new members to MyData
, and they all are optional, you may be able
to get backward compatibility for free: XmlSerializer
would simply set new members to default values
when reading old XML files.
The most irritating case is when the changes between version 1 and version 2 are minor, but the backward compatibility is lost. Typical cases include converting elements to attributes, changing XML namespace, renaming members, and the like. In this cases OverrideXml can help. Let's consider a concrete example. Suppose version 1 of your class looks as follows:
/* MyData, version 1 */ public class Organization { public string Name { get; set;} public string Headquarters { get; set;} } [XmlRoot(Namespace=Names.XmlNamespace)] public class MyData { public DateTime CreationTime { get; set; } public Organization Organization { get; set; } }
It serializes to XML like this:
<MyData xmlns="http://ikriv.com/OverrideXml/FileCreator/v1"> <CreationTime>2014-08-11T01:43:34.9688402Z</CreationTime> <Organization> <Name>United Nations</Name> <Headquarters>New York</Headquarters> </Organization> </MyData>
Then in version 2 you make a number of relatively small changes:
- XML namespace is changed
MyData
can now store multiple organizations instead of oneOrganization.Headquarters
is renamed toOrganization.Location
Organization.Name
andOrganization.Location
are now attributes, not elements
/* MyData, version 2 */ public class Organization { [XmlAttribute] public string Name { get; set;} [XmlAttribute] public string Location { get; set;} // was Headquarters } [XmlRoot(Namespace=Names.XmlNamespace)] public class MyData { [XmlAttribute] public DateTime CreationTime { get; set; } public List<Organization> Organizations { get; set; } }
Resulting XML looks like this:
<MyData CreationTime="2014-08-12T02:52:30.649185Z" xmlns="http://ikriv.com/OverrideXml/FileCreator/v2"> <Organizations> <Organization Name="United Nations" Location="New York" /> <Organization Name="FIFA" Location="Zurich" /> </Organizations> </MyData>
If you want to read XMl version 1 with MyData
class version 2, this can be achieved by using the following overrides:
new OverrideXml() .Override<MyData>() .Attr(new XmlRootAttribute { Namespace = Names.XmlNamespaceV1 }) .Member("Organizations").XmlElement("Organization") .Override<Organization>() .Member("Name").XmlElement() .Member("Location").XmlElement("Headquarters") .Commit();
Full source code of this example is in the Versioning
solution folder of the Visual Studio Solution.
Limitations of attribute overrides
Attribute overrides are a great tool for customizing produced XML, but their power has very specific limits. Here's a number of things attribute overrides cannot do:
- Cannot add new member to a class. However, you can effectively remove a member using
.XmlIgnore()
- Cannot convert read-only
{get;}
property to read-write{get;set;}
property. Read-only properties will not be serialized. - Cannot change type of a member. E.g. there is no way to serialize
System.Version
asString
. - Cannot do value conversion. E.g. if old version stored distance in kilometers and new version stores distance in meters, there is no way to
tell the serializer that
1.2345
in the source XML should be deserialized as1234.5
. - Cannot turn custom serialization (
IXmlSerializable
) on or off. I.e. there is no way to treat class that implementsIXmlSerializable
as if it were a "normal" class, or vice versa.
If you run into these limitations, you will have to use either manual serialization, or "conversion" classes. E.g.
the answers to this StackOverflow question give
two different ways to serialize System.Version
to XML using a conversion class.
Still, there are many scenarios where attribute overrides are useful, and OverrideXml
makes them
more intuitive and easier to use than built-in .NET framework API.
Feedback
Questions? Comments? Feel free to
Leave feedback
Copyright (c) Ivan Krivyakov. Last updated: Aug 16, 2014