Click here to Skip to main content
15,886,639 members
Articles / Programming Languages / C#
Article

Generics, Serialization and NUnit

Rate me:
Please Sign up or sign in to vote.
4.79/5 (20 votes)
10 Jul 2006CPOL12 min read 73.2K   241   55   5
Generic class to help de/serialize any Type, plus a discussion on NUnit testing Generic classes

Installing the supporting applications in the given directories is merely a recommendation: they're hardcoded in various spots in the source code.

Introduction

What's this article actually about?

Kelvin Static Class Diagram

  • Brief intro to Generics
  • Start building a Generic class : Kelvin (the Generic Serialization Helper)
  • Generics 'with' keyword
  • Testing using NUnit
  • Checking test code coverage using NCover
  • Quick explanation of the GenericBinder nested in Kelvin

Background: Generics

I'm not going to do an 'introduction to Generics': there's plenty of introduction to Generics articles around via Google if you wish to read more.

However, it is interesting to know that although Generics have only recently appeared in .NET 2.0, the concept has been around for a long while - Microsoft Research published a research paper on Generics in May 2001, and released 'Gyro' extension for the Microsoft Shared Source CLI (MS-SSCLI) in May 2003. MSDN magazine printed their first Introducing Generics article in September 2003, and an updated article on the final release of .NET 2.0 in January 2006.

I suspect most developers will encounter Generics using the new System.Collections.Generic classes such as Collection, Dictionary, SortedDictionary, List, Queue and Stack. You might even find yourself implementing KeyedCollection for a Business Object with an 'embedded key', or extending any one of the above. It's possible you'll never need to write your own complete Generic class.

But under what conditions "would" you write your own Generic class from scratch? What else can you accomplish with Generics, besides Collections? What else, besides storage/sorting, is 'common' enough that it makes sense to genericize...?

Kelvin the Generic Serialization Helper

Firstly, an acknowledgement, because the idea for a Gene<code>ric wrapper was not mine. Chris Webb deserves full credit for coming up with the most interesting proposal for a Generic class that I've heard to-date (Thanks Chris!). His concept of a type-safe class to read/write to SQL Server 2005 XML columns was the genesis of this article.

A serialization wrapper is a good candidate for a Generic implementation because it's the sort of operation that you might want to perform on pretty much any class, and have it type-checked. The initial test case was the serialization code in the Searcharoo3 project -- here's what the 'old' code looks like, and what we will replace it with:

OLD
C#
System.IO.Stream stream = new System.IO.FileStream(
    Preferences.CatalogFileName+".dat", System.IO.FileMode.Create);
System.Runtime.Serialization.IFormatter formatter
    = new System.Runtime.Serialization.Formatters.Binary.
                        BinaryFormatter();
formatter.Serialize(stream, this); /* 'this' is a Catalog object */
stream.Close();
PLANNED
C#
Kelvin<Catalog>.ToBinaryFile
            (this,Preferences.CatalogFileName+".dat");

Since we already have some existing code, the best place to start the Kelvin Generic Serialization Helper is with that code. Firstly, we declare the class (notice it's a static class - another new 2.0 feature) with a Generic Type parameter (the <T> bit). The first method ToBinaryFile consists purely of the existing Searcharoo3 code, with the Generic Typed object cryo as the first parameter.

C#
public static class Kelvin<T>
{
    public static bool ToBinaryFile (T cryo, string fileName)
    {
        try
        {
            System.IO.Stream stream
                = new System.IO.FileStream
                (fileName, System.IO.FileMode.Create);
            System.Runtime.Serialization.IFormatter formatter
                 = new System.Runtime.Serialization.Formatters.Binary.
                            BinaryFormatter();
            formatter.Serialize(stream, cryo);
            stream.Close();
            return true;
        }
        catch (System.IO.DirectoryNotFoundException)
       {
           return false;
       }
    }
}

OT: what's with the name Kelvin? Deserializing objects sometimes get referred to as 'dehydrating' them, as though the serialization process is similar to freeze-drying of food products, which are reconstituted/rehydrated by adding water. Kelvin (the temperature measure) begins at 'absolute zero', so it seemed appropriative. Excuse the geek humor - but what else would you call it, the GenericSerializationHelperClass()?

Anyway, we now have the beginnings of a Generic class. Since Searcharoo3 has a matching deserialize function, we'll include that too. This time the generic type T is more important - notice how the return values are cast to T. Even though we don't know "what" the return type will be, we can use the Generic type identifier T from the class declaration public static class Kelvin<T> anywhere a normal type would be used: as the method return type, to cast an object, as a method parameter type or a local variable.

C#
public static T FromBinaryFile(string frozenObjectFileName)
{
    if (System.IO.File.Exists(frozenObjectFileName))
    {
        System.IO.Stream stream = new System.IO.FileStream
            (frozenObjectFileName, System.IO.FileMode.Open);
        System.Runtime.Serialization.IFormatter formatter
            = new System.Runtime.Serialization.Formatters.Binary.
                            BinaryFormatter();
        try
        {
            return (T)formatter.Deserialize(stream);
        }
        catch (System.Runtime.Serialization.SerializationException)
        {
        // Unable to find assembly
        //'App_Code.vj-e_8q4,Version=0.0.0.0,Culture=neutral,
        // PublicKeyToken=null'
        // try the same operation with a custom Binder class
            stream.Position = 0;
            formatter.Binder = new GenericBinder();
            return (T)formatter.Deserialize(stream);
        }
        finally
        {
            stream.Close();
        }
    }
    else
    {
        throw new System.IO.FileNotFoundException
            (frozenObjectFileName+" was not found.");
    }
}

P. S. Ignore the catch block for now, it's discussed later.

With the Kelvin<T>.FromBinaryFile and Kelvin<T>.ToBinaryFile methods implemented, the most obvious place to test them was in Searcharoo3. The entire Save() and Load() methods were replaced by these two lines respectively (notice how the type parameter has been set to Catalog, which is the class we need to serialize):

C#
// Searcharoo.Net.Catalog.Save()
Kelvin<Catalog>.ToBinaryFile(this, Preferences.CatalogFileName + ".dat");
// Searcharoo.Net.Catalog.Load()
Catalog catalog = Kelvin<Catalog>.FromBinaryFile
                (Preferences.CatalogFileName + ".dat");

Et voila, it works! That's a start, but to properly test our code, shouldn't we cover more than just one possible 'input'. And with Generic classes, isn't the Type itself one of the inputs?

How to test Generic classes...

One of the challenges with Generic code is going to be testing it! Just because Kelvin works with one Type, doesn't mean it will work with ALL Types... for a start we know that System.Net.Mail.MailMessage is a class that is specifically NOT Serializable. What would happen if we tried to use Kelvin<MailMessage>.ToBinaryFile()? A run-time Exception <code>of course! So if we know that our Generic class needs things to be Serializable, how can we declare it so that Kelvin<MailMessage> will cause a compiler error rather than allow us developers to make that silly mistake? Using the where syntax allows us to specify that our Generic type MUST implement one or more interfaces, and hence tell the compiler exactly what types will work with our Generic class!

C#
public static class Kelvin<T> where T : ISerializable

Easy eh? Except in our case (i.e. for Kelvin the Generic Serialization Helper), we probably DON'T want our type parameter to absolutely "require" ISerializable implementation, because it would prevent us from serializing a whole pile of things, such as int[] and string[]. Seems strange, but for now we'll leave the where clause off, although in other cases using where would at least limit the number of types we needed to test!

How to test Generic classes with NUnit...

Back to the question, how can we test a Generic class? I had hoped it might be possible to write a 'Generic Test', like this:

C#
[TestFixture]
public class KelvinFixture<T>
{
    private T _objectToTest;
    protected T ObjectUnderTest
    {
        set { _objectToTest = value; }
    }
    /// <summary>
    /// Ok this is a bad example of a Unit Test. Too much tested at once.
    /// </summary>
    [Test]
    public virtual void BinaryFileTest()
    {
        string filePath = GetFileSavePath(typeof(T).ToString() +
            "BinaryFileTest.dat");
        if (Kelvin<T>.ToBinaryFile(_objectToTest, filePath))
        {
            T t = Kelvin<T>.FromBinaryFile(filePath);
            Assert.AreEqual(_objectToTest, t);
        }
        else
        {
            Assert.Fail("Could not save file to " + filePath);
        }
    }
}

then rely on NUnit's ability to recognise it's [Test*] attributes via inheritance, then simply declare any number of concrete-type tests like this:

C#
public class CatalogFixture : KelvinFixture<UnitTests.Catalog>
{
    public CatalogFixture()
    {
        ObjectUnderTest = Catalog.GetTestInstance();
    }
}

Unfortunately, this is not currently possible (NUnit 2.4 beta returns an error "UnitTests.KelvinFixture`1 : System.MemberAccessException : Cannot create an instance of UnitTests.KelvinFixture`1[T] because Type.ContainsGenericParameters is true."). Until there is a more 'elegant' solution (probably requiring explicit NUnit support for Generic Tests), a slightly lower-tech approach is required - the Visual Studio solution in the download (64Kb) is shown below:

Visual Studio 2005 Solution Layout

ConceptDevelopment project
Kelvin.cs contains the class to be tested

UnitTests project
References: ConceptDevelopment
References: nunit.core, nunit.framework

  • KelvinFixtureT.cs contains a pseudo-Generic-TestFixture as described above
  • <Type>Fixture.cs each contain a concrete implementation of KelvinFixture<T> for a specific type, where the ObjectUnderTest is set in the constructor
  • Catalog.cs is a sample 'custom' Searcharoo3 class to test with
  • SpecialCases.cs has special cases (such as verifying the Exception thrown when T:MailMessage)

And because NUnit doesn't appear to handle Generic types very neatly, our [TestFixture] classes (which implement KelvinFixture<T>) must also implement every pseudo-Test method, just to add the [Test] attribute. The class diagram below shows test classes for arrays of ints and strings (Int32[], String[]).

NUnit Test Fixture Classes

Note how each concrete class implements ALL the methods from KelvinFixture<T>? Each of these overrides has exactly the same content, and rely on the constructor supplying an actual instance of the type to use in the tests. When adding new tests to the Generic class, you must remember to add implementations in each concrete test (each concrete test is identical line-for-line except for the class declaration and constructor, so you may find yourself copy-pasting a lot as you add when new tests, or new types to test).

CLICK TO ENLARGE: NUnit Test Results

C#
[Test]
public override
    void BinaryFileTest()
{
    base.BinaryFileTest();
}

Happily, both concrete instances pass our first test; and it was relatively easy to extend Kelvin to serialize to/from a number of different formats (Byte[], String, XmlDocument, Binary File) and add new tests at the same time (click on the image to see all the passing tests).

There's something else fishy about these tests, although each [TestFixture] consists of 11 [Test]s, we're actually only testing ONE object (a 5-element Int32 Array and 6 element String Array). Not exactly covering boundary cases! Thankfully once we have a concrete-typed Kelvin class, any number of additional subclasses of the "concrete-typed" [Test] can be created simply by overriding the constructor. The class diagram below shows additional tests with empty arrays and 32,000 element arrays. You might want to add more tests for subclasses of the T type, with types that can be implicitly converted to T, or any other cases you can think of!

Additional Test Fixtures

Testing against custom classes

Now that we have a suite of tests that run against Int32[] and String[], it's time to go back and write a proper TestFixture for Kelvin<Catalog>. As with the other types, I implemented the Generic class and supplied an instance of Catalog in the constructor... but not all the tests passed!

If you look closely at the diagram below, you will be able to work out which tests failed (because I've removed them from the CatalogFixture). Under normal circumstances, a Failed Test indicates a problem with your code, but in this case they merely exposed a known weakness in the Searcharoo.Net.Catalog class: it doesn't support XML deserialization! If you're interested in "why", you should be able to figure it out just by looking at the internals of the Catalog/Word/File classes and what they expose via properties... very difficult to reconstitute the object graph that way! Luckily binary serialization works in both directions (as our earlier tests showed), and for Searcharoo's production purpose that's all that is required.

Recall the discussion earlier about the where clause, and why we didn't use it to restrict Kelvin to ISerializable types. Here's another example of why that can give you a false sense of security: Catalog "does" implement ISerializable, it just does a poor job which causes our otherwise 'correct' Kelvin code to fail. When writing Generic classes, always take care with the assumptions you make about the Types that could implement it... use where if possible, but otherwise code defensively and try to give implementors as much assistance as possible to handle errors.

NUnit Test Fixture Classes

How to start NUnit from Visual Studio 2005

If you haven't used NUnit before, but have downloaded this project and want to give it a try, right-click on UnitTests-Properties to set the Start Action to NUnit, and the argument to the NUnit config file within the project.

CLICK TO ENLARGE: Settings to trigger NUnit tests

How to check test coverage with NCover

NCover Code Coverage Results

You'll also notice the NUnit_NCover.cmd file in the project - NCover operates in conjunction with NUnit to report on which lines of code were executed while the tests were run. The CMD file is shown below (go to the NCover website for more info).

plain
@echo off
C:\DevTools2\NCover-1.5\NCover.Console.exe
  "C:\DevTools2\nunit-2.4.b1\bin\nunit-console.exe"
  "C:\Inetpub\Kelvin\UnitTests\KelvinUnitTests.nunit"
  //a "ConceptDevelopment"
  //w "C:\DevTools2\nunit-2.4.b1\bin"
  //l "C:\Inetpub\Kelvin\NCover.log.txt"
  //x "C:\Inetpub\Kelvin\NCover.output.xml"
start C:\DevTools2\NCoverExplorer\NCoverExplorer.exe
  C:\Inetpub\Kelvin\NCover.output.xml

Important: the above CMD file has had line-breaks added for readability. Note the directory locations of the programs and source files.

Used in conjunction with NCoverExplorer, you get output like that shown to the right, and the ability to view every line of code in your application, whether it was executed during the tests, and if so, how many times! Unfortunately NCover occasionally exhibits unusual behaviour (or else I'm still not using it right!)... the sub-100% items in the KelvinUnitTests should have higher coverage (by my calculations) as some of the lines it shows as 'unvisited' ARE definitely hit when I step through in the debugger. If I figure that out I'll update this para.

But what is GenericBinder for?

For history on the problem, once again visit Searcharoo3 and search down for "Loading the Catalog from Disk".

Basically, when you do Binary serialization, the "Type Information" is 'embedded' in the serialized stream so that when you come to DE-serialize it, the Framework can quickly and easily find the target type to instantiate and fill with data. The "Type Information" includes the source assembly, in the form App_Code.vj-e_8q4, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null; and you'll notice the "vj-e_8q4" string appears to be a random jumble of characters... because it is! Between two AppDomain lifecycles, any class defined in an ASP.NET Web Application (code-inline in v1.x, or in App_Code in 2.0) is 'hardcoded' as an instance of a randomly-named assembly, so that if you try to DE-serialize after the Server Application has been restarted (or other event has caused recompilation), the type cannot be found and DE-serialization fails!. D'oh!

Thankfully, the Binary Deserialization code in the framework allows you to get around that by supplying a Custom Formatter - this is done by inheriting from SerializationBinder and telling it which types to use. Searcharoo3 does just that, but you'll notice the types are hardcoded in the CatalogBinder method... hardly a viable solution for a Generic class!

The GenericBinder class in Kelvin<T> does these basic steps:

  1. the BindToType method gets parameters assemblyName and typeName
    1. assemblyName is basically useless, as it's probably some randomly generated rubbish (recall that code using GenericBinder is only called in a catch block, so if the assemblyName had been valid, we probably wouldn't be here!)
    2. typeName is fully qualified by namespace, such as System.Int32 or Searcharoo.Net.Catalog
  2. strip off the namespace from the typeName, so we're left with Catalog. Note the assumptions behind this: firstly that the classname will be "unique" in the assembly; and secondly that the class "may" exist in a different namespace. In our example, we have binary data that was "serialized" from Searcharoo.Net.Catalog but will be deserialized into UnitTests.Catalog.
  3. try to load the assembly where Kelvin's Generic type is defined, using GetAssembly(typeof(T)). This is a hack/guess, but the best place to start looking.
  4. try a.GetType(assembly + "." + className); to see if the 'unknown' type can be found, and if so use it
  5. if it works, we can use that type! if not, an Exception is thrown

The GenericBinder will NOT work in all cases - but it does a pretty good job in the App_Code situation. Notice that Kelvin<T> provides an overload for FromBinary() that allows consumers to provide their own Binder implementation as required:

C#
public static T FromBinary(Byte[] frozen, 
    System.Runtime.Serialization.SerializationBinder customBinder)

There's probably a lot more you could do with the GenericBinder class, including a wider search across all the assemblies you can find in the AppDomain (with and without the fully qualified class namespace). I'll leave that as an exercise for the reader - and don't forget to write unit tests too!

Conclusion

That's quite a few different topics to cover in about 7 printed pages... hopefully it made some sense.

For further reading, you could try this Generic Range Class (and pattern) or these two CodeProject articles Generic Tree in C# and Generics Explained.

You might also find updated code on my website ConceptDevelopment.net, and in case you missed it, you might also be interested in reading about the free ASP.NET search engine: Searcharoo.

History

  • 2006-07-10: posted on CodeProject

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
Australia Australia
-- ooo ---
www.conceptdevelopment.net
conceptdev.blogspot.com
www.searcharoo.net
www.recipenow.net
www.racereplay.net
www.silverlightearth.com

Comments and Discussions

 
GeneralA couple of things Pin
Kent Boogaart18-Jul-06 0:24
Kent Boogaart18-Jul-06 0:24 
GeneralWas this a problem with the download... Pin
craigd18-Jul-06 1:45
craigd18-Jul-06 1:45 
GeneralRe: Was this a problem with the download... Pin
Kent Boogaart18-Jul-06 12:55
Kent Boogaart18-Jul-06 12:55 
GeneralThanks! &lt;embarrassed /&gt; Pin
craigd18-Jul-06 13:51
craigd18-Jul-06 13:51 
GeneralRe: Thanks! &lt;embarrassed /&gt; Pin
Charlie Poole2-Sep-06 6:32
Charlie Poole2-Sep-06 6:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.