Splitting up WSDL: The Importance of targetNamespace

 

Scott Seely
Microsoft Corporation

August 20, 2002

Summary: Explores how to use Microsoft Visual Studio .NET to split up the WSDL into component pieces based on the XML namespace. This allows for reuse of the XML Schema definitions and WSDL elements. (13 printed pages)

Introduction

I'd like to start out with a little note that is way off topic. I don't know how many of you out there will be attending XML Web Services One in Boston, Massachusetts, USA from August 26-30, 2002. I will be at this conference as one of the developers participating in the Web Services Interoperability Demo. This demonstration showcases Web services interoperability across several different Web service stacks. If you are in the area, please stop by the Microsoft booth and say hi. For those of you that can't make it, I expect to be writing a piece on what the demo is all about soon after the show. You can get details on the demonstration by visiting http://www.xmethods.net/wsid-08-2002/. Now, let's get onto this week's topic: How does one use Microsoft® Visual Studio® .NET to help split up the WSDL into component pieces?

When Matt and I wrote the Pencil Sellers samples that showed how to split up the WSDL into several files, we didn't think too much of it. We just used Microsoft® .NET and the related tools to create the separated files for us, and then edited the generated files into something we could lock down. Everything worked fine and we continued. Then the questions started coming in. A number of e-mails basically said, "I don't think you're so smart that you write all of that WSDL by hand. How did you get ASP.NET Web services to do the right thing?" Even some of our co-workers were curious about this. So why is this important? Why is there so much interest in splitting things up?

Those developers looking at Web service interoperability across various implementations typically come to the conclusion that the interface to the Web service is the most important part to get right. The reason: the underlying code can be crafted to work with the XML, and the XML is how others will interact with the Web service. Consumers need not care if the code base is written in C++, Microsoft® Visual Basic® .NET, or Perl.

The ideal development pattern for Web services is to define the data types in XSD and the remainder of the WSDL before the underlying implementation is written. Each component will have its own targetNamespace with a common root URI. This links the items together at one level, and differentiates them by using separate XML namespaces. For example, I may decide to set the root of the URI as something like https://msdn.microsoft.com/samples/AYS. Then I could use the following namespaces for different parts:

So, do I follow the "ideal" development pattern? No. I typically prototype the Web service and verify that I am sending all the required data first. Not only does this provide me with an opportunity to fill in things missed in requirements-gathering, it also provides me with a set of schema and WSDL definitions that are generated by Microsoft® ASP.NET. What I would really like to do is to make use of this work by transitioning from generated WSDL to a set of persistent files. When using generated WSDL through ASP.NET, you are really saying, "This WSDL describes the underlying implementation." Whereas I want to be able to make the statement that the underlying code implements the interface defined by a WSDL binding.

This column describes how to make use of .NET to help you split up the WSDL into something more modular. We do this by defining appropriate namespaces for the base data types, message data types, and WSDL-specific information (message, portType, and binding). When complete, the Web service will only be able to generate a WSDL file that references a persistent WSDL file and that contains a standalone service element. This persistent WSDL file will reference an XSD that describes any data types. This takes the idea of sharing data types, expressed in my column on the topic, a step further. That column explained how to share a data type by reproducing the same type in each WSDL document. In this column, we look at how to take that type and save it in one file that can be imported into any number of WSDL documents.

The Web Service

To start with, we need a Web service. By the end of the article, we will have fine-tuned the WSDL that describes the Web service and store that WSDL in a persistent location. In the end, we will rely upon that WSDL instead of the WSDL generated by ASP.NET to define the Web service interface. The pattern I typically follow is:

  1. Create a Web service.
  2. Fine-tune the WSDL that ASP.NET generates.
  3. Move things into separate files.
  4. Attribute the class(es) so that the ASP.NET-generated WSDL only exposes a WSDL service element.

For our purposes, what the Web service does is not as important as how it is locked down. The following needs to be saved to persistent files:

  • XML Schema for complex types.
  • Binding, message, and portType information.

With that in mind, it should be useful enough to have a Web service that has two functions. One will be a simple Hello World. The other can take a person's name and return a string that contains first name, middle name, and last name properly concatenated. This should be enough to demonstrate the principles easily. The Web service will then be attributed to make the breakup easier.

The Web service will make use of a class called PersonName. PersonName has members to contain the first, middle, and last name. The class itself has a simple declaration:

public class PersonName {
    public string First;
    public string Middle;
    public string Last;
}

The Web service also has some fairly simple code. It contains two functions:

  • HelloWorld: This does exactly what you think it does.
  • NameToString: Takes a PersonName and returns the values as a concatenated string.
public class Lockdown : WebService {
    public Lockdown() {
    }


    [WebMethod()]
    public string HelloWorld() {
        return "Hello World!";
    }

    [WebMethod()]
    public string NameToString( PersonName pn ) {
        return string.Format( "{0} {1} {2}",
            pn.First, pn.Middle, pn.Last );
    }
}

ASP.NET will generate information for HTTP GET and HTTP POST bindings unless it is told not to. These bindings are not used very often except for testing the Web service. Let's remove them up front by editing the web.config file and adding the following lines inside the /configuration/system.web element.

<webServices>
    <protocols>
        <remove name="HttpGet" />
        <remove name="HttpPost" />
    </protocols>
</webServices>   

With all of this in place, we are ready to make the WSDL for this file a bit more permanent.

Breaking Things Up

You could save the generated WSDL to a file and break things up by hand. Along the way, you will likely decide that schema specific to the data being exchanged will go in one XML namespace, schema specific to the way that data is wrapped in messages goes in another XML namespace, and message/portType/binding information goes into yet another XML namespace.

Yet splitting the details up by hand can be tedious. If you know what you are doing, you can get ASP.NET to do most of the work for you. If you follow the details in this section, you will be able to force ASP.NET to place the components for the Web service at four different URLs. One URL will contain a WSDL that imports another WSDL and contains a single service element. A second will contain the schema for the PersonName data type. Another will contain the schema for the messages used by the portType operations. The fourth contains the WSDL for the Web service binding, messages, and portType. These splits happen when you properly apply the WebServiceBindingAttribute to the class implementing the Web service. You also have to attribute the other classes correctly and define what XML namespace the various pieces will live in so that ASP.NET knows which pieces should be generated with requests to various URLs.

Let's get started by setting the namespaces for the items. In all cases, we will be using a namespace that starts out with https://msdn.microsoft.com/samples/AtYourService/2002/08/20. This value will be followed by a name that describes what the namespace defines.

PersonName is a data type. As such, it should go into the /types namespace. When setting the XML namespace for a data type, you need to apply the System.Xml.Serialization.XmlTypeAttribute attribute to the class definition. This attribute affects the way the data type is serialized to and from an XML stream and the way the data type is represented as XML Schema. The following code snippet sets the XML namespace for PersonName:

[System.Xml.Serialization.XmlTypeAttribute(
     Namespace="https://msdn.microsoft.com/samples/AtYourService/"
     + "2002/08/20/types")]
public class PersonName {

If you were to view the WSDL for the Web service at this point, you would see a separate schema section within /definitions/types. A few more attributes need to be applied before we see what it takes to get ASP.NET to separate the schema and WSDL documents.

The next task is to correctly attribute the class implementing the Web service. First, I want to add an attribute that does not really help in our effort. Instead, this is just doing the right thing. By default, a Web service will have all of its information stored in the http://tempuri.org namespace. This is all well and good for testing a Web service, but one should never deploy a Web service that uses this namespace. The System.Web.Services.WebServiceAttribute attribute allows you to be a "good" Web service developer and specify a different namespace for WSDL and XSD that are not specifically related to a different XML namespace. In our case, the attribute is applied to the Lockdown class as follows:

[WebServiceAttribute(Namespace="https://msdn.microsoft.com/samples/"
     + "AtYourService/2002/08/20/WebService")]
public class Lockdown : WebService {

In order to get the binding data separated, it needs to live in an XML namespace separate from the Web service endpoint. We set the XML namespace used by the Web service endpoint when applying the WebServiceAttribute attribute to the Lockdown class. To set the XML namespace used by the binding, use the System.Web.Services.WebServiceBindingAttribute attribute. Again, the attribute will be applied to the Lockdown class.

[WebServiceBindingAttribute("AYS20Aug2002", 
        "https://msdn.microsoft.com/samples/AtYourService/"
        + "2002/08/20/ExternalWSDL")]
[WebServiceAttribute   (Namespace="https://msdn.microsoft.com/samples/"
        + "AtYourService/2002/08/20/WebService")]
public class Lockdown : WebService {

At this point, the WebServiceBindingAttribute has a logical name associated with it, AYS20Aug2002, and specifies the XML namespace that will be used for the message, portType, binding, and any types specific to the messages. This fact does not do us any good since no methods with the WebMethodAttribute attribute are associated with the binding. Once that association is made, ASP.NET will start splitting the layout of the WSDL into separate URLs. The classes that allow that association to be made are System.Web.Services.Protocols.SoapDocumentMethodAttribute and System.Web.Services.Protocols.SoapRpcMethodAttribute. The one you use depends on whether you want the message exchange to be rpc/encoded or document/literal. Since the Web service is document/literal by default, we will use the SoapDocumentMethodAttribute to associate the HelloWorld and NameToString methods with the binding named AYS20Aug2002. The code for these methods now reads:

[WebMethodAttribute()]
[SoapDocumentMethodAttribute(Binding="AYS20Aug2002")]
public string HelloWorld() {
    return "Hello World!";
}

[WebMethodAttribute()]
[SoapDocumentMethodAttribute(Binding="AYS20Aug2002")]
public string NameToString( PersonName pn ) {
    return string.Format( "{0} {1} {2}",
        pn.First, pn.Middle, pn.Last );
}

Now when we view the WSDL file for the Web service, we no longer get everything in one file. Instead, we get the following WSDL:

<?xml version="1.0" encoding="utf-8"?>
<definitions 
    xmlns:http="https://schemas.xmlsoap.org/wsdl/http/" 
    xmlns:soap="https://schemas.xmlsoap.org/wsdl/soap/" 
    xmlns:i1=
"https://msdn.microsoft.com/samples/AtYourService/2002/08/20/types" 
    xmlns:s="http://www.w3.org/2001/XMLSchema" 
    xmlns:i2=
"https://msdn.microsoft.com/samples/AtYourService/2002/08/20/ExternalWSDL" 
    xmlns:soapenc="https://schemas.xmlsoap.org/soap/encoding/" 
    xmlns:tns=
"https://msdn.microsoft.com/samples/AtYourService/2002/08/20/WebService" 
    xmlns:tm="https://microsoft.com/wsdl/mime/textMatching/" 
    xmlns:mime="https://schemas.xmlsoap.org/wsdl/mime/" 
    targetNamespace=
"https://msdn.microsoft.com/samples/AtYourService/2002/08/20/WebService" 
    xmlns="https://schemas.xmlsoap.org/wsdl/">
  <import     namespace="https://msdn.microsoft.com/samples/AtYourService/2002/08/20/ExternalWSDL"     location=    "https://localhost/AYS20Aug2002/Lockdown.asmx?schema=schema1" />  <import     namespace="https://msdn.microsoft.com/samples/AtYourService/2002/08/20/types"     location=    "https://localhost/AYS20Aug2002/Lockdown.asmx?schema=schema2" />  <import     namespace="https://msdn.microsoft.com/samples/AtYourService/2002/08/20/ExternalWSDL"     location=    "https://localhost/AYS20Aug2002/Lockdown.asmx?wsdl=wsdl1" />
  <types />
  <service name="Lockdown">
    <port name="AYS20Aug2002" binding="i2:AYS20Aug2002">
      <soap:address 
        location="https://localhost/AYS20Aug2002/Lockdown.asmx" />
    </port>
  </service>
</definitions>

Pay attention to the import elements. Two contain references to schema and one contains a reference to an external URL. By looking at the namespaces associated with the import elements, you will notice that the WSDL and schema for the XML namespace used for the binding are in two separate files. If you dig further, you would find that the binding WSDL, located at https://localhost/AYS20Aug2002/Lockdown.asmx?wsdl=wsdl1, also imports the schema at https://localhost/AYS20Aug2002/Lockdown.asmx?schema=schema1 and https://localhost/AYS20Aug2002/Lockdown.asmx?schema=schema2.

We are very close to being able to make all of these files persistent. A number of steps are still left.

Saving to Files

Right now, the WSDL returned from a query to https://localhost/AYS20Aug2002/Lockdown.asmx?wsdl returns something that is actually pretty close to perfect. It references other URLs required to figure out what the endpoint looks like, and only contains information pertinent to using the endpoint. Many endpoints based on the same WSDL could be written and deployed referencing the base WSDL file and any related XSD files. A proxy based on that same static WSDL could dynamically attach to any of those endpoints implementing the locked down WSDL file.

The first thing to do is to save the content created when going to the various URLs to some location. For my purposes, I saved everything to the same directory as the Web service. The procedure is as follows:

  1. Open up Microsoft® Internet Explorer.
  2. Navigate to https://localhost/AYS20Aug2002/Lockdown.asmx?schema=schema1.
  3. Click File, click Save As, and save the file as \Inetpub\wwwroot\AYS20Aug2002\MessageTypes.xsd.
  4. Navigate to https://localhost/AYS20Aug2002/Lockdown.asmx?schema=schema2.
  5. Click File, click Save As, and save the file as \Inetpub\wwwroot\AYS20Aug2002\DataTypes.xsd.
  6. Navigate to https://localhost/AYS20Aug2002/Lockdown.asmx?wsdl=wsdl1.
  7. Click File, click Save As, and save the file as \Inetpub\wwwroot\AYS20Aug2002\External.WSDL.

Now, it is time to do some editing.

DataTypes.XSD

The edits to this file are fairly minor. As a matter of fact, you can choose to leave the file alone and no harm will come to you. The only real change I can recommend is editing the value of the /schema/@id attribute. It has the meaningless value of schema2. I would change that to something more meaningful such as CommonDataTypes. When I'm done editing, DataTypes.xsd has the following contents:

<?xml version="1.0" encoding="utf-8" ?>
<xs:schema 
    xmlns:tns=
      "https://msdn.microsoft.com/samples/AtYourService/2002/08/20/types"  
    elementFormDefault="qualified" 
    targetNamespace=
    "https://msdn.microsoft.com/samples/AtYourService/2002/08/20/types" 
    id="CommonDataTypes" 
    xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:complexType name="PersonName">
        <xs:sequence>
            <xs:element minOccurs="0" maxOccurs="1" name="First" 
                type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" name="Middle" 
                type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" name="Last" 
                type="xs:string" />
        </xs:sequence>
    </xs:complexType>
</xs:schema>

MessageTypes.XSD

This next schema document contains all the definitions for message types used by the AYS20Aug2002 binding. To this file, I again changed the /schema/@id value to something meaningful: MessageTypes. Other than that, I left the schema definition alone.

External.WSDL

Finally, the WSDL file containing the message, portType, and binding information needs to be edited. The edits to this file are almost as simple as those for the XSD files. Namespaces get removed and file locations are hard coded. I removed the following XML namespace declarations:

  • xmlns:http="https://schemas.xmlsoap.org/wsdl/http/": I already eliminated the HTTP bindings, so this XML namespace will not be used.
  • xmlns:soapenc="https://schemas.xmlsoap.org/soap/encoding/": The Web service binding uses document/literal encoding exclusively. This namespace only applies to rpc/encoded Web services.
  • xmlns:tm="https://microsoft.com/wsdl/mime/textMatching/": This is used to create Web services that parse the contents of an HTML page. Since we are using SOAP, we do not make use of this XML namespace or its related semantics.
  • xmlns:mime="https://schemas.xmlsoap.org/wsdl/mime/": The MIME binding is not used by this document.
  • xmlns:s="http://www.w3.org/2001/XMLSchema": Because all the schema is in different documents, I do not need this XML namespace declaration either.

Once those are removed, the only remaining change is to the WSDL import elements. The two elements refer to both XSD files. These need to be changed to refer to the physical files, not to the automatically generated ones. The changed elements now look like this:

  <import 
    namespace=
"https://msdn.microsoft.com/samples/AtYourService/2002/08/20/ExternalWSDL" 
    location="https://localhost/AYS20Aug2002/MessageTypes.XSD" />
  <import 
    namespace=
"https://msdn.microsoft.com/samples/AtYourService/2002/08/20/types" 
    location="https://localhost/AYS20Aug2002/DataTypes.XSD" />

In production systems, you would want the location URI to be something other than localhost. If I were to deploy this on ColdRooster.com, the location for DataTypes.XSD would be http://www.coldrooster.com/AYS20Aug2002/DataTypes.XSD.

At this point, the WSDL and XSD files can be considered locked down.

Referencing Those files

The WebServiceBindingAttribute class contains a property called Location, which can be set by one of the classes constructors. To complete the experiment, we need to reference the file External.WSDL from the Lockdown Web service. The following declaration does just that:

[WebServiceBindingAttribute("AYS20Aug2002", 
     "https://msdn.microsoft.com/samples/AtYourService/"
     + "2002/08/20/ExternalWSDL",
     "https://localhost/AYS20Aug2002/External.WSDL")]
[WebServiceAttribute(Namespace="https://msdn.microsoft.com/samples/"
     + "AtYourService/2002/08/20/WebService")]
public class Lockdown : WebService {

This transforms the dynamically generated WSDL to the following:

<?xml version="1.0" encoding="utf-8"?>
<definitions 
    xmlns:http="https://schemas.xmlsoap.org/wsdl/http/" 
    xmlns:soap="https://schemas.xmlsoap.org/wsdl/soap/" 
    xmlns:s="http://www.w3.org/2001/XMLSchema" 
    xmlns:soapenc="https://schemas.xmlsoap.org/soap/encoding/" 
    xmlns:i0=
"https://msdn.microsoft.com/samples/AtYourService/2002/08/20/ExternalWSDL" 
    xmlns:tns=
"https://msdn.microsoft.com/samples/AtYourService/2002/08/20/WebService" 
    xmlns:tm="https://microsoft.com/wsdl/mime/textMatching/" 
    xmlns:mime="https://schemas.xmlsoap.org/wsdl/mime/" 
    targetNamespace=
"https://msdn.microsoft.com/samples/AtYourService/2002/08/20/WebService" 
    xmlns="https://schemas.xmlsoap.org/wsdl/">
  <import     namespace="https://msdn.microsoft.com/samples/AtYourService/2002/08/20/ExternalWSDL"     location="https://localhost/AYS20Aug2002/External.WSDL" />
  <types />
  <service name="Lockdown">
    <port name="AYS20Aug2002" binding="i0:AYS20Aug2002">
      <soap:address 
        location="https://localhost/AYS20Aug2002/Lockdown.asmx" />
    </port>
  </service>
</definitions>

The file now contains two fewer import elements and references the static WSDL file. We have now completed the task at hand.

Summary

By using various attributes and understanding how they work together, you can separate your WSDL into its component pieces. This example made the split this way:

  • One XSD for data types used in messages.
  • One XSD for data types that represent messages.
  • One WSDL that contains message, portType, and binding elements.
  • One WSDL that references the binding definition and contains a service element.

Of the above files, only the WSDL containing the service element is generated by the ASP.NET runtime. Everything else was saved as a persistent file. Each file has a separate purpose and may have a separate targetNamespace.

Now that you have an idea of how to get the Web service to use static XSD and WSDL files, what can you do with that? Lots! Here is just a small sampling:

  • Write your XSD first, base the objects off of that.
  • Enable your Web service to implement multiple bindings. This can come in handy when versioning the Web service.

The most common request I hear from others is how to handle writing the XSD first. Assume that you had written the contents for DataTypes.XSD before the PersonName class ever existed. People using the XSD editor in Visual Studio .NET or products such as XML Spy do this all the time. With the current incarnation of tools, you may have problems generating classes based on the XSD because of issues with XSD.EXE. Namely, XSD.EXE expects at least one element to be declared at root level that is of the type you are defining. To make the PersonName schema ready to be imported by XSD.EXE, an XSD element expression needs to be added to the document. The schema should now read:

<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema xmlns:tns=
    "https://msdn.microsoft.com/samples/AtYourService/2002/08/20/types"
    elementFormDefault="qualified" 
    targetNamespace=
"https://msdn.microsoft.com/samples/AtYourService/2002/08/20/types"
    id="CommonDataTypes" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <xsd:element name="PersonName" nillable="true"     xmlns:q1="https://msdn.microsoft.com/samples/AtYourService/2002/08/20/types"     type="q1:PersonName" />
    <xsd:complexType name="PersonName">
        <xsd:sequence>
            <xsd:element minOccurs="0" maxOccurs="1" name="First" 
                type="xsd:string" />
            <xsd:element minOccurs="0" maxOccurs="1" name="Middle" 
                type="xsd:string" />
            <xsd:element minOccurs="0" maxOccurs="1" name="Last" 
                type="xsd:string" />
        </xsd:sequence>
    </xsd:complexType>
</xsd:schema>

You will need to add one element of the XSD type for each type being expressed in the schema document. Once this is done, you can generate .NET code using the following command line:

xsd /classes /l:cs PersonData.xsd

Finally, let me answer the question "Why does XSD.EXE work this way?" When you use XSD.EXE to create classes based on schema, it assumes it's seeing every schema you're ever interested in serializing XML for. Since all XML documents start with a root element, executing XSD.EXE with the /classes switch looks for top-level elements that could serve as the root of an XML document. Note that I said "elements" here, not "types." This means that XSD.EXE looks for xsd:element instances within the schema. For each xsd:element defined in the schema, XSD.EXE will track down all types directly and indirectly referenced by the xsd:element. XSD.EXE then generates classes for only those types. If the type is not referenced by an xsd:element, no code is generated. This last bit is fairly hard to discover. My thanks go out to Alex DeJarnatt for explaining why XSD.EXE works the way it does.

 

At Your Service

Scott Seely is a member of the MSDN Architectural Samples team. Besides his work there, Scott is the author of SOAP: Cross Platform Web Service Development Using XML (Prentice Hall—PTR) and the lead author for Creating and Consuming Web Services in Visual Basic (Addison-Wesley).