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.
<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.
<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.
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="test" type="testType"/>
</xsd:schema>
Schema c.xsd - test is declared with the testType type.
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.
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 the targetNamespace
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);
}
UpdateTargetAndDefaultNamespace(schemaDocument);
System.IO.Stream updatedSchemaStream = ConvertToStream(schemaDocument);
return updatedSchemaStream;
}
catch (System.Exception ex)
{
_internalException = ex;
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.
private void UpdateTargetAndDefaultNamespace(XmlDocument schemaDocument)
{
XmlNode rootNode = schemaDocument.FirstChild;
while(rootNode is XmlDeclaration || rootNode is XmlComment)
{
?> 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.
private System.IO.Stream ConvertToStream(XmlDocument schemaDocument)
{
System.IO.MemoryStream memoryStream = new System.IO.MemoryStream();
schemaDocument.Save(memoryStream);
memoryStream.Flush();
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
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.