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 System.Xml.XmlUrlResolvers:
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)
{
rootNode = rootNode.NextSibling;
}
System.Diagnostics.Debug.Assert(rootNode is XmlElement);
foreach(XmlAttribute existingXmlAttribute in rootNode.Attributes)
{
if(existingXmlAttribute.Name == "xmlns"
|| existingXmlAttribute.Name == "targetNamespace")
{
return;
}
}
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