Some Background
In CruiseControl.NET we have a file merge task. This task will load one or more files and merge them into a single XML document. From this document the reports get generated.
Recently we made some changes to CruiseControl.NET to allow reports and report artefacts to come from a different location, which now allows images and HTML reports to be included. This was actually a flow-on effect from the new NDepend and NCover tasks, as these tasks needed to display non-XML data.
These worked so well, one of the other developers asked how to include items from other tasks in these reports, and even potentially user-specified files. The bad news at the time is this was not possible. The only merging task we had was the merge files task (<merge> element) and this task merges into the XML document.
This left us with two options:
- Add a new task to merge non-XML files
- Modify the merge task to do non-merge copying
Since I didn’t particularly like the idea of a new task just for this specialised functionality, I decided to look at extending the current merge task.
The Problem
An example configuration for the current merge task would look something like this:
<merge><files><file>File 1</file><file>File 2</file><file>File 3</file></files></merge>
In contrast, this is what I wanted to achieve:
<merge target="somewhere"><files><file>File 1</file><file action="Copy">File 2</file><file action="Merge">File 3</file></files></merge>
The problem with this lies in the action attributes – NetReflector does not handle them. The closest we can get is something like:
<merge target="somewhere"><files><file>File 1</file><file><action>Copy</action><name>File 2</name></file><file><action>Merge</action><name>File 3</name></file></files></merge>
Which would break existing config files! So, based on plain-vanilla NetReflector we would be stuck.
But, there is a way around this, and one that doesn’t involve modifying NetReflector at all.
Expanding the Attribute
All of the configuration is based on NetReflector attributes and most of it uses ReflectorPropertyAttribute. Like all the NetReflector attributes it has a constructor that requires a name, which is the name of the element. However, this attribute is unique – it also has a second constructor allows the developer to pass in a factory Type.
The factory argument allows the developer to override the default serialisation/de-serialisation behaviour with their own custom behaviour.
The factory type must implement ISerialiserFactory. This is a simple interface with one method that creates a new serialiser. This serialiser can then serialise/de-serialise in any way it wants!
The serialiser must implement IXmlMemberSerialiser, which also includes IXmlSerialiser. IXmlSerialiser has a Read() and a Write() method, while IXmlMemberSerialiser provides two properties and an extra method (I haven’t figured out what they are needed for yet!)
While it is possible to directly implement these interfaces, a simpler way is to inherit from XmlMemberSerialiser. This class implements all of the methods and properties – we just choose which ones we want to override (normally Read() and Write()).
Both the Read() and the Write() methods work directly with XML. The Read() method receives in the node to be de-serialised, while the Write() method receives an XmlWriter to work with. Generally, if you going to override one, you should override both – especially as both are used in CruiseControl.NET.
The only down-side to this approach is you have to completely implement the serialisation/de-serialisation behaviour. The default serialiser automatically handles whether it is an array, a list or a single value, our custom serialiser needs to do this for itself.
Reading a Node
As an example of how to implement a custom serialiser, I’ll go over what I have done for the new serialiser for the merge file task. This serialiser handles the desired XML I showed above.
The code for the Read() method is:
public override object Read(XmlNode node, NetReflectorTypeTable table){var fileList = new List<MergeFileInfo>();if (node != null){// Validate the attributesif (node.Attributes.Count > 0){throw new NetReflectorException("A file list cannot directly contain attributes.\r\nXML: " + node.OuterXml);}// Check each elementforeach (XmlElement fileElement in node.SelectNodes("*")){if (fileElement.Name == "file"){// Make sure there are no child elementsif (fileElement.SelectNodes("*").Count > 0){throw new NetReflectorException("file cannot contain any sub-items.\r\nXML: " + node.OuterXml);}// Load the filenamevar newFile = new MergeFileInfo();newFile.FileName = fileElement.InnerText;// Load the merge actionvar typeAttribute = fileElement.GetAttribute("action");if (string.IsNullOrEmpty(typeAttribute)){newFile.MergeAction = MergeFileInfo.MergeActionType.Merge;}else{try{newFile.MergeAction = (MergeFileInfo.MergeActionType)Enum.Parse(typeof(MergeFileInfo.MergeActionType),typeAttribute);}catch (Exception error){throw new NetReflectorConverterException("Unknown action :'" + typeAttribute + "'\r\nXML: " + node.OuterXml, error);}}fileList.Add(newFile);}else{// Unknown sub-itemthrow new NetReflectorException(fileElement.Name + " is not a valid sub-item.\r\nXML: " + node.OuterXml);}}}return fileList.ToArray();}
The arguments are an XmlNode and a NetReflectorTable. The node is the complete node to be de-serialised, in this case the <files> element and everything in it. The NetReflectorTable is a type custom to NetReflector – it basically contains a dictionary of all the available types that can be loaded. In this scenario we are not going to use it, but it is used in other serialisers (e.g. for hash-tables).
The majority of the work in this method is validation – since this will override the automatic behaviour we have to do everything ourselves! Since the end objective is to return an array of MergeFileInfo instances, I need to make sure the user isn’t trying anything that is not allowed.
The main working lines are instantiating a new MergeFileInfo, setting the file name from the text of the element and then loading the action from the attribute. If the attribute is not there, then it assumes a Merge action. Once this is done it is added to a List<MergeFileInfo>. This gets done for every child element, and then at the end it returns an array of MergeFileInfo items.
Writing an Element
The Read() was a fairly long method because it included lots of validation (plus it might need some more later one). In contrast the Write() method is very simple. Here is the code that I wrote for it:
public override void Write(XmlWriter writer, object target){var list = target as MergeFileInfo[];if (list != null){writer.WriteStartElement(base.Attribute.Name);foreach (var file in list){writer.WriteStartElement("file");writer.WriteAttributeString("action", file.MergeAction.ToString());writer.WriteString(file.FileName);writer.WriteEndElement();}writer.WriteEndElement();}}
The writer is where the XML will be written to, the target is the item to be serialised.
The only check is that the target is actually an array of MergeFileInfo. Once this is verified, then is is a simple matter of iterating through the array and dumping all the values to XML.
Beyond the Ordinary – Serialisers
I should mention that once this was implemented, the rest of the modifications were very simple. Now the <merge> task has the ability to copy files, rather than just merge them. And not only that, but it is also backwards compatible with previous versions!
So, while this post started off talking about the <merge> task, the real focus is how we can extend NetReflector serialisation by using a custom serialiser and type factory. if you want to see how it is done, take a look at the MergeFileSerialiser, MergeFileSerialiserFactory and MergeFilesTask classes.
On a final note, I should mention this did not need any changes to NetReflector. This functionality has always been there, even though I have only just discovered it. As well as the new merge files, time-outs also use this functionality – take a look at the TimeoutSerializer and TimeoutSerializerFactory classes.
So hopefully this opens some more possibilities, happy coding…
RSS - Posts