Monday, 23 November 2009

System.Data Serialisation Bug - Scenario 1

Introduction

System.Data contains a bug which can cause it to throw an exception during serlisation or deserialisation of a DataSet which contains a column that uses System.Object as its .NET type.

The bug manifests itself in two known scenarios:

  • Serialising a DataSet containing a column of type System.Object using a System.Xml.XmlTextWriter instance created via the static Create method; this throws a System.ArgumentException.
  • Deserialising a DataSet containing a column of type System.Object after having successfully passed it across the wire via Windows Communication Foundation (WCF) using netTcp binding; this throws a System.Data.DataException.

I reported the bug to Microsoft back in 2007 whilst using .NET 2.0 and 3.0. At the time was told "it's definitely in our system for a Post Orcas release". The bug still exists in .NET Framework 3.5 SP1, although I haven't checked any of the .NET 4.0 betas yet.

This post covers the first scenario you might come across – simply serialising the DataSet with an XmlTextWriter. I'll do a follow-up next week covering the second scenario.

Reproducing the Bug

To demonstrate this behaviour we're going to need a DataSet with a System.Object column, and we're going to need to populate it with some data.

static System.Data.DataSet CreateAndPopulateDataSet()
{
  // create a DataSet containing two columns: a string (Key) and an object (Value)
  System.Data.DataSet dataSet = new System.Data.DataSet();
  System.Data.DataTable dataTable = dataSet.Tables.Add();
  dataTable.Columns.Add("Key", typeof(string));
  dataTable.Columns.Add("Value", typeof(object));

  // add two rows, one containing an integer Value, and one containing a string Value
  dataTable.LoadDataRow(new object[] { "foo", 42 }, true);
  dataTable.LoadDataRow(new object[] { "bar", "Hello World" }, true);

  // return the DataSet
  return dataSet;
}

We can serialise this to XML easily using a System.Xml.XmlTextWriter:

static void SerializeDataSetUsingXmlTextWriter(System.Data.DataSet dataSet)
{
  // serialise the DataSet into a memoryStream using an XmlTextWriter
  System.IO.MemoryStream memoryStream = new System.IO.MemoryStream();
  System.Xml.XmlWriter xmlWriter = System.Xml.XmlTextWriter.Create(memoryStream);
  dataSet.WriteXml(xmlWriter);

  // write the contents of the MemoryStream to the console
  memoryStream.WriteTo(Console.OpenStandardOutput());
}

We can plug the two methods above into a console application, together with the following entry-point, to test the serialisation.

static void Main(string[] args)
{
  try
  {
    // create and populate the DataSet we'll use for the test
    System.Data.DataSet dataSet = CreateAndPopulateDataSet();

    // serialise the DataSet
    SerializeDataSetUsingXmlTextWriter(dataSet);
  }
  catch (System.Exception ex)
  {
    Console.WriteLine(ex);
  }

  // pause
  Console.ReadLine();
}

Doing so yields the following result:

System.ArgumentException: Invalid name character in 'xmlns:xs'. The ':' characte
r, hexadecimal value 0x3A, cannot be included in a name.
   at System.Xml.XmlWellFormedWriter.CheckNCName(String ncname)
   at System.Xml.XmlWellFormedWriter.WriteStartAttribute(String prefix, String l
ocalName, String namespaceName)
   at System.Data.DataTextWriter.WriteStartAttribute(String prefix, String local
Name, String ns)
   at System.Xml.XmlWriter.WriteAttributeString(String localName, String value)
   at System.Data.XmlDataTreeWriter.XmlDataRowWriter(DataRow row, String encoded
TableName)
   at System.Data.XmlDataTreeWriter.Save(XmlWriter xw, Boolean writeSchema)
   at System.Data.DataSet.WriteXml(XmlWriter writer, XmlWriteMode mode)
   at System.Data.DataSet.WriteXml(XmlWriter writer)
   at SystemDataSerialisationBugScenario1.Program.SerializeDataSetUsingXmlTextWr
iter(DataSet dataSet) in C:\Users\Ian.Picknell\Documents\Blog\System.Data Serial
isation Bug\SystemDataSerialisationBugScenario1\Program.cs:line 31
   at SystemDataSerialisationBugScenario1.Program.Main(String[] args) in C:\User
s\Ian.Picknell\Documents\Blog\System.Data Serialisation Bug\SystemDataSerialisat
ionBugScenario1\Program.cs:line 45

Understanding the Bug

So, what's going on here? Well, the exception occurred during System.Xml.XmlWellFormedWriter.CheckNCName which is clearly upset at finding a ':' character within the name 'xmlns:xs'. But 'xmlns:xs' isn't actually a name – the name is 'xs' which is within the namespace identified by the prefix 'xmlns'. Looking down the stack trace we can see a call to System.Xml.XmlWriter.WriteAttributeString(String localName, String value). We don't know what was being passed to this method, but by looking the documentation for System.Xml.XmlWriter.WriteAttributeString we can see that it has several overloads:

NameDescription
WriteAttributeString(String, String)When overridden in a derived class, writes out the attribute with the specified local name and value.
WriteAttributeString(String, String, String)When overridden in a derived class, writes an attribute with the specified local name, namespace URI, and value.
WriteAttributeString(String, String, String, String)When overridden in a derived class, writes out the attribute with the specified prefix, local name, namespace URI, and value.

Note that the overload which takes two strings (which is the overload being called) is clearly designed to accept a local name and a value. We could have guessed that from the parameter names localName and value in the stack trace, but it's nice to have this backed-up by the documentation. We've clearly got 'xmlns:xs' being passed around somewhere, which clearly isn't a local name – it's a namespace prefix and colon and a local name. Let's fire-up .NET Reflector and see what's going on in the call to System.Xml.XmlWriter.WriteAttributeString, which we can see from the stack trace occurs within System.Data.XmlDataTreeWriter.XmlDataRowWriter. So we're looking for calls to System.Xml.XmlWriter.WriteAttributeString from within System.Data.XmlDataTreeWriter.XmlDataRowWriter. There are actually quite a few such calls, so I won't list them all. They all look perfectly reasonable and innocent until we get to the last one:

this._xmlw.WriteAttributeString("xmlns:xs", "http://www.w3.org/2001/XMLSchema");

Now, that certainly looks wrong. As it's the two-string overload which is being called, the first parameter is expected to be a local name and it's clearly not. This is the bug.

Working Around the Bug

The solution is actually quite simple. In the SerializeDataSetUsingXmlTextWriter method we used to re-produce the bug, we created our System.Xml.XmlWriter instance using the static Create method of System.Xml.XmlTextWriter, as has been the recommended practice since .NET 2.0. But we can still create a System.Xml.XmlWriter using one of System.Xml.XmlTextWriter's instance constructors. So we simply replace this line in SerializeDataSetUsingXmlTextWriter:

System.Xml.XmlWriter xmlWriter = System.Xml.XmlTextWriter.Create(memoryStream);

with this one:

System.Xml.XmlWriter xmlWriter = new System.Xml.XmlTextWriter(memoryStream, null);

Running the program with this single change now produces the following output:

<NewDataSet><Table1><Key>foo</Key><Value xsi:type="xs:int" xmlns:xs="http://www.
w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">42<
/Value></Table1><Table1><Key>bar</Key><Value xsi:type="xs:string" xmlns:xs="http
://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instan
ce">Hello World</Value></Table1></NewDataSet>

That's better isn't it? Note that System.Data.XmlDataTreeWriter.XmlDataRowWrite is still calling the wrong overload of System.Xml.XmlWriter.WriteAttributeString; it's just that a System.Xml.XmlTextWriter created via the instance constructor is more forgiving than one created using the static Create method.

In my next post I'll describe a second scenario where this bug can manifest itself (using the netTcp binding within WCF) where the workaround provided above cannot be used.

See Also

1 comment:

  1. Thanks for this. Appears to still be the same bug in .net 4

    ReplyDelete