Click to Rate and Give Feedback
Related Articles

Jeremy Miller continues his discussion of persistence patterns by reviewing the Unit of Work design pattern and examining the issues around persistence ignorance.

Jeremy Miller

MSDN Magazine June 2009

...

Read more!

This article describes how to use XHTML and ASP.NET MVC to implement REST services.

Aaron Skonnard

MSDN Magazine July 2009

...

Read more!

Silverlight 2 applications are restricted to running inside a browser. However, Silverlight 3 applications can run inside the browser or out. Here we build a social networking app as a standalone Silverlight 3 application.

John Papa

MSDN Magazine June 2009

...

Read more!

This month we demonstrate how easy it is to use IronPython to test .NET-based libraries.

James McCaffrey

MSDN Magazine June 2009

...

Read more!

Use Test-Driven Development with mock objects to design object oriented code in terms of roles and responsibilities, not categorization of objects into class hierarchies.

Isaiah Perumalla

MSDN Magazine June 2009

...

Read more!

Popular Articles

See how routed events and routed commands in Windows Presentation Foundation form the basis for communication between the parts of your UI.

Brian Noyes

MSDN Magazine September 2008

...

Read more!

Jason Clark

MSDN Magazine July 2003

...

Read more!

One-time passwords offer solutions to dictionary attacks, phishing, interception, and lots of other security breaches. Here's how it all works.

Dan Griffin

MSDN Magazine May 2008

...

Read more!

Paul DiLascia

MSDN Magazine August 2002

...

Read more!

The MVP pattern helps you separate your logic and keep your UI layer free of clutter. This month learn how.

Jean-Paul Boodhoo

MSDN Magazine August 2006

...

Read more!

XML Schemas
Take Advantage of Existing External XML Schemas with a Custom Import Framework in ASP.NET
Scott Short
Code download available at: XMLSchemaImporting.exe (58 KB)
Browse the Code Online

This article assumes you're familiar with XML and ASP.NET
Level of Difficulty 1 2 3
SUMMARY
Over the years, many industry-standard XML schemas and dialects have been developed. These industry-specific schemas embrace the original purpose of XML and are extremely valuable in promoting and supporting B2B interaction. Unfortunately, the ASP.NET Web Services runtime does not allow developers to directly reference external schemas from within their XML Web Services interface (the WSDL file). This article builds an external schema framework as an extension to the ASP.NET Web Services runtime to enable you to reference external schemas within your XML Web Service interface.
One of the primary goals of XML Web Services is to facilitate interoperability between applications running on a broad range of platforms. This interoperability is achieved through the adherence to recognized standards. They include, but are not limited to, the standards at the core of XML Web Services: XML, XML Schema (XSD), Web Services Description Language (WSDL), and SOAP.
In addition, you may also want to take advantage of emerging standards such as the Global XML Web Services Architecture (GXA). GXA defines a set of modular extensions to the XML Web Services platform for security, routing, and discovery.
Adherence to standards is not limited to platform protocols. Many industries have collaboratively developed XML schemas that define industry-specific concepts. Among others, the travel industry (http://www.opentravel.org), the hospitality industry (http://www.hitis.org), and the education industry (http://www.imsproject.org) have published schemas. If your XML Web Service is intended for a particular industry, it makes sense to leverage targeted schemas.
You can also use task-oriented XML dialects. Some examples include the Astronomical Markup Language (AIML), Robotic Markup Language (RoboML), and the Speech Application Language Tags (SALT). The more an XML Web Service adheres to recognized standards, the higher the probability that it will be consumed by others.
In most cases, leveraging an industry standard involves referencing external schemas within the WSDL document of your XML Web Service. Unfortunately, the initial version of the ASP.NET Web Services platform does not provide out-of-the-box support for referencing external schemas.
One way to overcome this limitation would be to write your WSDL file by hand and ensure that your XML Web Service supports the interface defined by the custom WSDL file. Doing so, however, makes development and maintenance more complex. First, it is pretty easy for the implementation and the interface to become out of sync. Second, not only does the original developer need to have a strong knowledge of WSDL, but any developer who maintains the code will have to as well.
Another way to reference external schemas from your XML Web Service is to extend the ASP.NET Web Services platform, which provides a rich interception model that allows developers to add this kind of extended functionality. I used this interception model to create what I call an external schema framework (ESF).
Using an ESF has many advantages over modifying WSDL by hand. For example, it supports an attribute-based programming paradigm that should already be familiar to developers writing to the ASP.NET platform. It also circumvents our problem associated with WSDL.exe when it is used to generate Microsoft® .NET proxy classes for a WSDL document that imports an external schema. The problem is that the current version of WSDL.exe requires a superfluous WSDL import, even if none of the entities defined within the external schema are directly referenced by WSDL elements. I'll explain this in more detail later in the article, but if you use my external schema framework, you will avoid this.
This article is composed of two primary sections. In the first section, I'll describe how to use the ESF and in the second, I'll discuss its implementation. If you are interested in delving deeper into the implementation, download the entire source code from the link at the top of this article.

Using the External Schema Framework
The ESF will feel very familiar to anyone who has used the XML serialization attributes—the set of attributes that can be used to control how instances of .NET classes, as well as the class definitions themselves, are serialized to XML. The XML serialization attributes are also used by the ASP.NET runtime to control the creation of the XSD within the WSDL file of a document-literal style XML Web Service.
The ESF defines three attributes: XmlImportedType, XmlImportedElement, and XmlImportedRoot. These custom attributes are extensions of their XML serialization attribute counterparts. For example, the XmlImportedElement attribute is used in much the same way as the XmlElement attribute.
Before you can use the ESF attributes, you must configure the web.config file of the XML Web Service that will use the framework. Add the <soapExtensionReflectorTypes/> section to the web.config file for the ASP.NET Web Service that uses the ESF attributes. (In the case of the ESF, I defined the XmlImportedAttributesReflector class.)
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.web>
    <webServices>
      <soapExtensionReflectorTypes>
        <add type=
          "SShort.Xml.Serialization.XmlImportedAttributesReflector, 
          SShort.Xml.Serialization"/>
      </soapExtensionReflectorTypes>
    </webServices>
  </system.web>
</configuration>
The XmlImportedAttributesReflector is registered as a SOAP Extension Reflector type. I will discuss this class and the SOAP Extension Reflector extension framework in much greater detail in the second half of this article. For now, suffice it to say that this class is responsible for modifying the WSDL document based on the ESF attributes that are used in the implementation of the ASP.NET Web Service.
Now that I have explained how to properly configure the XmlImportedAttributesReflector, I'll walk through a few examples that demonstrate how to use the ESF attributes.

The XmlImportedType Attribute
In my first example, suppose I want to create an XML Web Service that consumes an element of type MyExternalDatatype as defined in a schema file called MyExternalSchema.xsd. The contents of MyExternalSchema.xsd are shown in the MyExternalDatatype definition (see Figure 1).
<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema targetNamespace="urn:MyExternalSchema" xmlns:xsd="http://
  www.w3.org/2001/XMLSchema">
    <xsd:complexType name="MyExternalDatatype">
        <xsd:sequence>
            <xsd:element minOccurs="1" maxOccurs="1" name="MyString" 
              type="xsd:string" />
            <xsd:element minOccurs="1" maxOccurs="1" name="MyInt" 
              type="xsd:int" />
            <xsd:element minOccurs="1" maxOccurs="1" name="MyBool" 
              type="xsd:boolean" />
        </xsd:sequence>
    </xsd:complexType>
</xsd:schema>
First, I define a .NET type to which the MyExternalDatatype will be mapped. There are a number of ways you can define the .NET type; the primary requirement is that the type not be more restrictive than instances of the MyExternalDatatype XML datatype. This ensures that the XML serializer, the component responsible for serializing and deserializing instances of .NET types to and from XML, will be able to deserialize all valid instances of the XML datatype received from the client into an instance of the corresponding .NET type.
Consider the following type definitions:
[XmlImportedType("urn:MyExternalSchema", "MyExternalSchema.xsd",
TypeName="MyExternalDatatype")]
public class MyLooseType
{
    [XmlAnyAttribute]
    XmlAttribute [] attributes;

    [XmlAnyElement]
    XmlElement [] elements;
}
The MyLooseType .NET type definition certainly meets the criteria of not being more restrictive than instances of the MyExternalDatatype XML datatype. However, consider the following alternative .NET type definition:
[XmlImportedType("urn:MyExternalSchema", "MyExternalSchema.xsd",
TypeName="MyExternalDatatype")]
public class MyType
{
    public string    MyString;
    public int       MyInt;
    public bool      MyBool;
}
The MyType .NET type maps more closely to the MyExternalDatatype XML datatype. Because of this, it offers a number of advantages over the MyLooseType .NET type.
First, the MyType .NET type is easier to program against. Most developers find it easier to access fields and properties than to iterate through an array of loosely typed elements. Second, the MyType .NET type provides better safeguards against errors seeping into the implementation of the XML Web Service. For example, if you try to set the MyBool field to a string value, the C# compiler will generate an error when you compile the Web Service.
It doesn't matter if you use the MyLooseType or the MyType .NET types within your XML Web Service; the same WSDL will be generated since they both are decorated with the XmlImportedType attribute that specifies the same set of parameters. Because of these advantages, the MyExternalDatatype code (shown in Figure 2) uses the MyType .NET type definition.
using System.Web.Services;
using SShort.Xml.Serialization;

public class MyWebService : WebService
{
    [WebMethod]
    public void MyWebMethod(MyType param1)
    {
    }
}

[XmlImportedType("urn:my-schema", "MySchema.xsd", TypeName="MyExternalDatatype")]
public class MyType
{
    public string    MyString;
    public int    MyInt;
    public bool    MyBool;
}

The XmlImportedElement Attribute
Many times, an external schema will use anonymous XML datatypes, which are defined within an element declaration and cannot be referenced by some other definition. Instead of referencing the XML datatype directly, the schema will define a set of elements that you can reference within your XML Web Service.
The use of anonymous XML datatypes is becoming common in modern schema definitions. For example, many of the GXA schemas, such as the WS-Security schema (http://schemas.xmlsoap.org/ws/2002/04/secext), make heavy use of anonymous XML datatypes. As a result, I cannot use the XmlImportedType attribute because I cannot reference them directly. Instead, I will use the XmlImportedElement attribute.
For my next example, I will create an XML Web Service that accepts a purchase order as defined by the external schema shown in Figure 3. The schema, contained with the PurchaseOrder.xsd file, defines the PurchaseOrder element that contains a number of anonymous XML datatype definitions.
<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema targetNamespace="urn:SomeIndustryStandardsOrganization" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <xsd:element name="PurchaseOrder">
    <xsd:complexType>
      <xsd:sequence>
        <xsd:element minOccurs="1" maxOccurs="1" name="CompanyName" 
          type="xsd:string" />
        <xsd:element minOccurs="1" maxOccurs="1" name="MyInt" 
          type="xsd:int" />
        <xsd:element minOccurs="1" maxOccurs="1" name="Items">
          <xsd:complexType>
            <xsd:sequence>
              <xsd:element minOccurs="1" maxOccurs="unbounded" 
                name="Item">
                <xsd:complexType>
                  <xsd:sequence>
                    <xsd:element minOccurs="1" maxOccurs="1" 
                      name="Quantity" type="xsd:int" />
                    <xsd:element minOccurs="1" maxOccurs="1" 
                      name="UnitPrice" type="xsd:double" />
                    <xsd:element minOccurs="1" maxOccurs="1" 
                      name="ExtendedPrice" type="xsd:double"
                    />
                  </xsd:sequence>
                    <xsd:attribute name="sku" type="xsd:string" />
                </xsd:complexType>
              </xsd:element>
            </xsd:sequence>
          </xsd:complexType>
        </xsd:element>
      </xsd:sequence>
        <xsd:attribute name="id" type="xsd:string" />
    </xsd:complexType>
  </xsd:element>
</xsd:schema>
I cannot reference the underlying XML datatype of the PurchaseOrder element because it is anonymous. Instead, I need to reference the element definition. In the next example, the SubmitPurchaseOrder1 Web method, I do so using the XmlImportedElement attribute shown here:
[WebMethod]
[SoapDocumentMethod(OneWay=true)]
public void SubmitPurchaseOrder(
  [XmlImportedElement("urn:SomeIndustryStandardsOrganization", 
    "PurchaseOrder.xsd",
    ElementName="PurchaseOrder")]
  PurchaseOrder po)
{
  // Implementation ...
}
In the previous code, the SubmitPurchaseOrder Web method accepts a purchase order as defined by the schema contained in the PurchaseOrder.xsd file. This is done by using the XmlImportedElement attribute to map the PurchaseOrder parameter to the PurchaseOrder XML element. When the user submits a valid purchase order document, it will be deserialized by the ASP.NET runtime into an instance of the PurchaseOrder .NET type.

The XmlImportedRoot Attribute
In general, the XmlImportedElement attribute should be used to decorate public properties and fields defined by a class and not parameters within a method signature. To understand why, suppose I wanted to define another Web method called RequestPurchaseOrder that returns a purchase order to the caller.
Both the SubmitPurchaseOrder and ReceivePurchaseOrder methods could reference the same class definition, but each of the parameters of type PurchaseOrder would have to be decorated with the XmlImportedElement attribute. In the event that the definition of the PurchaseOrder element changed, both attributes would need to be updated.
Instead of decorating each parameter with the XmlImportedElement attribute, I will decorate the PurchaseOrder class itself with the XmlImportedRoot attribute, as shown here in the SubmitPurchaseOrderRequest2 Web method:
[WebMethod]
  [SoapDocumentMethod(OneWay=true)]
  public void SubmitPurchaseOrder(PurchaseOrder po)
  {
    // Implementation ...
}
XmlImportedRoot controls how an XML instance document is deserialized into an instance of the PurchaseOrder .NET datatype.
Even though the purchase order element is not the root element of the request document, the use of the XmlImportedRoot attribute is valid because the implementation of the ASP.NET runtime serializes/deserializes each parameter separately. Therefore, because each input parameter is deserialized from its own instance document, the XmlImportedRoot attribute decorating the underlying .NET type definition is honored.
There are some scenarios where you must use the XmlImportedRoot attribute rather than the XmlImportedElement attribute. For example, suppose your Web method accepts a SOAP header defined by an external schema. The ASP.NET runtime deserializes the SOAP header into a member variable of the class that types the XML Web Service. The problem with SOAP headers is that if you decorate the member variable that represents the SOAP header with the XmlImportedElement attribute, the attribute will be ignored by the ASP.NET runtime. Like parameters, the member variable containing the SOAP header is serialized and deserialized separately. Therefore, you must decorate the .NET type definition that corresponds to the SOAP header with the XmlImportedRoot attribute in order to reference SOAP headers that are defined in external schemas.

Implementing the ESF
Now that you know how to use the ESF, I want to show you the fun part—how the framework was created. To cover every detail, however, would require more than a single article, so I'll cover only some of the more interesting sections of the code and recommend that you take a look at the full code download for more details.
As you have seen, the framework is composed of two parts: a set of custom attributes and a component responsible for modifying the WSDL created by ASP.NET, the XmlImportedAttributesReflector. The implementation of the custom attributes is fairly straightforward, whereas the bulk of the work is performed within the XmlImportedAttributesReflector.

Defining the Custom Attributes
Within the framework, I define three custom attributes: XmlImportedElement, XmlImportedRoot, and XmlImportedType. The XmlImportedTypeAttribute is shown in Figure 4. All three of these attributes are derivatives of the custom attributes defined by the XML serialization framework.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, 
  Inherited = false, AllowMultiple = false)]
public class XmlImportedTypeAttribute : XmlTypeAttribute
{
    private string schemaLocation;

    public XmlImportedTypeAttribute(string xmlNamespace, string 
      schemaLocation)
    {
        base.Namespace = xmlNamespace;
        this.schemaLocation = schemaLocation;
    }

    public string SchemaLocation
    {
        get { return schemaLocation; }
        set { schemaLocation = value; }
    }
}
As with all the ESF attributes, the XmlImportedType attribute extends its base type by exposing one additional property, the SchemaLocation property, which exposes the location of the external schema. Since the schema location as well as the XML namespace URI is required by the XmlImportedAttributesReflector, a constructor is defined to ensure that both are provided when creating a new instance of the attribute.
Since the XmlImportedTypeAttribute class derives from the XmlTypeAttribute class, .NET types decorated with the XmlImportedType attribute will be processed by the ASP.NET runtime as if they were decorated with the XmlType attribute. The XmlImportedAttributesReflector class will take advantage of this and build upon this functionality. I will describe this in more detail later.

The XmlImportedAttributesReflector
As previously mentioned, the XmlImportedReflector class is responsible for modifying the WSDL that is auto-generated by the ASP.NET runtime based on the use of the custom attributes I defined. It does so by using the SOAP Extension Reflector interception mechanism provided by the ASP.NET platform.
To create a SOAP Extension Reflector, you create a class that derives from the SoapExtensionReflector class. You can then add your own implementation by overriding the ReflectMethod virtual method defined by the SoapExtensionReflector class. In the case of the ESF, the XmlImportedReflector class derives from the SoapExtensionReflector class.

The ReflectMethod Method
The ASP.NET runtime builds the WSDL file incrementally by iterating through each method decorated with the WebMethod attribute within the class that implements the Web Service. When the SOAP Extension Reflector is registered within an ASP.NET Web Service's web.config file, the ASP.NET runtime will create an instance of the class and then invoke its ReflectMethod method.
In most cases, the ASP.NET runtime will invoke ReflectMethod after the WSDL has been created for each Web method. This provides the SOAP Extension Reflector with an opportunity to modify the WSDL as it sees fit.
In the case of the SoapExtensionReflector class, I need to process the ESF attributes. This involves processing each Web method as well as the complete WSDL document.
Unfortunately, the SOAP Extension Reflector framework does not provide an interception point for performing processing that must occur once the WSDL document has been completely built. Therefore, I implement a workaround within the ReflectMethod method, which I will cover later in the article.
For now, I will discuss the implementation of ReflectMethod for the XmlImportedAttributes class shown in Figure 5. Each time ReflectMethod is invoked, it is responsible for reflecting on the method signature of the Web method in search of ESF attributes. This is accomplished through the SoapExtensionReflector.ReflectionContext property.
// Processes the XmlImportedXxx attributes for an individual Web method.
public override void ReflectMethod()
{
    this.reflectMethodCallCount++;

    // If it hasn't already been done, count the number of Web methods 
    // that do not reference an externally defined binding.
    if(this.totalReflectMethodCalls == -1)
    {
        this.totalReflectMethodCalls = this.GetTotalRefectMethodCalls();
    }

    // Process the return parameter and its type.
    this.ProcessReturnParameter();

    // Process each parameter in the method signature.
    this.ProcessMethodParameters();

    // Process the method's SOAP headers.
    this.ProcessSoapHeaders();

    // If this is the last call to ReflectMethod, then process the 
    // imports. 
    // Note:  The code to modify the WSDL document needs to be 
    //        run after all of the Web methods have been processed.
    //        When the framework provides a better interception point, 
    //        this code should be relocated accordingly.
    if(this.reflectMethodCallCount == this.totalReflectMethodCalls)
    {
        this.ProcessImportedNamespaces();
    }
}

Processing the ESF Attributes
The ReflectionContext property provides access to an instance of the ProtocolReflector class. This object is updated before the ReflectMethod is executed by the runtime and contains references to objects related to the Web method currently being processed. The properties of ProtocolReflector provide access to WSDL-specific objects, such as the corresponding binding definition and instances of the Reflection API objects related to the Web method.
For an example where I use the ReflectionContext property, let's review the ProcessReturnParameter method, shown in Figure 6. This method is responsible for processing any ESF attributes that decorate the return parameter and its underlying type. To determine if any ESF attributes do this, I access the Method property through the ReflectionContext property. This property contains information relevant to the method, including its return parameter. First, I determine if the return parameter is decorated with the XmlImportedElement attribute. I do so by examining the ReturnTypeCustomAttributeProvider property. Despite its name, it provides access to the custom attributes exposed by the return itself and not its underlying type. Next, I determine if the return parameter's underlying type is decorated with the XmlImportedRoot and XmlImportedType attributes. I perform searches for each attribute via the ReturnType property's GetCustomAttributes method. Finally, I call the ProcessTypeMembers method, which recursively processes the public properties and fields exposed by the type. This recursion can be expensive, so consider publishing a static copy of the WSDL document generated by the ASP.NET runtime when the XML Web Service is released to production.
// Process the return parameter and its type.
// - Process XmlImportedElement attribute on the return parameter.
// - Process XmlImportedRoot and XmlImportedType attributes on the return 
// parameter type.
// - Process the members of the return parameter type.
private void ProcessReturnParameter()
{
    // Process the return parameter for the XmlImportedElement attribute.
    // Note:  The method name ReturnTypeCustomAttributeProvider is 
    //        confusing because it is really the custom attribute 
    //        provider for the return parameter, not the return parameter 
    //        type.
    object [] importedElementAttributes = this.ReflectionContext
        .Method.ReturnTypeCustomAttributeProvider.GetCustomAttributes
        (typeof(XmlImportedElementAttribute), false);
    if(importedElementAttributes.Length > 0)
    {
        // Note:  If length is > 0, then I can assume that the length is 
        //        1 since the AllowMultiple property is set to false.
        XmlImportedElementAttribute importedElementAttribute = 
            (XmlImportedElementAttribute)importedElementAttributes[0];
        this.AddImportedNamespace(importedElementAttribute.Namespace, 
            importedElementAttribute.SchemaLocation);
    }

    // Process the return parameter type for the XmlImportedRoot 
    // attribute.
    object [] importedRootAttributes = 
        this.ReflectionContext.Method.ReturnType.GetCustomAttributes
        (typeof(XmlImportedRootAttribute), false);
    if(importedRootAttributes.Length > 0)
    {
        // Note:  If length is > 0, then I can assume that the length is 
        //        1 since the AllowMultiple property is set to false.
        XmlImportedRootAttribute importedRootAttribute = 
            (XmlImportedRootAttribute)importedRootAttributes[0];
        this.AddImportedNamespace(importedRootAttribute.Namespace, 
            importedRootAttribute.SchemaLocation);
    }

    // Process the return parameter type for the XmlImportedType 
    // attribute.
    object [] importedTypeAttributes = 
        this.ReflectionContext.Method.ReturnType.GetCustomAttributes
        (typeof(XmlImportedTypeAttribute), false);
    if(importedTypeAttributes.Length > 0)
    {
        // Note:  If length is > 0, then I can assume that the length is 
        //        1 since the AllowMultiple property is set to false.
        XmlImportedTypeAttribute importedTypeAttribute = 
            (XmlImportedTypeAttribute)importedTypeAttributes[0];
        this.AddImportedNamespace(importedTypeAttribute.Namespace, 
            importedTypeAttribute.SchemaLocation);
    }

    // Process the members of the return type.
    this.ProcessTypeMembers(this.ReflectionContext.Method.ReturnType);
}
I perform similar steps when processing each of the method parameters as well as any SOAP headers associated with the method. The major difference is when processing the SOAP headers, I don't need to search for the XmlImportedElement attribute.
If an attribute is found on a public property or field, the WSDL document is not immediately modified. Instead, the namespace and the schema location are obtained from the attribute and placed in a hash table. Once the entire WSDL document is created, it is then modified to reflect the imported elements and/or datatypes.
The primary reason for waiting to modify the WSDL until it has been entirely populated by the ASP.NET runtime is efficiency. By processing a consolidated list of namespaces and schema locations, I avoid having to delete the schema for a given namespace multiple times.
Unfortunately, the SOAP Extension Reflector framework does not provide an interception point for this purpose. As a workaround, I take advantage of the fact that I have the entire WSDL generated by the ASP.NET runtime during the execution of the last call to ReflectMethod.
When ReflectMethod is first called, I calculate the number of times the method will be called (totalReflectMethodCalls) and maintain a counter on how many times the method has been called (reflectMethodCallCount). Once totalReflectMethodCalls is equal to reflectMethodCallCount, I will execute the code that modifies the WSDL so that it properly imports the external schemas.
The real trick is figuring out the number of times the ReflectMethod will be called. The GetTotalReflectMethodCalls method shown in Figure 7 is responsible for obtaining the count.
// Count the number of Web methods that do not reference an externally 
// defined binding. Note: ReflectMethod is called by the runtime for 
// each Web method exposed by the Web Service that is not also decorated 
// with the WebServiceBinding attribute.
protected int GetTotalRefectMethodCalls()
{
    int count = 0;

    MethodInfo [] methodInfos = 
        this.ReflectionContext.ServiceType.GetMethods();
    foreach(MethodInfo methodInfo in methodInfos)
    {
        // If this is a Web method and it , increment the count.
        if(methodInfo.GetCustomAttributes(typeof(WebMethodAttribute), 
        false).Length > 0)
        {
            // Determine if the Web method references an externally 
            // defined binding. If so, don't include it in the count.
            Object [] docAttributes = 
                methodInfo.GetCustomAttributes
                (typeof(SoapDocumentMethodAttribute), false);
            if(docAttributes.Length > 0)
            {
                SoapDocumentMethodAttribute docAttribute = 
                    (SoapDocumentMethodAttribute)docAttributes[0];
                if(docAttribute.Binding == null || docAttribute.Binding 
                  == "")
                {
                    count++;
                }
            }
            else
            {
                count++;
            }
        }
    }

    return count;
}
The implementation is actually pretty straightforward, with one minor twist. I iterate through each of the methods exposed by the .NET type that contains the implementation of the Web Service. While doing so, I maintain a count of the methods that are decorated by the WebMethod attribute. Now here's the twist. The ReflectMethod will not be called for any methods that reference an externally defined WSDL binding. Therefore, I check to see if the method is also decorated with the SoapDocumentMethod attribute where its Binding property contains a non-empty string. If so, I will not increment the counter.

Modifying the WSDL
Once the WSDL document has been created by the ASP.NET runtime, the XmlImportedAttributesReflector needs to modify the WSDL so that it properly imports any external schemas. This may involve importing the external schema into the WSDL, importing the external schema in one of the schemas embedded within the types section of the WSDL document, or both.
Both the WSDL and XML Schema specifications define their own mechanism for importing an external schema. To confuse matters, both of the specifications accomplish this by defining an <import/> element.
If an entity defined within the external schema is referenced within the WSDL document itself, the external schema can be referenced using the WSDL <import/> element. If entities defined within the external schema are referenced within a schema definition that is in turn embedded within the WSDL document, then the external schema can be referenced by the embedded schema using the XML Schema <import/> element.
When the ASP.NET runtime makes its last call to ReflectMethod, I invoke the ProcessImportedNamespaces method shown in Figure 8. This method is responsible for modifying the WSDL document so that it properly references elements and datatypes defined within external schemas.
// Modifies the WSDL document generated by the runtime so that it 
// properly imports external schemas.
public void ProcessImportedNamespaces()
{
    foreach(DictionaryEntry importedNamespace in this.importedNamespaces)
    {
        // If the datatype is defined within the schema, remove it and 
        // import it instead.
        XmlSchema externalSchema = 
            this.ReflectionContext.ServiceDescription.Types.Schemas[(string)
            importedNamespace.Key];
        if (externalSchema != null) 
        {
            // Remove the schema containing the externally defined 
            // datatypes.
            ReflectionContext.ServiceDescription
                .Types.Schemas.Remove(externalSchema);

            // Import the schema into the WSDL document.
            // Note:  Setting the schemaLocation attribute in the schema 
            //        that uses the type should be sufficient.  However, 
            //        WSDL.exe currently has a bug (#108843) that 
            //        requires that a WSDL import element be defined.
            //        Otherwise, I would only need to do so if the WSDL 
            //        document directly references entities defined 
            //        within the external schema.
            Import import = new Import();
            import.Namespace = (string)importedNamespace.Key;
            import.Location = (string)importedNamespace.Value;
            ReflectionContext.ServiceDescription.Imports.Add(import);
        }
    }

    // Modify the import statements in all remaining schemas.
    foreach(XmlSchema schema in 
        this.ReflectionContext.ServiceDescription.Types.Schemas)
    {
        foreach(XmlSchemaImport importedSchema in schema.Includes)
        {
            if(this.importedNamespaces.ContainsKey(importedSchema.Namespace))
            {
                // Add the schemaLocation attribute to the import 
                // element.
                importedSchema.SchemaLocation = 
                    (string)this.importedNamespaces[importedSchema.Namespace];
            }
        }
    }
}
Recall that the ESF attributes derive from their XML serialization attribute counterparts. Because of this, the ASP.NET runtime will process the attributes as if they were their XML serialization counterparts. Therefore, the ASP.NET runtime will create a schema and redefine the externally defined XML elements and datatypes, properly import the schema into other schemas that reference it, and fully qualify all references to elements and datatypes defined within the schema.
Although incomplete, quite a bit of useful work is performed by the ASP.NET runtime to properly reference entities defined within the external schema. Because of the work performed by the ASP.NET runtime, I avoid writing a considerable amount of grunge code. As a result, I have a couple of additional tasks that I need to perform.
1.Delete any schemas that redefine externally defined XML elements and datatypes.
2.Modify any XML Schema <import/> elements that reference external schemas to include the URL where the external schema can be obtained.
3.If the externally defined XML elements and datatypes are referenced directly from WSDL, I need to add a WSDL <import/> element.
I implement these tasks in the ProcessImportedNamespaces method shown in Figure 8. First, I complete the first and third tasks by iterating through the dictionary of namespaces of external schemas used by the XML Web Service's interface. For each item in the dictionary, I delete the schema that defines the namespace within the WSDL and add a WSDL <import/> element.
I add a WSDL <import/> element for each namespace, regardless if it is used within the WSDL document itself. The primary reason for this is that the current version of WSDL.exe has a bug. WSDL.exe only uses the WSDL <import/> element to resolve the location of an external schema and will ignore any XML Schema <import/> elements.
Finally, I perform the second task and modify all XML Schema <import/> elements that reference the externally defined schemas in order to include the URL where the associated external schemas can be obtained. This is accomplished by iterating through the import elements for each schema within the WSDL document. If the namespace attribute matches one of the external schemas in the importedNamespaces dictionary, then I add the schemaLocation attribute to the import element. I set the value of the schemaLocation attribute to the value of the URI where the external schema is located.

Conclusion
I hope you find the ESF as indispensable as I do. In a future version, I would like to incorporate caching. Better yet, I would like a future version of the SOAP Extension Reflector framework to provide integrated support for caching, even though recursion and the Reflection API impose a performance penalty when generating the WSDL file.
In the meantime, I typically include a static version of the WSDL file as part of the ASP.NET Web Service's deployment instructions. If it is included as part of the installation process, you reduce the chance of the WSDL file getting out of sync with the implementation. I would also like to extend the ESF to allow a developer to import additional kinds of entities. This would include XSD entities such as enumerations. This would also include WSDL entities such as port type and message definitions. The ASP.NET runtime provides a means for you to import binding definitions through the use of the WebServiceBinding attribute. Stay tuned as the ESF continues to evolve and feel free to contact me if you have any questions or suggestions.

For background information see:
Building XML Web Services for the Microsoft .NET Platform by Scott Short (Microsoft Press, 2002)

Scott Shortis a Senior Consultant with Microsoft Consulting Services and author of Building XML Web Services for the Microsoft .NET Platform (Microsoft Press, 2002). Contact him at sshort@microsoft.com.

Page view tracker