About Links Archives Search Feed Albums of Note

TDD, XSD and CodeGen : Part 2, Converting Complex Types and Attributes

In the last episode, we started building our alternate to xsd.exe and wrote a set of five tests that made sure the converter worked against the smallest possible schema. We also learnt about building basic schemas programmatically. Next up, we’ll look at some more basic pieces of a schema and start working in the CodeNamespace area.

Let’s start with that. Xsd.exe (that comes with .NET v2.0) generates many more attributes for each class  enum etc than did the version in .NET 1.x. Indeed, we now get

[System.CodeDom.Compiler.GeneratedCodeAttribute(“xsd”, “2.0.50727.42”)] [System.SerializableAttribute()] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute(“code”)] [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)] [System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)] public partial class root { }

Rather than .net 1.x’s

[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)] [System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)] public class root { }

In this second set of tests, we need to know how to check each of the values in the XmlTypeAttribute and XmlRootAttribute so now would be a good time to see how to access them in a codenamespace object.

Our sixth test is to check that adding a value to the targetNamespace property of the XmlSchema object alters the XmlRootAttributeAttribute to appear as [System.Xml.Serialization.XmlRootAttribute(Namespace=“targetNamespace”)].

[Test] public void Convert_SpecifySchemaTargetNamespace_RootIsNotAnonymousType() { //Generate complex type XmlSchema schema = CreateSchemaWithComplexRootType(); schema.TargetNamespace = “http://hmobius.com/testns”; //Output schema generated to Console.Out WriteSchemaToOutputWindow(schema); //Convert it CodeNamespace ns = sc.Convert(schema); //Grab the type declaration representing the root element CodeTypeDeclaration type = ns.Types[0]; }

Note that I’ve written half the test here so that I can set a breakpoint on the last line of code and look into the CodeTypeDeclaration object and find where to look for the XmlRootAttributeAttribute and its Namespace property. Here they are

So the XmlRootAttributeAttribute can be found in type.CustomAttributes and the Namespace property is in the type.CustomAttributes[x].Arguments collection. Looking again at the original class gen’d from the blank schema

[System.CodeDom.Compiler.GeneratedCodeAttribute(“xsd”, “2.0.50727.42”)] [System.SerializableAttribute()] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute(“code”)] [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)] [System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)] public partial class root { }

We’ll have to assume that it won’t always be in this position though, so we’ll have to search through the collection. One thing we should also check for though is that two of the same attributes haven’t been generated. Something really would be wrong at that point.

/// /// Test 6. Adding a value to schema. /// targetnamespace should alter Namespace attribute for the /// XmlRootAttributeAttribute attached to the class that /// the schema is converted into /// [Test] public void Convert_SpecifySchemaTargetNamespace_RootIsNotAnonymousType() { //Generate complex type XmlSchema schema = CreateSchemaWithComplexRootType(); schema.TargetNamespace = “http://hmobius.com/testns”; //Output schema generated to Console.Out WriteSchemaToOutputWindow(schema); //Convert it CodeNamespace ns = sc.Convert(schema); //Get XmlRootAttributeAttribute CodeAttributeDeclaration xmlRootAttribute = GetCodeAttributeDeclaration(type, “System.Xml.Serialization.XmlRootAttribute”); //Get Namespace Argument from the XmlRootAttribute CodeAttributeArgument nsArgument = GetCodeAttributeArgument(xmlRootAttribute, “Namespace”); CodePrimitiveExpression cpe = nsArgument.Value as CodePrimitiveExpression; Assert.AreEqual(“http://hmobius.com/testns”, cpe.Value.ToString()); } /// /// Gets the named code attribute argument from a CodeAttributeDeclaration. /// /// The CodeAttributeDeclaration to be searched. /// The Name of the argument to be found. /// The argument object private CodeAttributeArgument GetCodeAttributeArgument( CodeAttributeDeclaration attribute, string argumentName) { for (int i = 0; i < attribute.Arguments.Count; i++) { if (attribute.Arguments[i].Name == argumentName) { return attribute.Arguments[i]; } } throw new Exception(“Couldn’t find specified Attribute Argument”); } /// /// Gets the named code attribute declaration from a CodeTypeDeclaration. /// /// The CodeTypeDeclaration /// The name of the attribute to look for /// The target attribute declaration private CodeAttributeDeclaration GetCodeAttributeDeclaration( CodeTypeDeclaration type, string name) { for (int i = 0; i < type.CustomAttributes.Count; i++) { if (type.CustomAttributes[i].Name == name) { return type.CustomAttributes[i]; } } throw new Exception(“Couldn’t find specified Attribute Declaration”); }

For ease, I’ve already separated out the routines to get first the correct attribute and then the argument on that attribute as we’ll be using them again.

<xs:element name=“root”?> <xs:complexType?> <xs:sequence?> <xs:element name=“StringElement” type=“xs:string”?> <xs:element name=“dateElement” type=“xs:dateTime”?> </xs:sequence?> </xs:complexType?> </xs:element?> </xs:schema?>

Running this through xsd.exe will get us the following

using System.Xml.Serialization; // // This source code was auto-generated by xsd, Version=2.0.50727.42. // /// [System.CodeDom.Compiler.GeneratedCodeAttribute(“xsd”, “2.0.50727.42”)] [System.SerializableAttribute()] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute(“code”)] [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)] [System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)] public partial class root { private string stringElementField; private System.DateTime dateElementField; /// [System.Xml.Serialization.XmlElementAttribute( Form=System.Xml.Schema.XmlSchemaForm.Unqualified)] public string StringElement { get { return this.stringElementField; } set { this.stringElementField = value; } } /// [System.Xml.Serialization.XmlElementAttribute( Form=System.Xml.Schema.XmlSchemaForm.Unqualified)] public System.DateTime dateElement { get { return this.dateElementField; } set { this.dateElementField = value; } } }

The default for xsd.exe is to create a field and getproperty for each element in a sequence as shown above, so we need to test for several things against each element we’ve added to the sequence. (Note how the code is case-specific to the names of the elements in the schema. One of our future customizations will be to alter that to useful naming conventions.

Again we can apply our process of building the schema in memory, checking the schema generated is correct, running that through xsd.exe and working from there.

All members of a class - methods, fields, properties, events - can be found in the Member collection of a CodeTypeDeclaration. They are not sorted however, so you’ll need to loop through this collection to find out if a member you think exists actually does. For example,

Once you have identified what kind of member an object is, you’ll need to cast it to query it further. For example, a CodeMemberField object looks like this.

As you can see, I’ve highlighted the three particular properties that we’re interested in testing here. The corresponding CodeMemberProperty object has a similar set of properties.

Again, I’ve highlighted the useful ones to test. Worth noting here in particular are that we can also test the structure of the get and set statements for the property. We can do this by checking into the GetStatements and SetStatements collections of the CodeMemberProperty object.

So we can test for the fieldName being got for the property and being set as well. We could also look into the expression doing the setting as well by investigating the Right property of the SetStatement but it should just be value so Right will be of type CodePropertySetValueRefrenceExpression

Let’s translate this into code. We’ll split out the main test a bit for ease of use. The main method builds the schema we’ll use and then calls the assertions elsewhere.

/// /// Test 7. Builds a simple sequence of primitive elements /// for the complex root type /// Checks that a field  property combination is added /// for each element in the sequence /// [Test] public void Convert_SequenceOf2PrimitiveTypes_Returns2FieldPropertyCombinations() { string stringElementName = “StringElement”; string dateElementName = “dateElement”; //Create two simple elements XmlSchemaElement stringElement = new XmlSchemaElement(); stringElement.Name = stringElementName; stringElement.SchemaTypeName = new XmlQualifiedName(“string”, “http://www.w3.org/2001/XMLSchema”); XmlSchemaElement dateElement = new XmlSchemaElement(); dateElement.Name = dateElementName; dateElement.SchemaTypeName = new XmlQualifiedName(“dateTime”, “http://www.w3.org/2001/XMLSchema”); //Create sequence of string and date elements XmlSchemaSequence sequence = new XmlSchemaSequence(); sequence.Items.Add(stringElement); sequence.Items.Add(dateElement); //Create schema with complex root element //and insert sequence into complex type XmlSchema schema = CreateSchemaWithComplexRootType(); XmlSchemaElement root = schema.Items[0] as XmlSchemaElement; XmlSchemaComplexType rootType = root.SchemaType as XmlSchemaComplexType; rootType.Particle = sequence; //Output schema generated to Console.Out WriteSchemaToOutputWindow(schema); //Convert it CodeNamespace ns = sc.Convert(schema); //Get root type CodeTypeDeclaration rootClass = ns.Types[0]; AssertFieldExists(rootClass.Members, MemberAttributes.Private, “System.String”, DefaultGeneratedFieldName(stringElementName)); AssertFieldExists(rootClass.Members, MemberAttributes.Private, “System.DateTime”, DefaultGeneratedFieldName(dateElementName)); AssertPropertyExists(rootClass.Members, stringElementName, “System.String”, true, true, DefaultGeneratedFieldName(stringElementName)); AssertPropertyExists(rootClass.Members, dateElementName, “System.DateTime”, true, true, DefaultGeneratedFieldName(dateElementName)); }

One key thing to note here. Xsd.exe and thus our code will literally copy the name of the sequence element into a property and then auto-generate the fieldname by lower casing the first letter of the element and adding ‘Field’. So we need to add this.

private string DefaultGeneratedFieldName(string propertyName) { string firstLetter = propertyName.Trim().Substring(0,1).ToLower(); return firstLetter + propertyName.Trim().Substring(1) + “Field”; }

And we test for fields like this

/// /// Asserts the field exists with the given type and access level. /// /// The code type member collection. /// Access level /// Expected type of the field. /// Name of the field. private void AssertFieldExists(CodeTypeMemberCollection members, MemberAttributes accessor, string ExpectedFieldType, string fieldName) { bool fieldFound = false; int numberOfFieldsFound = 0; for (int i = 0; i < members.Count; i++) { //See if the member has the right name if (members[i].Name == fieldName) { CodeMemberField thisField = members[i] as CodeMemberField; Assert.IsInstanceOfType(typeof(CodeMemberField), thisField, “This should be a field but isn’t”); //Check field has correct accessor property. Assert.AreEqual(thisField.Attributes, accessor, “This field doesn’t have the expected access level”); //Check field is the correct type Assert.AreEqual(thisField.Type.BaseType, ExpectedFieldType, “This field isn’t of the expected type”); fieldFound = true; numberOfFieldsFound += 1; } } Assert.IsTrue(fieldFound, “Could not find expected field in generated class”); Assert.AreEqual(numberOfFieldsFound, 1, “There should only be one field of this name being generated”); }

And for properties like this. Assuming that the property get and set statements are simple ones.

private void AssertPropertyExists(CodeTypeMemberCollection members, string expectedPropertyName, string expectedPropertyType, bool hasGetter, bool hasSetter, string expectedFieldName) { bool propertyFound = false; int numberOfPropertiesFound = 0; for (int i = 0; i < members.Count; i++) { if (members[i].Name == expectedPropertyName) { CodeMemberProperty thisProperty = members[i] as CodeMemberProperty; Assert.IsInstanceOfType(typeof(CodeMemberProperty), thisProperty, “This should be a property but isn’t”); //Check field has expected base type Assert.AreEqual(thisProperty.Type.BaseType, expectedPropertyType, “This property isn’t of the expected type”); //Check GetStatement if there is supposed to be one. //Assumes it looks like “return fieldname” if (hasGetter) { Assert.IsTrue(thisProperty.HasGet, “Expecting a Get Statement but there isn’t one”); //A simple get statement just returns the field CodeMethodReturnStatement thisGetter = thisProperty.GetStatements[0] as CodeMethodReturnStatement; //But you need to get the Reference to the field from the return //statement to see which field is being returned. CodeFieldReferenceExpression fieldGotten = thisGetter.Expression as CodeFieldReferenceExpression; Assert.AreEqual(fieldGotten.FieldName, expectedFieldName, “Property not getting the expected field name”); } else { Assert.IsFalse(thisProperty.HasGet, “Not expecting a get statement but there is one”); } //Check SetStatement if there is supposed to be one. //Assumes it looks like “fieldname = value”; if (hasSetter) { Assert.IsTrue(thisProperty.HasSet, “Expecting a Set statement but there isn’t one”); //A set statement assigns values, //so we need to find a CodeAssignStatement CodeAssignStatement thisSetter = thisProperty.SetStatements[0] as CodeAssignStatement; //Get left side of statement. Should be a reference to the private field CodeFieldReferenceExpression fieldBeingSet = thisSetter.Left as CodeFieldReferenceExpression; Assert.AreEqual(expectedFieldName, fieldBeingSet.FieldName, “Field being set by property not the expected one”); //Get right side of statement. Should be a reference to ‘value’ //which is a special SetValueReferenceExpression CodePropertySetValueReferenceExpression value = thisSetter.Right as CodePropertySetValueReferenceExpression; Assert.IsNotNull(value, “Was expecting a simple field = value set statement but no value here”); } else { Assert.IsFalse(thisProperty.HasSet, “Not expecting a set statement but there is one”); } propertyFound = true; numberOfPropertiesFound += 1; } } Assert.IsTrue(propertyFound, “Expected property was not found”); Assert.AreEqual(1, numberOfPropertiesFound, “Expected only one property of this name but found more than one”); }

At this point we could run a quick test to see what happens if you specify schema.ElementFormDefault = XmlSchemaForm.Qualified; but I’ll leave that out here. What we do need to look at are attributes. An element can have an attribute that is a simple or primitive type. This translates from e.g.


public partial class root { private string stringAttributeField; /// [System.Xml.Serialization.XmlAttributeAttribute()] public string StringAttribute { get { return this.stringAttributeField; } set { this.stringAttributeField = value; } } }

As you can see, we get another property field combination but with the addition that the public property has an XmlAttribute attribute appended on top of it. We already have tests for the existence of fields, properties and customattributes so this shouldn’t be too hard.

[Test] public void Convert_AddAttributeToElement_ReturnTaggedPropertyFieldComboForAttribute() { string attributeName = “StringAttribute”; //Create an attribute XmlSchemaAttribute attr = new XmlSchemaAttribute(); attr.Name = attributeName; attr.Use = XmlSchemaUse.Optional; attr.SchemaTypeName = new XmlQualifiedName(“string”, “http://www.w3.org/2001/XMLSchema”); //Create schema with complex root element and //insert attribute into complex type XmlSchema schema = CreateSchemaWithComplexRootType(); XmlSchemaElement root = schema.Items[0] as XmlSchemaElement; XmlSchemaComplexType rootType = root.SchemaType as XmlSchemaComplexType; rootType.Attributes.Add(attr); //Output schema generated to Console.Out WriteSchemaToOutputWindow(schema); //Convert it CodeNamespace ns = sc.Convert(schema); //Get root type CodeTypeDeclaration rootClass = ns.Types[0]; //Attributes should return a simple field of the same type //as the XMl attribute and a tagged property AssertFieldExists(rootClass.Members, MemberAttributes.Private, “System.String”, DefaultGeneratedFieldName(attributeName)); AssertTaggedPropertyExists(rootClass.Members, attributeName, “System.String”, true, true, DefaultGeneratedFieldName(attributeName), “System.Xml.Serialization.XmlAttributeAttribute”, null, null); }

We’ve refactored a few things here now from earlier tests so our CodeNamespace asserts are now as follows.

Each of these runs basically the same structure. Loop through the supplied collection, look for the name and if found check for the properties required.

private void AssertArgumentExists( CodeAttributeArgumentCollection arguments, string AttributeArgumentToCheck, string ExpectedArgumentValue) { bool argumentFound = false; int numberOfArgumentsFound = 0; //Iterate through arguments in collection for (int i = 0; i < arguments.Count; i++) { if (arguments[i].Name == AttributeArgumentToCheck) { //Argument exists, so check value CodePrimitiveExpression value = arguments[i].Value as CodePrimitiveExpression; Assert.AreEqual(ExpectedArgumentValue, value.Value.ToString(), “Expected argument value is not found”); argumentFound = true; numberOfArgumentsFound += 1; } } Assert.IsTrue(argumentFound, “Couldn’t find specified argument”); Assert.AreEqual(1, numberOfArgumentsFound, “Only one argument of the given name should exist”); }

To finish off for this episode, I wrote one more test to look at how choice complex types are dealt with and verified with another test that this is how the SchemaConverter also works. Then some more refactoring and this zip file contains how the code looks so far [/file.axd?file=xsdgenerator0.1.zip]. 743 lines of test code to 91 lines of program, but the ratio should improve now and we have started building a simple framework to build schemas and inspect pieces of the CodeNamespace being generated.

Posted on September 5, 2006   #Geek Stuff  

← Next post    ·    Previous post →