Introduction
There are a couple known issues with the XmlSerializer:
- First-time use is painfully slow, because of the temporary assembly generation
and compilation.
- If you want to do something else other than straight serialization, there's
no easy way of customizing it.
Whidbey will bring a solution to the first, in the form of the "sgen"
tool, which will generate at design-time the serialization assembly. I still
haven't looked at the solutions it offers for the second issue.
So, are you condemned to waiting 'till Whidbey? Are you condemned to either
choose between the straightforward-but-almost-impossible-to-customize XmlSerializer
approach and endless lines of XmlReader/XmlDocument/XPath code to do almost
the same yet generally with less performance just to gain the flexibility you
need? Well, I turns out that I am a firmly believer on automating boring and
repetitive tasks, so I'm not happy at all with people choosing the later, because
I believe it leads to hard to maintain, inflexible, and really ugly code. You
waste so much effort that would be better put to work on creative stuff, and
making your app great. And performance is always a big issue, at least in my
view.
This article explains how to achieve design-time XmlSerializer generation,
how it's customized to allow for the stuff you always thought impossible, such
as getting events for each kind of object being deserialized.
Recall that I wrote this code in 4 hours as I waited for my plane to Redmond
on the airport :o), so it's not the most gorgeous piece of code you'll see coming
from me, but it certainly gets a pretty cool job done!
Near the end of a previous
article I said it was not possible to customize the XmlSerializer to use
something else other than the built-in dynamically generated code. I was wrong.
What follows is an explanation of how I achieved what seemed impossible.
Usage
Right now, this is a command-line utility. Its arguments are:
SGen.exe fullTypeName assemblyFile targetNamespace outputFileName
First argument is the namespace-qualified name of the type you'll be using
with the XmlSerializer. The second argument is the file name of the assembly
containing the type. Next follows the namespace you want to put the generated
code into. And finally you can specify the file name to write the generated
code to.
The output of the tool is a set of classes you can use for XML serialization,
which not only allows you to avoid the run-time impact of temporary assembly
generation, but also allows you to attach to events that are exposed for each
element that will be deserialized. If you had a class called Order like the
following (presumably generated from an XSD):
public class Order
{
// Members plus optional XML serialization attributes
}
You would get the following classes:
- OrderReader: a class
inheriting from the XmlSerializationReader-derived class generated by the
XmlSerializer, also included in the file but as an inner
class of the OrderSerializer.
- OrderWriter: a class
inheriting from the XmlSerializationWriter-derived class generated by the
XmlSerializer, also included in the file but as an inner
class of the OrderSerializer too.
- OrderSerializer: XmlSerializer-derived
class that allows you to pass the two previous custom classes for serialization,
in order to skip the dynamic code generation.
- OrderDeserializedHandler:
handler for an event exposed by the OrderReader,
called OrderDeserialized, which you can attach to in
order to perform additional processing when deserialization is done.
The custom serializer is generated basically to allow for the custom reader/writer
classes to be passed-in, as the XmlSerializer
class itself doesn't allow for this, but provides the hook methods CreateReader
and CreateWriter, as
well Serialize and Deserialize
overloads receiving the result of those method calls. So in order to deserialize
an order class, you instantiate the reader, pass it to the custom serializer,
and call Deserialize as usual:
// Create the typed reader.
OrderReader or = new OrderReader();
// Create custom serializer receiving the custom reader.
OrderSerializer os = new OrderSerializer(or);
// Deserialize as usual
object order = os.Deserialize(inputReader);
The same process would be done for serializing the object. At this point you
already saved a *huge* amount of processing for the initial hit on this class
upon deserialization. And this impact is higher as the object to deserialize
is more complex.
But there's more to this generation process than just performance boost. Let's
say the Order class, among other properties, has one of type Customer, and then
a collection of Items. The custom reader would expose an event for each of them,
so you can perform additional processing. So you could do the following:
public void Test()
{
XmlTextReader tr = new XmlTextReader(GetInputStream());
// Typed reader.
OrderReader or = new OrderReader();
// Attach to events for each object!
reader.CustomerDeserialized += new CustomerDeserializedHandler(OnCustomerDeserialized);
reader.OrderDeserialized += new OrderDeserializedHandler(OnOrderDeserialized);
reader.ItemDeserialized += new ItemDeserializedHandler(OnItemDeserialized);
// Custom serializer receiving the custom reader.
OrderSerializer os = new OrderSerializer(or);
// Deserialize as usual, but all event handlers called while deserializing!
object order = os.Deserialize(tr);
// Do something with the order...
}
private void OnCustomerDeserialized(Customer customer)
{
Console.WriteLine(customer.FirstName);
}
private void OnOrderDeserialized(Order order)
{
Console.WriteLine(order.Id);
}
private void OnItemDeserialized(Item item)
{
Console.WriteLine(item.Price);
}
You can even use this event callbacks as a more programmer-friendly approach
to XML processing than the lower-level XmlReader. You just have to create the
XSD, generate classes with xsd.exe or something
better, and use the SGen generated reader. You just attach to the events
for each element you're interested in, and process it using friendly properties/fields
instead of XmlReader.Value, XmlReader.GetAttribute and the like.
You can easily set this tool to run as a post-build event, by setting appropriate
project field to:
..\..\..\SGen\bin\Debug\SGen SGen.Tests.Order SGen.Tests.dll SGen.Tests.Serialization ..\..\OrderSerialization.cs
Implementation
The code generated by the XmlSerializer can be kept around by using the
technique explained in a previous post. Using that approach, the SGen utility
instantiates an XmlSerializer passing the type you specify as arguments. After
that, using hacky reflection, it retrieves the temporary code location burned
deep inside the XmlSerializer private members and internal classes. Afterwards,
using a mix of CodeDom and raw string manipulation, the final code is generated.
I've done quite a bit of regular expressions-based parsing too, and there's
a region called "Code templates" that provide the skeleton for the
generation process.
I didn't create everything using CodeDom because the XmlSerializer generates
just C# output, therefore, there wasn't much benefit in trying to do everything
the "right" way. The code and the regular expressions are pretty nasty
for the non-accustomed eye, so I'll save you the trouble and instead point you
to the respective
code download.
What's worth seeing is how the XmlSerializer is being extended:
/// <summary>Custom serializer for Order type.</summary>
public class OrderSerializer : System.Xml.Serialization.XmlSerializer
{
OrderReader _reader;
OrderWriter _writer;
/// <summary>Constructs the serializer with a pre-built reader.</summary>
public OrderSerializer(OrderReader reader)
{
_reader = reader;
}
/// <summary>Constructs the serializer with a pre-writer reader.</summary>
public OrderSerializer(OrderWriter writer)
{
_writer = writer;
}
/// <summary>Constructs the serializer with pre-built reader and writer.</summary>
public OrderSerializer(OrderReader reader, OrderWriter writer)
{
_reader = reader;
_writer = writer;
}
/// <summary>See <see cref="XmlSerializer.CreateReader"/>.</summary>
protected override XmlSerializationReader CreateReader()
{
return _reader;
}
/// <summary>See <see cref="XmlSerializer.CreateWriter"/>.</summary>
protected override XmlSerializationWriter CreateWriter()
{
return _writer;
}
/// <summary>See <see cref="XmlSerializer.Deserialize"/>.</summary>
protected override object Deserialize(XmlSerializationReader reader)
{
if (!(reader is OrderReader))
throw new ArgumentException("reader");
return ((OrderReader)reader).Read();
}
/// <summary>See <see cref="XmlSerializer.Serialize"/>.</summary>
protected override void Serialize(object o, XmlSerializationWriter writer)
{
if (!(writer is OrderWriter))
throw new ArgumentException("writer");
((OrderWriter)writer).Write((SGen.Tests.Order)o);
}
// Inner XmlSerializer-generated reader and writer classes
}
So it *was* extensible in the end, right? It's a little bit cumbersome, but
it's possible to extend it as you can see.
Don't forget to download
the tool and play with it!
About the author
Daniel Cazzulino
kzu@aspnet2.com
Blog: http://clariusconsulting.net/blogs/kzu/
Daniel Cazzulino (a.k.a. kzu) lives in Buenos Aires, Argentina, and is a senior architect, developer, and cofounder of Clarius Consulting S.A. He has coauthored several books on Web development and server controls with ASP.NET, written and reviewed many articles for ASP Today and C# Today, and currently enjoys sharing his .NET and XML experiences through his blog at http://clariusconsulting.net/blogs/kzu/.
|