So let's lay out what we want our application to do.

  • Convert any number of given, connected schema files to a set of C# classes
  • Allow us to tweak aspects of the generated classes to suit our needs elsewhere - for example, change field, property and class names to our own naming conventions or the way something is serialized.
  • Save the generated classes into a given file.
  • Present some sort of UI that allows us to specify what tweaks should be made.
  • Save those settings for reuse later on.

With that in mind, let's bear in mind the mantra of 'code little, think, code a little more' and write some tests. The heart of our app is a two stage process.

  1. Convert schema to namespace
  2. Tweak namespace

So let's start on stage one and write some tests.

We'll assume we use a public class that will convert our schema into a class library so we test the framework to make sure we can create the class.

[Test]
public void CreateSchemaConverter()
{
  SchemaConverter sc = new SchemaConverter();
  Assert.IsNotNull(sc);
}

And with it, the code to make it run.

public class SchemaConverter 
{ 
}

One test written, one test passed. Good start. For reference, I'm using VS2005, and nUnit here with the app in one project and the tests in another project referencing the app. This looks to be the best way to able to test your code and then not include it in your release code.

Now the most basic schema we can try and convert is an empty schema. Sending an empty schema to xsd.exe such as

<?xml version="1.0" encoding="Windows-1252" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
</xs:schema>

Returns the error : Cannot generate any classes because no top-level elements with complex type were found. So we should do the same and expect our converter to return the same XmlSchemaException error that xsd.exe does.

///  
/// Test 2. Converting an empty schema should return an empty namespace. 
///  
[Test] 
[ExpectedException(typeof(XmlSchemaException), 
"Cannot generate any classes because no top-level elements with complex type were found")] 
public void Convert_EmptySchema_ReturnXmlSchemaException() 
{ 
    //Create schema and add root to it 
    XmlSchema schema = new XmlSchema(); 
    //Output schema generated to Console.Out 
    schema.Write(Console.Out); 
    Console.WriteLine(); 
    SchemaConverter sc = new SchemaConverter();
    CodeNamespace ns = sc.Convert(schema); 
} 

Rather than saving a schema file for each test, I'm going to create them programmatically and display them in the nUnit output window to verify the schema built is the one we think we've built. I've used this MSDN page as a reference to create the schemas and expect to get a nice schema builder class library out of this process as a side product of the TDD process which I can use elsewhere. I've called the main conversion method Convert and use VS2005 to generate the stub for Convert() for us. All that's required of it so far is to throw an exception, so Convert begins its life as

public CodeNamespace Convert(XmlSchema xmlSchema)
{
    throw new XmlSchemaException(
    "Cannot generate any classes because no top-level elements with complex type were found");
}

Compile and run. Both tests pass. Time for a thought before coding a little more. I can already see that both tests use common code to create the SchemaConverter class, so I can refactor that into a test setup method which will run every time before a test is run.

private SchemaConverter sc;
[SetUp]
public void SetUp()
{
    sc = new SchemaConverter();
}

Removing this refactored line from both tests and running them again, they still pass, so it's time for test three. The next smallest (and, incidentally, malformed) schema is one which defines a root element that is a primitive type such as a string. For example

<?xml version="1.0" encoding="Windows-1252"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> 
    <xs:element type="xs:string" name="root">
    </xs:element> 
</xs:schema>

Now if you pass this to xsd.exe, you'll get the same error as in test 2, so this should be quite simple. This test should expect the same results as test 2.

/// 
/// Test 3. 
/// Schema contains root element which is an empty complex type
/// 
[Test]
[ExpectedException(typeof(XmlSchemaException), 
"Cannot generate any classes because no top-level 
elements with complex type were found")]
public void Convert_RootIsString_ReturnXmlSchemaException()
{
    //Create root element as a string
    XmlSchemaElement rootElement = new XmlSchemaElement();
    rootElement.Name = "root";
    rootElement.SchemaTypeName = 
    new XmlQualifiedName("string", "http://www.w3.org/2001/XMLSchema");
    //Create schema and add root to it
    XmlSchema schema = new XmlSchema();
    schema.Items.Add(rootElement);
    //Output schema generated to Console.Out
    schema.Write(Console.Out);
    Console.WriteLine();
    //Convert it
    CodeNamespace ns = sc.Convert(schema);
}

Run it and all three tests pass first time. So why did we write this test? To make sure that when we check whether or not to return the exception, we check for the right things rather than whether or not the schema is just empty.

A bit more refactoring too. We can pull out the code to display the schema in the output window to a separate method.

private void WriteSchemaToOutputWindow(XmlSchema schema)
{
//Output schema generated to Console.Out
schema.Write(Console.Out);
Console.WriteLine();
}

Next test - a schema defining a root which is a simple type.

<?xml version="1.0" encoding="Windows-1252"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="root">
<xs:simpletype name="rootType"></xs:simpletype>
</xs:element>
</xs:schema>

Again, this should return the same Exception as before.

[Test]
[ExpectedException(typeof(XmlSchemaException),
"Cannot generate any classes because no top-level 
elements with complex type were found")]
public void Convert_RootIsSimpleType_ReturnXmlSchemaException()
{
//Create Simple Type
XmlSchemaSimpleType rootSimpleType = 
new XmlSchemaSimpleType();
rootSimpleType.Name = "rootType";
//Create Root Element
XmlSchemaElement rootElement = new XmlSchemaElement();
rootElement.Name = "root";
rootElement.SchemaType = rootSimpleType;
//Create Empty Schema and add root element to it
XmlSchema schema = new XmlSchema();
schema.Items.Add(rootElement);
//Output schema generated to Console.Out
WriteSchemaToOutputWindow(schema);
//Convert it
CodeNamespace ns = sc.Convert(schema);
}

Compile and run. Four tests passed. Hurrah. Our fifth test will finally necessitate a bit more code in the actual converter. A schema which contains a complex type as the root element. Need to keep it simple though, so let's start with an empty complex type. This should produce an empty public partial class with the same name as the root element in the schema.

[Test]
public void Convert_RootIsEmptyComplexType_ReturnsSingleEmptyClass()
{
//Generate complex type
XmlSchemaComplexType rootType = new XmlSchemaComplexType();
//Create root element
XmlSchemaElement rootElement = new XmlSchemaElement();
rootElement.Name = "root";
rootElement.SchemaType = rootType;
//Create Empty Schema and add root element to it
XmlSchema schema = new XmlSchema();
schema.Items.Add(rootElement);
//Output schema generated to Console.Out
WriteSchemaToOutputWindow(schema);
//Convert it
CodeNamespace ns = sc.Convert(schema);
Assert.AreEqual(1, ns.Types.Count);
Assert.AreEqual("root", ns.Types[0].Name, 
"The root of the namespace should be named 
the same as the root of the schema");
}

Running this test straight away of course produces a fail. So let's fill things in a bit using Dan Cazzulino's code as a base and see if it still holds up in .NET 2.0.

public CodeNamespace Convert(XmlSchema xmlSchema)
{
//Check top level element of the schema is a 
//Complex Type Element else throw an error
if (xmlSchema.Items[0].GetType() 
== typeof(XmlSchemaComplexType))
{
//Add schema into collection
XmlSchemas schemas = new XmlSchemas();
schemas.Add(xmlSchema);
//Create importer for the schema
XmlSchemaImporter importer = 
new XmlSchemaImporter(schemas);
// Create new System.CodeDom namespace for 
// the XmlCodeExporter to put the classes in
CodeNamespace ns = new CodeNamespace();
XmlCodeExporter exporter = new XmlCodeExporter(ns);
// Iterate through top-level (global, not local) elements in the 
// schemas and export code for each into the namespace
foreach (XmlSchema schema in schemas)
{
foreach (XmlSchemaElement element in schema.Elements.Values)
{
// Import the mapping from XML to .NET code and 
// export it to the code namespace
XmlTypeMapping mapping = 
importer.ImportTypeMapping(element.QualifiedName);
exporter.ExportTypeMapping(mapping);
}
}
return ns;
}
else
{
throw new XmlSchemaException
("Cannot generate any classes because no top-level 
elements with complex type were found");
}
}

However, two tests fail. Our complex type test and the one where we send an empty schema. Our schema check can't find xmlSchema.Items[0] so it errors out. We can change the check in our Convert method to correct this.

if ((xmlSchema.Items.Count > 1) && 
(xmlSchema.Items[0].GetType() == typeof(XmlSchemaComplexType)))
{ ... }

Our latest test still fails though, so let's use TestDriven.NET's test with debug feature (Image 1) and set a watchpoint at the check to see where we're going wrong (Image 2)

testwithdebugging_2

debugging_2

Ah-ha. xmlSchema.Items[0].GetType() returns System.Xml.Schema.XmlSchemaObject and not XmlSchemaComplexType as guessed. However, looking down the lines, we can see where the ComplexType does appear.

whereisthecomplextype_2

So we need to cast the first Item in the schema to an XmlSchemaElement type and then check its schematype property. Let's pull these checks into a simple method that returns a boolean.

private bool SchemaIsValidForConversion(XmlSchema schema)
{ 
//Check for empty schema
if (schema.Items.Count == 0)
{
return false;
}
//Check for root element as not complex type
XmlSchemaElement element = 
schema.Items[0] as XmlSchemaElement;
if (element.SchemaType.GetType() 
!= typeof(XmlSchemaComplexType))
{
return false;
}
return true;
}

Running all five tests again, now the root as a string test fails because element.SchemaType returns null in this instance. A little tweak for the second check in this new method is required.

// SchemaType can return null is element is a primitive type 
// so check for null and not a complex type
if ((element.SchemaType == null) || 
(element.SchemaType.GetType() != typeof(XmlSchemaComplexType)))
{
return false;
}

Finally, all five tests test green. I can do a little bit of refactoring here to pull out some literals into constant values as well and I'm done for now.

In this first instalment, we've written five tests that establish a basic conversion from schema to code namespace. In the second part, we'll start building up a schema builder class and a code namespace inspection class as we write more tests to establish how a schema is represented as a class.