Click here to Skip to main content
15,886,137 members
Articles / Programming Languages / XML

No-namespace/Chameleon Include Support for .NET 1.1

Rate me:
Please Sign up or sign in to vote.
4.21/5 (3 votes)
30 Nov 2006CPOL4 min read 31K   119   5   1
Add support for chameleon include to .NET 1.1 using a custom XmlResolver to modify schema dynamically.
Sample Image - ChameleonInclude.jpg

Introduction

The target namespace of a schema document identifies the namespace name of the elements and attributes which can be validated against the schema. A schema without a target namespace can typically only validate elements and attributes without a namespace name. However, if a schema without a target namespace is included in a schema with a target namespace, the target namespaceless schema assumes the target namespaces of the including schema. This feature is typically called the Chameleon schema design pattern.

W3C XML Schema Design Patterns: Avoiding Complexity
by Dare Obasanjo

When attempting to perform a no-namespace/chameleon include with the .NET 1.1 Framework, an XmlSchemaException is thrown with a message like 'Type "###" is not declared'. ### is the type defined outside the chameleon schema. Note that the .NET 2.0 Framework does not have this issue when using the XmlSchemaSet class.

Microsoft refers to this bug as 'Type "###" is not declared in reference to local type of an included XSD Schema file' in Knowledge Base article 317611. Their proposed resolution is to workaround the XSD schema-validation bug by adding the namespace explicitly in the chameleon schema.

This article provides an alternative workaround to that presented by Microsoft. A custom XmlResolver is used to modify chameleon schema dynamically when adding them to a XmlSchemaCollection. The advantage of this approach is that the chameleon schema can still be [__I title="to " xfront]? from [def. <include> the doing is that schema of targetNamespace take-on?__]namespace-coerced by the parent schema.

Background

The Microsoft KB317611 article provides a good background for how to reproduce the bug. The three example schemas they provide, shown below, are used in this article to demonstrate the proposed alternative workaround.

Example Schemas Used by Microsoft

XML
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
     targetNamespace="test">

    <xsd:include schemaLocation="b.xsd" />
    <xsd:include schemaLocation="c.xsd" />
</xsd:schema>
Schema a.xsd - includes b.xsd and c.xsd.
XML
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <xsd:simpleType name="testType">
        <xsd:restriction base="xsd:string">
            <xsd:enumeration value="test"/>
        </xsd:restriction>
    </xsd:simpleType>
</xsd:schema>
Schema b.xsd - a testType type is defined.
XML
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <xsd:element name="test" type="testType"/>             
	<!-- this fails, but it should not fail -->
</xsd:schema>
Schema c.xsd - test is declared with the testType type.

Concepts

The following links are useful for understanding Chameleon Namespaces and <a href="http://msdn.microsoft.com/library/en-us/cpref/html/frlrfsystemxmlxmlurlresolverclasstopic.asp">System.Xml.XmlUrlResolver</a>s:

Reproducing the Problem

The following is a fairly standard way to add a Schema to a XmlSchemaCollection using the namespace and main schema path.

C#
XmlSchemaCollection sc = new XmlSchemaCollection();
sc.Add("test", "..\\..\\Schema\\a.xsd");

Using this code with the example schema will throw a XmlSchemaException due to test in c.xsd that uses the testType type defined in b.xsd.

Fixing the Problem Using a Custom XmlResolver

C#
XmlTextReader xmlTextReader = new XmlTextReader("..\\..\\Schema\\a.xsd");
XmlSchema aSchema = XmlSchema.Read(xmlTextReader, null);
XmlChameleonSchemaResolver xmlResolver = 
	new XmlChameleonSchemaResolver(aSchema);

XmlSchemaCollection sc = new XmlSchemaCollection();
sc.Add(aSchema, xmlResolver);

if(xmlResolver.InternalException != null)
{
	throw xmlResolver.InternalException;
}

Using this approach, the schema is added to the XmlSchemaCollection along with the custom XmlResolver.

The XmlChameleonSchemaResolver inherits from XmlUrlResolver and overrides the GetEntity method. The implementation performs the following:

  1. Use the base implementation of GetEntity to retrieve the included schema.
  2. Create an XmlDocument from the stream.
  3. If the schema is chameleon (it does not define a targetNamespace or default namespace), then set the targetNamespace and default namespace (xmlns) attributes to the parent schemas namespace.
  4. Convert the XmlDocument back to a stream.
C#
public override object GetEntity(Uri absoluteUri, string role, Type typeToReturn)
{
    try
    {
        XmlDocument schemaDocument = new XmlDocument();
        using(System.IO.Stream baseEntity = 
            (System.IO.Stream)base.GetEntity(absoluteUri, role, typeToReturn))
        {			
            schemaDocument.Load(baseEntity);
        }

        //HACK: Workaround bug 317611 with XmlSchemaCollection by putting 
        // any chameleon XSD's into the including schema's target namespace.
        // Bug Report: http://support.microsoft.com/default.aspx?scid=kb;en-us;317611
        UpdateTargetAndDefaultNamespace(schemaDocument);

        System.IO.Stream updatedSchemaStream = ConvertToStream(schemaDocument);

        // Assume typeToReturn is System.IO.Stream
        return updatedSchemaStream;
    }
    catch (System.Exception ex)
    {
        _internalException = ex; //This allows the exception to be read later on
        System.Diagnostics.Debug.WriteLine(ex.Message, "XmlChameleonSchemaResolver");
        throw;
    }
}
GetEntity Method from XmlChameleonSchemaResolver

To update the XmlDocument's namespace attributes, the first XmlElement is found and its attributes checked. If it is a chameleon schema, the attributes are set.

The XmlChameleonSchemaResolver determines the parents targetNamespace from the Schema object passed to the constructor.

C#
private void UpdateTargetAndDefaultNamespace(XmlDocument schemaDocument)
{
    XmlNode rootNode = schemaDocument.FirstChild;
    while(rootNode is XmlDeclaration || rootNode is XmlComment)
    {
        //We aren't interested in <?xml...?> or comments.
        rootNode = rootNode.NextSibling;
    }

    System.Diagnostics.Debug.Assert(rootNode is XmlElement);

    foreach(XmlAttribute existingXmlAttribute in rootNode.Attributes)
    {
        if(existingXmlAttribute.Name == "xmlns" 
            || existingXmlAttribute.Name == "targetNamespace")
        {
            //This is not a chameleon schema as it defines a namespace
            return;
        }
    }

    // Assume that it is safe to make the targetNamespace the 
    // default namespace (xmlns) and that the 
    // XmlSchema (http://www.w3.org/2001/XMLSchema) namespace will be 
    // explicitly qualified for all components in the XMLSchema namespace.
    // See http://www.xfront.com/DefaultNamespace.html for a 
    // good explanation of default namespaces.
    XmlAttribute targetNamespaceAttribute = 
        schemaDocument.CreateAttribute("targetNamespace");
    targetNamespaceAttribute.Value = this._namespace;
    rootNode.Attributes.Append(targetNamespaceAttribute);

    XmlAttribute xmlnsAttribute = schemaDocument.CreateAttribute("xmlns");
    xmlnsAttribute.Value = this._namespace;
    rootNode.Attributes.Append(xmlnsAttribute);
}
UpdateTargetAndDefaultNamespace Method from XmlChameleonSchemaResolver

Points of Interest

XmlChameleonSchemaResolver.InternalException Property

If an exception occurs in XmlChameleonSchemaResolver.GetEntity(...), it gets silently consumed in the framework code. To overcome this, I have added a property to the XmlChameleonSchemaResolver that will expose the last recorded exception. This needs to be checked after adding the schema to the XmlSchemaCollection to identify problems.

It is possible to use XmlSchemaCollection.ValidationEventHandler to detect schema validation errors, but these don't extend to the actual thrown exception.

Converting an XmlDocument to a Stream in Memory

The following approach was used to get an System.IO.Stream from the XmlDocument in memory.

C#
private System.IO.Stream ConvertToStream(XmlDocument schemaDocument)
{
	System.IO.MemoryStream memoryStream = new System.IO.MemoryStream();
	schemaDocument.Save(memoryStream);

	memoryStream.Flush();
	
	// Rewind the Stream
	memoryStream.Seek(0, System.IO.SeekOrigin.Begin);

	return (System.IO.Stream)memoryStream;
}
ConvertToStream Method from XmlChameleonSchemaResolver

Locating the Schema

In the applications where I have used this code, I have also overridden the ResolveUri method to change the absoluteUri that is passed to GetEntiry. The MSDN article from the concepts section provides several examples of this.

Final Words

This workaround provides another option for working with Chameleon schema in .NET 1.1. It assumes it is safe to move all chameleon schemas into one namespace. Also, the XmlSchema (http://www.w3.org/2001/XMLSchema) namespace will need to be explicitly qualified.

It has not been tried with deeply nested includes. Your mileage may vary.

History

  • 29-11-2006: Original article

License

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


Written By
Software Developer (Senior) FuseIT
New Zealand New Zealand
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralAdjustment for non-chameleon schemas Pin
MediaNick12-Apr-07 11:39
MediaNick12-Apr-07 11:39 

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.