Tuesday, November 24, 2009

Errors Enumerating DataRows in a DataTable in a strongly typed DataSet that has been upgraded from .NET 2.0 to .NET 3.5

In Visual Studio 2008 the version number of MSDataSetGenerator, the tool which generates strongly typed DataSets based upon a *.xsd file, increased from 2.0.50727.1433 to 2.0.50727.3074. The only significant difference in the output files is that when targeting framework .NET 3.5, the new version of the tool uses System.Data.TypedTableBase (from System.Data.DataSetExtensions) as the base class rather than System.Data.DataTable.
This sounds like a small difference but it can trip you up! Let’s say I have the following program:
class Program
{
   static void Main(string[] args)
   {
      SearchResultsDataSet ds = new SearchResultsDataSet();
      ds.SearchResults.AddSearchResultsRow(Guid.NewGuid(), "Ref1", "Title1");
      ds.SearchResults.AddSearchResultsRow(Guid.NewGuid(), "Ref2", "Title2");
      ds.AcceptChanges();
      foreach (SearchResultsDataSet.SearchResultsRow row in ds.SearchResults)
      {
         Console.WriteLine(row.Ref);
      }
      Console.ReadLine();
   }
}

SearchResultsDataSet is a strongly typed DataSet in a separate assembly called X. Assembly X and my test program are both built targeting .NET 2.0, and deployed to a customer site. I subsequently upgrade both my test program and X to .NET 3.5 and rebuild them, but only deploy the test program to the customer site; X has not changed, so I leave the existing copy in place. When my test program attempts to enumerate the DataRows in the “SearchResults” table I will get the following error:

C:\Users\alexr\Documents\Visual Studio 2008\Projects\TypedDataSetTest\bin\Debug> TypedDataSetTest.exe

Unhandled Exception: System.MissingMethodException: Method not found: 'System.Collections.IEnumerator SearchResultsDataTable.GetEnumerator()'.
at TypedDataSetTest.Program.Main(String[] args)

C:\Users\alexr\Documents\Visual Studio 2008\Projects\TypedDataSetTest\bin\Debug>

Or let’s say I do this the other way around: I upgrade both projects to .NET 3.5, rebuild both but deploy the new version of X alongside the old version of the test program. When I try to enumerate the “SearchResults” table I will get the following error:

C:\Users\alexr\Documents\Visual Studio 2008\Projects\TypedDataSetTest\bin\Debug>
TypedDataSetTest.exe

Unhandled Exception: System.NullReferenceException: Object reference not set to
an instance of an object.
at System.Data.TypedTableBase`1.GetEnumerator()
at TypedDataSetTest.Program.Main(String[] args) in C:\Users\alexr\Documents\V
isual Studio 2008\Projects\TypedDataSetTest\Program.cs:line 17

C:\Users\alexr\Documents\Visual Studio 2008\Projects\TypedDataSetTest\bin\Debug>

What’s really going on AND what can make this problem difficult to spot
You may be thinking “What kind of fool would change the .NET framework an assembly was targeting and then not deploy it to the live environment”. And you’re right. But consider the following scenario:

  1. Create a strongly typed DataSet in an assembly that targets .NET 2.0.
  2. MSDataSetGenerator generates a .Designer.cs file containing the code.
  3. Upgrade the assembly to target .NET 3.5.
  4. The .Designer.cs file is already in place and it doesn’t get regenerated. It doesn’t get regenerated UNTIL you make a change to the *.xsd file.
This caught us out with our product, because we deploy Hotfixes by keeping the Assembly Version Number the same and deploying only assemblies which have been modified (using other file metadata to distinguish them). We were caught unawares by our .NET 3.5 strongly typed DataSets suddenly morphing from being based upon System.Data.DataTable to being based upon System.Data.TypedTableBase and some of our integration components broke.

Another thing that makes this problem hard to identify is that a lot of us (quite rightly) try to diagnose problems like this by using Reflector to decompile the assembly. In this case though, the source code of the test program is the same in both cases; it’s the underlying IL that is different. The convenient C# construct of foreach (SearchResultsDataSet.SearchResultsRow row in ds.SearchResults) has this going on behind the scenes:

First, for the .NET 2.0 version:

L_0046: callvirt instance class [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet/SearchResultsDataTable [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet::get_SearchResults()
L_004b: callvirt instance class [mscorlib]System.Collections.IEnumerator [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet/SearchResultsDataTable::GetEnumerator()
L_0050: stloc.2
L_0051: br.s L_006d
L_0053: ldloc.2
L_0054: callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
L_0059: castclass [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet/SearchResultsRow
L_005e: stloc.1

And then the .NET 3.5 version:

L_0046: callvirt instance class [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet/SearchResultsDataTable [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet::get_SearchResults()
L_004b: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1 [System.Data.DataSetExtensions]System.Data.TypedTableBase`1::GetEnumerator()
L_0050: stloc.2
L_0051: br.s L_0068
L_0053: ldloc.2
L_0054: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1::get_Current()


(I might have missed a bit off the start and the end, I don’t understand IL that well). As you can see, in the second example the compiled assembly uses a generic enumerator rather than a standard System.Collections.IEnumerator. But this difference is not visible in C#.

2 comments:

  1. So what is the solution to this?I also encountered the same problem

    ReplyDelete
  2. Just hit this problem today, and made a work-around by using the enumerator from DataRowCollection instead of the autogenerated code for the strongly typed data set.

    That is, instead of this:

    foreach (SomeTypedDataSet.SomeDataTableRow row in someTypedDataSetInstance.SomeDataTable)
    { ... }

    I did this:

    foreach (SomeTypedDataSet.SomeDataTableRow row in someTypedDataSetInstance.Tables["SomeDataTable"].Rows)
    { ... }

    Worked just fine, but is of course a bit of a kludge.

    ReplyDelete