No-namespace/Chameleon Include Support for .NET 1.1






4.21/5 (3 votes)
Add support for chameleon include to .NET 1.1 using a custom XmlResolver to modify schema dynamically.

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.
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
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="test">
<xsd:include schemaLocation="b.xsd" />
<xsd:include schemaLocation="c.xsd" />
</xsd:schema>
<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>
<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>
Concepts
The following links are useful for understanding Chameleon Namespaces and System.Xml.XmlUrlResolver
s:
- Multi-Schema Project: Zero, One, or Many Namespaces? # Chameleon Namespace Design
An example of a "main" schema defining thetargetNamespace
and the "supporting" schemas have notargetNamespace
. - MSDN: Resolving the Unknown: Building Custom XmlResolvers in the .NET Framework
Examples of customXmlResolver
s. The Data Flow Diagram for theXmlUrlResolver
class if helpful for understanding how things fit together.
Reproducing the Problem
The following is a fairly standard way to add a Schema
to a XmlSchemaCollection
using the namespace and main schema path.
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
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:
- Use the base implementation of
GetEntity
to retrieve the included schema. - Create an
XmlDocument
from the stream. - If the schema is chameleon (it does not define a
targetNamespace
or default namespace), then set thetargetNamespace
and default namespace (xmlns) attributes to the parent schemas namespace. - Convert the
XmlDocument
back to a stream.
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;
}
}
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.
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);
}
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.
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;
}
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