Menu

The .NET Schema Object Model

December 4, 2002

Priya Lakshminarayanan

Despite the many articles explaining W3C XML Schema (WXS), it's not enough to discuss WXS as a specification only. Educational materials should also discuss tools which aid the development of XML applications which employ WXS. This article focuses on an API in the .NET platform, the XML Schema Object Model (SOM). SOM is a rich API which allows developers to create, edit, and validate schemas programmatically -- one of the few such tools available so far.

SOM operates on schema documents analogously to the way DOM operates on XML documents. Schema documents are valid XML files that, once loaded into the SOM, convey meaning about the structure and validity of other XML documents which conform to the schema. SOM is indispensable for a certain class of application, like a schema editor, where it needs to construct the schema in memory and check the schema's validity according to the WXS specification.

The SOM comprises an extensive set of classes corresponding to the elements in a schema. For example, the <xsd:schema> ... </xsd:schema> element maps to the XmlSchema class -- all schema information that can possibly be contained within those tags can be represented using the XmlSchema class.

Similarly <xsd:element> ... </xsd:element> maps to XmlSchemaElement, <xsd:attribute> ... </xsd:attribute> maps to XmlSchemaAttribute and so on. This mapping helps easy use of the API. For a complete listing of all the classes available in the System.Xml.Schema namespace, refer to the .NET Framework Class Library Reference.

Creating A Schema Using The SOM

The following is a simple customer schema, with a top-level Customer element that has two child elements, FirstName and LastName, and one attribute, CustID.

<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:tns="http://tempuri.org" 
           targetNamespace="http://tempuri.org"
           xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="Customer">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="FirstName" type="xs:string" />
        <xs:element name="LastName" type="tns:LastNameType" />
      </xs:sequence>
      <xs:attribute name="CustID" type="xs:positiveInteger"
                    use="required" />
    </xs:complexType>
  </xs:element>
  <xs:simpleType name="LastNameType">
    <xs:restriction base="xs:string">
      <xs:maxLength value="20"/>
    </xs:restriction>
  </xs:simpleType>
</xs:schema>

The following code creates the customer schema in memory using the SOM API. We will use a bottom-up approach in building the schema, constructing the child elements, attributes and their corresponding types first, and then proceed to build the top-level components.

// Create the FirstName and LastName elements
XmlSchemaElement firstNameElem = new XmlSchemaElement();
firstNameElem.Name = "FirstName";
XmlSchemaElement lastNameElem = new XmlSchemaElement();
lastNameElem.Name = "LastName";

// Create CustID attribute
XmlSchemaAttribute idAtt = new XmlSchemaAttribute();
idAtt.Name = "CustID";
idAtt.Use = XmlSchemaUse.Required; 
// The XmlSchemaUse enumeration has the values 
// Required/Optional/Prohibited/None.
// Default is XmlSchemaUse.Optional

Apart from the Name property, which corresponds to the "name" attribute of <xs:element> or <xs:attribute> in a schema, all other attributes allowed by the schema (defaultValue, fixedValue, form to name a few) also have corresponding properties in the XmlSchemaElement/XmlSchemaAttribute classes.

The content of elements and attributes is defined by their types. To create elements or attributes whose types are one of the built-in schema types as defined in XML Schema Part 2: Datatypes, the SchemaTypeName property on XmlSchemaElement or XmlSchemaAttribute is set with the corresponding qualified name of the type. In order to create a user-defined type for the element or attribute, a new simple or complex type is created using the XmlSchemaSimpleType or XmlSchemaComplexType class.

In the customer schema the FirstName element's type is the built-in type "string". The LastName element's type is a named simple type that is a restriction of the built-in type "string", with a maxLength facet value of 20. If we don't want to create named simple or complex types at the top-level, but want the types to be anonymous children of the element or attribute (only simple types apply for attributes), we must set the SchemaType property, with the unnamed simple or complex type, instead of the SchemaTypeName property.

WXS allows both anonymous and named simple types to be derived by restriction from other simple types (built-in or user-defined) or constructed as a list or union of other simple types. The XmlSchemaSimpleTypeRestriction class is used to create a simple type by restricting the built-in string type. We can use XmlSchemaSimpleTypeList or XmlSchemaSimpleTypeUnion classes to create list or union types. The Content property of XmlSchemaSimpleType denotes whether it is a simple type restriction, list, or union.

// Create the simple type for the LastName element
XmlSchemaSimpleType lastNameType = new XmlSchemaSimpleType();
lastNameType.Name = "LastNameType";
XmlSchemaSimpleTypeRestriction lastNameRestriction = new
XmlSchemaSimpleTypeRestriction();
lastNameRestriction.BaseTypeName = new
XmlQualifiedName("string","http://www.w3.org/2001/XMLSchema");
XmlSchemaMaxLengthFacet maxLength = new XmlSchemaMaxLengthFacet();
maxLength.Value = "20";
lastNameRestriction.Facets.Add(maxLength);
lastNameType.Content = lastNameRestriction;

// Associate the elements/attributes with their types
firstNameElem.SchemaTypeName = new XmlQualifiedName("string",
    "http://www.w3.org/2001/XMLSchema"); // Built-in type
lastNameElem.SchemaTypeName  = new XmlQualifiedName("LastNameType",
    "http://tempuri.org");               // User-defined type
idAtt.SchemaTypeName         = new XmlQualifiedName("positiveInteger",
    "http://www.w3.org/2001/XMLSchema"); // Built-in type

// Create the top-level Customer element
XmlSchemaElement custElem = new XmlSchemaElement();
custElem.Name = "Customer";

// Create an anonymous complex type for the Customer element
XmlSchemaComplexType customerType = new XmlSchemaComplexType();
XmlSchemaSequence seq = new XmlSchemaSequence();
seq.Items.Add(firstNameElem);
seq.Items.Add(lastNameElem);
customerType.Particle = seq;

We can also create XmlSchemaChoice or XmlSchemaAll as the particle of the complex type to replicate <xs:choice> or <xs:all> semantics. The sequence is the container to which we add the child elements (FirstName and LastName) for our customer schema.

// Add attribute CustID to the complex type
customerType.Attributes.Add(idAtt);

// Set the SchemaType of Customer element to the 
// anonymous complex type created above
custElem.SchemaType = customerType;

//Create an empty schema
XmlSchema custSchema = new XmlSchema();
custSchema.TargetNamespace = "http://tempuri.org";

// Add all top-level elements and types to the schema
// (In this case customer element custElem and the 
// simple type LastNameType)
custSchema.Items.Add(custElem);
custSchema.Items.Add(lastNameType);

Now we compile the schema and write it to stdout or to a file.

// Compile schema
custSchema.Compile(new ValidationEventHandler(ValidationCallbackOne));
custSchema.Write(Console.Out);

The Compile method of the XmlSchema object validates the schema document against the rules specified in WXS specification. Compilation also makes the post-schema-compilation properties available. All such properties on different SOM classes form the Post-Schema-Compilation Infoset.

The ValidationEventHandler, passed as a parameter to the Compile method, is a delegate that calls the callback function ValidationCallbackOne to handle validation errors and warnings. For more information about ValidationEventHandling, refer to Validation and the Schema object Model.

It would be more useful to write the schema out to a file using the XmlTextWriter. Replace the custSchema.Write with the call to the following method:

private void WriteCustomerSchema(XmlSchema custSchema) {
    FileStream file = new FileStream ("Customer.xsd", 
                                      FileMode.Create,
    FileAccess.ReadWrite);
    XmlTextWriter Writer = new XmlTextWriter (file, 
                                      new UTF8Encoding());
    Writer.Formatting = Formatting.Indented;
    custSchema.Write(Writer);
}

Post-Schema-Compilation Infoset (PSCI)

Once the schema is compiled, the SOM classes that correspond to every schema component expose the post-schema compilation infoset properties. Each class has a set of properties that can be set when they are created in the object model. Such properties are pre-schema compilation infoset properties. In the customer schema created above, the SchemaTypeName property, used to set the name of the simple or complex type of the element or attribute, is a property in the pre-schema-compilation infoset, and the ElementType or AttributeType property is the corresponding post-schema-compilation property.

For the customer schema the following table lists the corresponding post-schema-compilation properties:

XML Representation

SOM Object

Property

Type (Value)

Customer element

XmlSchemaElement

QualifiedName

XmlQualifiedName (tns:Customer)

ElementType

object (Anonymous XmlSchemaComplexType)

BlockResolved

XmlSchemaDerivationMethod (Empty)

FinalResolved

XmlSchemaDerivationMethod (Empty)

Anonymous Complex Type

XmlSchemaComplexType

QualifiedName

XmlQualifiedName (Empty)

BaseSchemaType

XmlSchemaComplexType (anyType)

DerivedBy

XmlSchemaDerivationMethod (Restriction)

Datatype

XmlSchemaDataytpe (Null)

ContentType

XmlSchemaContentType (ElementOnly)

FinalResolved

XmlSchemaDerivationMethod (Empty)

ContentTypeParticle

XmlSchemaSequence

AttributeUses

XmlSchemaObjectTable with one attribute

AttributeWildCard

XmlSchemaAnyAttribute (Null)

CustID attribute

XmlSchemaAttribute

QualifiedName

XmlQualifiedName (CustID)

AttributeType

XmlSchemaDatatype

(positiveInteger)

LastNameType

Simple type

XmlSchemaSimpleType

QualifiedName

XmlQualifiedName (tns:LastNameType)

BaseSchemaType

XmlSchemaDatatype

(string)

DerivedBy

XmlSchemaDerivationMethod (Restriction)

Datatype

XmlSchemaDatatype (string)

FinalResolved

XmlSchemaDerivationMethod (Empty)

Table 1

Manipulating the SOM

In order to manipulate the schema loaded into the SOM, we should be able to traverse the SOM and get at the elements, attributes, and types stored in it. The XmlSchema class has the following properties that provide access to the collection of all global items added to the schema.

Property Name

Item type stored in the collection/Array

Elements

XmlSchemaElement

Attributes

XmlSchemaAttribute

AttributeGroups

XmlSchemaAttributeGroup

Groups

XmlSchemaGroup

Includes

XmlSchemaExternal (Can be XmlSchemaInclude/XmlSchemaImport/XmlSchemaRedefine)

Items

XmlSchemaObject (Can access all global level items: elements/attributes/types added to this collection)

Notations

XmlSchemaNotation

SchemaTypes

XmlSchemaType (Can be XmlSchemaSimpleType/XmlSchemaComplexType)

UnhandledAttributes

XmlAttribute (Attributes that do not belong to the schema namespace)

The Items property is a pre-schema-compilation property that can be queried to look up Elements, Attributes, Types, Groups, and so on.

The UnhandledAttributes property provides access to all the non-schema-namespace attributes found in the schema. These attributes are not processed by the schema processor. All the other properties are post-schema-compilation properties and become available once the schema is compiled.

Traversal

Depending on what we need to manipulate in the schema, we have to traverse one or more of these collections. The following is a small code sample to traverse the Customer schema and print out the global elements and their attributes:

private static XmlSchema 
ReadAndCompileSchema(string fileName) {
    XmlTextReader tr = new XmlTextReader(fileName, 
        new NameTable());

    // The Read method will throw errors encountered
    // on parsing the schema
    XmlSchema schema = XmlSchema.Read(tr, 
        new ValidationEventHandler(ValidationCallbackOne));
    tr.Close();

    // The Compile method will throw errors 
    // encountered on compiling the schema
    schema.Compile(new ValidationEventHandler
                    (ValidationCallbackOne));
    return schema;
}

public static void ValidationCallbackOne(object sender, 
    ValidationEventArgs args) {
    Console.WriteLine(“Exception Severity: “ + args.Severity);
    Console.WriteLine(args.Message);
}

private void TraverseSOM() {
    XmlSchema custSchema = ReadAndCompileSchema(
                              "Customer.xsd");

    foreach (XmlSchemaElement elem in 
                 custSchema.Elements.Values) {

        Console.WriteLine("Element: {0}", elem.Name);

        if (elem.ElementType is XmlSchemaComplexType) {

            XmlSchemaComplexType ct = 
                (XmlSchemaComplexType) elem.ElementType;

            if (ct.AttributeUses.Count > 0) {

                IDictionaryEnumerator ienum = 
                    ct.AttributeUses.GetEnumerator();

                while (ienum.MoveNext()) {

                    XmlSchemaAttribute att = 
                       (XmlSchemaAttribute) ienum.Value;

                    Console.Write("Attribute: {0}  ",
                                  att.Name);
                }
            }
        }
    }
}

The ElementType can be XmlSchemaType (XmlSchemaSimpleType or XmlSchemaComplexType) if it is a user-defined simple type or complex type; or it can be XmlSchemaDatatype if it is one of the built-in datatypes defined in XML Schema Part 2: Datatypes.

For our Customer schema, the ElementType of the Customer element is XmlSchemaComplexType, FirstName element is XmlSchemaDatatype, and LastName element is XmlSchemaSimpleType. The first code sample to create a Customer schema made use of the Attributes collection in the XmlSchemaComplexType class to add the attribute CustID to the Customer element. This is a pre-schema-compilation property. The corresponding post-schema-compilation property is the AttributeUses collection on the XmlSchemaComplexType class, which holds all the attributes of the complex type, including the ones that are inherited through type derivation. It's more useful to query this property once the schema has been compiled.

Editing

Editing is one of the most important usage scenarios for the SOM. All the pre-schema-compilation properties can be set to change the existing values, and the schema can be re-compiled to reflect changes in the object model. The following two samples illustrate editing scenarios in the SOM.

Sample 1

Let's modify our Customer schema by adding a new element "PhoneNumber" to the Customer element. We load the schema into the SOM using the Read method, traverse to the element, retrieve its complexType and add an element to it. Since the schema has been already created and compiled, we can iterate through its "Elements" post-schema-compilation property to find the "Customer" element.

The ElementType is also part of the PSCI: it is the compiled complex type of the "customer" element. Since we need to add another element to the "Customer" element, we can retrieve the particle of the complex type and add our new "PhoneNumber" element to it, which the following code accomplishes:

private void ModifySOM1() {

    XmlSchema custSchema = ReadAndCompileSchema("Customer.xsd");

    // Create the new element to be added
    XmlSchemaElement phoneElem = new XmlSchemaElement();
    phoneElem.Name = "PhoneNumber";

    // Create a simple type with a pattern for the 
    // phone number element
    XmlSchemaSimpleType phoneType = new XmlSchemaSimpleType();
    XmlSchemaSimpleTypeRestriction restr = new 
        XmlSchemaSimpleTypeRestriction();
    restr.BaseTypeName = new XmlQualifiedName("string",
                             "http://www.w3.org/2001/XMLSchema");

    // Create a Pattern facet
    XmlSchemaPatternFacet phPattern = new XmlSchemaPatternFacet();
    // The value for the pattern is a Regular expression that allows
    // 3 digits followed by "-", then 3 digits, another "-" and 4
    // more digits
    phPattern.Value = "\\d{3}-\\d{3}-\\d{4}";
    restr.Facets.Add(phPattern);

    // Set the content of the simple type to be the simpleType
    // restriction created above
    phoneType.Content = rest;

    // Set the schema type of the PhoneNumber element to be
    //  the anonymous simple type
    phoneElem.SchemaType = phoneType;

    // Locate position to add the PhoneNumber element
    foreach(XmlSchemaElement elem in custSchema.Elements.Values) {
        if(elem.QualifiedName.Name.Equals("Customer")) {
            //get the Complex type of the element
            XmlSchemaComplexType custType = 
                (XmlSchemaComplexType) elem.ElementType;
            XmlSchemaSequence seq = 
                (XmlSchemaSequence) custType.Particle;
            seq.Items.Add (phoneElem);
            break;
        }
    }

    // Recompile schema
    custSchema.Compile(new 
        ValidationEventHandler(ValidationCallbackOne));
    WriteCustomerSchema(custSchema);
}

And emits the following (reformatted for readability.)

<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:tns="http://tempuri.org" 
           targetNamespace="http://tempuri.org" 
           xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="Customer">
    <xs:complexType>
       <xs:sequence>
         <xs:element name="FirstName" type="xs:string" />
         <xs:element name="LastName" type="tns:LastNameType" />
         <xs:element name="PhoneNumber">
           <xs:simpleType>
            <xs:restriction base="xs:string">
            <xs:pattern value="\d{3}-\d{3}-\d{4}" />
           </xs:restriction>
           </xs:simpleType>
         </xs:element>
       </xs:sequence>
       <xs:attribute name="CustID" type="xs:positiveInteger"
                     use="required" />
    </xs:complexType>
   </xs:element>
   <xs:simpleType name="LastNameType">
     <xs:restriction base="xs:string">
       <xs:maxLength value="20"/>
     </xs:restriction>
   </xs:simpleType>
</xs:schema>

Sample 2

In this sample we add an attribute Title to the FirstName element. The FirstName element's type is "xs:string". For it to have an attribute along with string content, we need to change its type to be a complex type, with simple content extension content model.

Since the FirstName element is not a global element in the schema, but a child element of Customer, it is not directly available in the Items or Elements collections. We need to locate the Customer element first before we can edit the FirstName element.

In Sample 1 we saw how to iterate through the XmlSchema Elements PSCI property. In this sample, we iterate through the collection returned by the Items property. Though both properties provide access to the global elements, iterating through the Items collection is more time-consuming since we have to go over all the global components in the schema, and it does not have any post-compilation properties available. By contrast, the PSCI collections (Elements, Attributes, SchemaTypes, etc.) listed in Table 2 provide direct access to the corresponding global element, attribute, or type and their PSCI properties.

private static void ModifySOM2() {
    // Read the customer schema into the SOM
    XmlSchema custSchema = ReadAndCompileSchema("Customer.xsd");

    // Create Complex type for FirstName Element
    XmlSchemaComplexType ct = new XmlSchemaComplexType();
    ct.Name = "FirstNameCT";
    XmlSchemaSimpleContent sct = new XmlSchemaSimpleContent();

    // Create simple content extension
    XmlSchemaSimpleContentExtension simpleExt =
        new XmlSchemaSimpleContentExtension();
    simpleExt.BaseTypeName = new XmlQualifiedName("string",
        "http://www.w3.org/2001/XMLSchema");

    // Create attribute to be added
    XmlSchemaAttribute titleAtt = new XmlSchemaAttribute();
    titleAtt.Name = "Title";
    titleAtt.SchemaTypeName = new XmlQualifiedName("string",
       "http://www.w3.org/2001/XMLSchema");

    // Add attribute to simple content extension
    simpleExt.Attributes.Add(titleAtt);
    
    // Set content of the simple content and content 
    // model of complex type
    sct.Content = simpleExt;
    ct.ContentModel = sct;

    // Add the new complex type to the XmlSchema Items collection
    custSchema.Items.Add(ct);

    foreach (XmlSchemaObject xso in custSchema.Items) {
       if (xso is XmlSchemaElement) {
           XmlSchemaElement elem = (XmlSchemaElement)xso;
           if (elem.QualifiedName.Name.Equals("Customer")) {
               XmlSchemaComplexType custType = 
                   (XmlSchemaComplexType)elem.ElementType;
               XmlSchemaSequence children = 
                   (XmlSchemaSequence)custType.Particle;
               foreach(XmlSchemaParticle p1 in children.Items) {
                   // Check if it is element
                   if (p1 is XmlSchemaElement) {
                       XmlSchemaElement childElem = 
                          (XmlSchemaElement)p1;

                       if (childElem.Name.Equals("FirstName")) {
                           childElem.SchemaTypeName = 
                               new XmlQualifiedName("FirstNameCT", 
                                 "http://tempuri.org");
                           break;
                       }
                   }
               }
           }
       }
    }
    custSchema.Compile(new ValidationEventHandler(ValidationCallbackOne));
    WriteCustomerSchema(custSchema);
}

The following output is generated by the above sample:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:tns="http://tempuri.org" 
            targetNamespace="http://tempuri.org"
            xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="Customer">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="FirstName" type="tns:FirstNameCT" />
        <xs:element name="LastName" type="tns:LastNameType" />
        <xs:element name="PhoneNumber">
          <xs:simpleType>
            <xs:restriction base="xs:string">
              <xs:pattern value="\d{3}-\d{3}-\d{4}" />
            </xs:restriction>
          </xs:simpleType>
        </xs:element>
      </xs:sequence>
      <xs:attribute name="CustID" type="xs:positiveInteger" use="required" />
    </xs:complexType>
  </xs:element>
  <xs:simpleType name="LastNameType">
    <xs:restriction base="xs:string">
      <xs:maxLength value="20"/>
    </xs:restriction>
  </xs:simpleType>
  <xs:complexType name="FirstNameCT">
    <xs:simpleContent>
      <xs:extension base="xs:string">
        <xs:attribute name="Title" type="xs:string" />
      </xs:extension>
    </xs:simpleContent>
 </xs:complexType>
</xs:schema>


Including or Importing Schema Documents

A schema may contain import, include, and redefine elements. These point to other schema documents and augment the structure of the main schema in some way.

For example, the address.xsd sample schema in the WXS Primer can be incorporated into our customer schema so that different address types are available for use. We can incorporate it either as an <xs:include> or <xs:import>, to use its components as-is, or as <xs:redefine> to modify any of its components to suit our needs. Since the address schema has a targetNamespace that is different from that of our customer schema, we use the import semantics.

The XmlSchema object has an Includes property that can be accessed to get at all the includes, imports, or redefines added to the main schema. Each component added to this collection is of type XmlSchemaExternal. Since the XmlSchema object also exposes an Items collection, one might incorrectly assume that the includes and imports defined in the schema are added to it. In fact, they are added to a separate collection which can accessed through the Includes property on XmlSchema. In our example, the imported address schema will be added to the Includes collection and not to the Items collection in the XmlSchema object for the customer schema. The following sample shows how to import the address.xsd schema to the Customer schema:

private void IncludeSchema(string fileName) {

    // Read address.xsd and customer.xsd into the SOM and compile
    XmlSchema addrSchema = ReadAndCompileSchema("address.xsd");
    XmlSchema custSchema = ReadAndCompileSchema("Customer.xsd");

    //Create the import
    XmlSchemaImport imp = new XmlSchemaImport();
    imp.Namespace = "http://www.example.com/IPO";
    imp.Schema = addrSchema;

    //Add the import to the customer schema
    custSchema.Includes.Add(imp);
    custSchema.Compile(new 
       ValidationEventHandler(ValidationCallbackOne));
    custSchema.Write(console.Out);
}

We can also set the SchemaLocation property on the XmlSchemaImport object, instead of the Schema property. In this case, during compilation, the schema will be fetched from the location provided.

The following schema is generated from the sample code:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:tns="http://tempuri.org" 
           targetNamespace="http://tempuri.org"
           xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:import namespace="http://www.example.com/IPO"/>
  <xs:element name="Customer">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="FirstName" type="tns:FirstNameCT" />
        <xs:element name="LastName" type="tns:LastNameType" />
        <xs:element name="PhoneNumber">
          <xs:simpleType>
            <xs:restriction base="xs:string">
              <xs:pattern value="\d{3}-\d{3}-\d{4}" />
            </xs:restriction>
          </xs:simpleType>
        </xs:element>
      </xs:sequence>
      <xs:attribute name="CustID" type="xs:positiveInteger"
                    use="required" />
    </xs:complexType>
  </xs:element>
  <xs:simpleType name="LastNameType">
    <xs:restriction base="xs:string">
      <xs:maxLength value="20"/>
    </xs:restriction>
  </xs:simpleType>
  <xs:complexType name="FirstNameCT">
    <xs:simpleContent>
      <xs:extension base="xs:string">
        <xs:attribute name="Title" type="xs:string" />
      </xs:extension>
    </xs:simpleContent>
  </xs:complexType>
</xs:schema>

If we have included or imported a number of related schemas to the Customer schema -- Order.xsd, address.xsd, orderdetail.xsd and so on -- the following code demonstrates recursive iteration through them and each of their includes or imports:

private void TraverseExternals() {
    XmlSchema custSchema = ReadAndCompileSchema("Customer.xsd");
    RecurseExternals(custSchema);
}

private void RecurseExternals(XmlSchema schema) {
    foreach(XmlSchemaExternal ext in schema.Includes) {
        if(ext.SchemaLocation != null) {
            Console.WriteLine("External SchemaLocation: " +
                ext.SchemaLocation);
        }
        if (ext is XmlSchemaImport) {
            XmlSchemaImport imp = (XmlSchemaImport)ext;
            Console.WriteLine("Imported Namespace: " + 
                imp.Namespace);
        }

        if (ext.Schema != null) {
            ext.Schema.Write(Console.Out);
            //Traverse its externals
            RecurseExternals(ext.Schema);
        }
    }
}

A Schema Library -- The XmlSchemaCollection

For most applications that make use of schemas, there is a need to be able to load a set of schemas together for validating instance documents and cache the schemas for later reuse. The XmlSchemaCollection is designed to serve this purpose. The XmlSchemaCollection can store a set of WXS schemas or XML-Data Reduced (XDR) schemas.

The following code sample illustrates the usage of this class:

private static XmlSchemaCollection CreateSchemaCollection() {
    XmlSchemaCollection xsc = new XmlSchemaCollection();
    xsc.ValidationEventHandler += 
        new ValidationEventHandler(ValidationCallbackOne);
    xsc.Add(null,"address.xsd");
    xsc.Add(null,"Customer.xsd");
    return xsc;
}

The Add method loads the schema document into an XmlSchema object, then compiles the schema and adds it to the collection. The first parameter takes the namespace that the schema belongs to, which is the same as the targetNamespace of the schema. If this parameter is null, the targetNamespace defaults to the one defined in the schema.

Validation using the XmlSchemaCollection

The XmlValidatingReader class in the .NET Framework is used for validating XML documents against a schema or a set of schemas. The XmlSchemaCollection is used by the validating reader for efficient validation. Schemas can be added to the schema collection by accessing its Schemas property. We can either add individual schemas or an entire collection. The following code validates the Customer.xml file against the Customer schema.

private static void Validate() {
    XmlTextReader tr = new XmlTextReader("Customer.xml", 
                                         new NameTable());
    XmlValidatingReader vr = new XmlValidatingReader(tr);
    vr.ValidationEventHandler += 
        new ValidationEventHandler(ValidationCallbackOne);
    vr.ValidationType = ValidationType.Schema;
    vr.Schemas.Add(CreateSchemaCollection());
    while(vr.Read()) {} //Validate
}

Security in the SOM -- Can You Trust Your Schema?

One reason WXS schemas are important to XML is because they set the ground rules for communication across different XML applications. When schemas have to be exchanged across applications, situations may arise in which you cannot trust the source from which your schemas originated. There was a recent question asked on one of our discussion lists about how security is handled in the SOM:

"The larger question I’m investigating is that my app reads XSD schemas, and I’d like to attempt to find externally referenced schemas for the user. I’m trying to figure out if I can safely open imports or includes that have a schemaLocation value without allowing a malicious schema to call a harmful URL with my user’s credentials (like schemaLocation="http://my401k.com/sellEverything?sendCheckTo=MyAddress").

The SOM provides a way by which one can choose to not resolve the externals if the schema is not from a trustworthy source. The Compile method on the XmlSchema class has an overload that takes in an XmlResolver. The Add() methods on XmlSchemaCollection also have corresponding overloads that take in XmlResolvers. In both cases, you can pass in null for the resolver so that the schemaLocation of the includes, imports, and redefines are not resolved. This is available in the Beta 1 release of the .Net Framework. You can also create your own custom resolvers by subclassing the XmlResolver class and overriding the GetEntity method.

Further Reading

The following are links to the complete reference on the Schema Object Model, the class hierarchy in the System.Xml.Schema namespace in the .NET Framework class library.

Acknowledgments

Thanks to Dare Obasanjo, Mark Fussell, Tejal Joshi and Yan Leshinsky for reviewing this article and providing feedback.