July 19, 2002
This column addresses a common problem with Microsoft® Visual Studio® .NET Web service development: sharing data types across Web services. This issue arises when a developer creates a set of Web services with what appears to be well-thought-out portTypes and data types. Then, things quickly go awry when creating a client for that Web service. How? Two Web services use the exact same type. When developing the client, the developer dutifully uses "Add Web Reference" to create the proxies. Sprinkle in a little code that calls a Web method and returns a custom type.
Later, the same type is sent to another Web service. If the defaults are used, the data type, known to be the same for both Web services, cannot be used with both Web services. The code won't even compile to allow you to send the data. One thing causes this problem: the code is mapping from a CLR type to an XSD type, then back to a CLR type. A bunch of assumptions wind up hurting you, the developer. This column will take a look at what those assumptions are and show how to work around them.
Setting the Stage
We need some simple example that demonstrates the problem. To that end, we will have two Web services that expose a Name structure. Name will contain the following information:
- Unique identifier
- First name
- Middle name
- Last name
All items are strings. This is a simple representation of the data in Microsoft® Visual Basic® .NET:
Public Class Name Public ID As String = "" Public First As String = "" Public Middle As String = "" Public Last As String = "" End Class
Two Web methods that are a part of two larger portTypes are GetName and AddNameToList. GetName returns a Name based on some ID. AddNameToList will add the name to some list of names—no need to get into why, let's just say that it does. To finish setting the stage, let's show what the Web service code looks like for both services:
<WebService(Namespace:= _ "http://msdn.microsoft.com/columns/" & _ "AtYourService/2002/07/Service1.asmx")> _ Public Class Service1 Inherits System.Web.Services.WebService <WebMethod()> _ Public Function GetName(ByVal id As String) As Name Dim retval As New Name() retval.ID = id retval.First = "Scott" retval.Middle = "Christopher" retval.Last = "Seely" Return retval End Function End Class
<WebService(Namespace:= _ "http://msdn.microsoft.com/columns/" & _ "AtYourService/2002/07/Service2.asmx")> _ Public Class Service2 Inherits System.Web.Services.WebService <WebMethod()> _ Public Sub AddNameToList(ByVal theName As Name) ' Do nothing End Sub End Class
As good Web service developers do, we declared a special namespace for the Web services. The URI is unique and does not reference tempuri.org. (Whenever a Web service does reference tempuri.org, the default page for the .ASMX will warn all who view the page that the developer is using the default namespace for Microsoft® ASP.NET Web services.) The stage is set, and the curtains are ready to go up. Have we gone far enough to be able to share the Name type between Service1 and Service2? Sadly, no. The reason why: XML serialization issues.
Serialization of Name
How will the Name type be serialized when it is translated from a CLR type to XML? To answer that question, let's take a look at the XSD representation of Name within the Service1 and Service2 WSDL files. According to Service1.asmx?WSDL, the schema for Name looks like this:
<s:complexType name="Name"> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="ID" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="First" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="Middle" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="Last" type="s:string" /> </s:sequence> </s:complexType>
Not too surprisingly, Service2.asmx?WSDL concurs with this representation. And yet, the two services disagree on something fundamental about the definition of the Name type: they are defined in different XML Schema (XSD) targetNamespaces. Each data type defined using XSD exists in a particular namespace, as defined by the schema's targetNamespace attribute. When instances of the type are represented as XML, they are qualified by the targetNamespace name. If a CLR data type like Name does not explicitly declare which XML namespace it belongs to, ASP.NET will define the data type in the same XML namespace as the Web service. This is the default behavior.
For Service1, that namespace is http://msdn.microsoft.com/columns/AtYourService/2002/07/Service1.asmx. For Service2, the namespace is http://msdn.microsoft.com/columns/AtYourService/2002/07/Service2.asmx. As a result, the underlying Web service uses the one Name class but generates two XSD types, each in a different targetNamespace. As a result, Service1 and Service2 do not share the same Name data type—despite the fact that they use the same CLR type, it's the XSD type that really matters. Have no fear; this can be fixed. "How?" you ask. Tell Microsoft® .NET what namespace to use when mapping the Name type to XSD.
To tell .NET what XML namespace to use, you need to give the environment a small hint. System.Xml.Serialization contains a number of attribute classes that let you change the way a given data type is serialized to an XML document. One of the most basic things you can do is to state the XML namespace to use when the data type is serialized. System.Xml.Serialization.XmlType does just that. To set the XML namespace for Name to http://msdn.microsoft.com/columns/AtYourService/2002/07/Name, make Name look like this:
Imports System.Xml.Serialization <XmlType(Namespace:= _ "http://msdn.microsoft.com/columns/" & _ "AtYourService/2002/07/Name")> _ Public Class Name Public ID As String = "" Public First As String = "" Public Middle As String = "" Public Last As String = "" End Class
Now, when ASP.NET generates the WSDL for Service1.asmx and Service2.asmx it will show the exact same XSD for Name in both instances. Since both Web services place the data type in the same namespace and have a shared representation, it only stands to reason that this type should be shareable between proxies, right? Wrong!
To try sharing these types, I created a small console application. All that this application will do is to attempt to invoke Service1 and Service2 using the same version of Name. The first thing I did was use Add Web Reference from within Visual Studio .NET to create the proxies. The Solution Explorer reflects this addition as shown in Figure 1.
Figure 1. Web references to the two Web services: Service1.asmx and Service2.asmx
When Add Web Reference adds a new Web service, it places the proxy in a separate CLR namespace. The namespace is typically named [project namespace].[machine name]. For the local machine, this is [project namespace].localhost. When extra Web references are added for other Web services located on the same machine name, the environment will automatically append a numeral onto the end of the initial namespace. So, the second Web reference will be in the [project namespace].localhost1 namespace, the third will be in [project namespace].localhost2, and so on. This keeps all the data types and other information segregated. It also means that the Name data type is forcibly different on the client side because of a CLR namespace issue. As a result, the following code will not even compile:
Sub Main() Dim svc1 As New localhost.Service1() Dim svc2 As New localhost1.Service2() Dim theName As localhost.Name theName = svc1.GetName("1234") svc2.AddNameToList(theName) End Sub
Specifically, I get the following error for the line svc2.AddNameToList(theName):
Value of type 'AYS07162002_console.localhost.Name' cannot be converted to 'AYS07162002_console.localhost1.Name'.
All is not lost. We just need a way to fix things.
The Fix is In
As with most problems, there are a number of ways to fix this and wind up with the desired results. It all depends on what you want to do. Let's take a look at two simple fixes. One can be done from within Visual Studio .NET, the other from the command line. Neither solution is very hard to use. The end result of both methods will be that the Web service proxies live in the same namespace.
Within Visual Studio .NET
When expanding the nodes under the two Web References, we have the layout shown in Figure 2.
Figure 2. The Web References in the console application
The really easy thing to do in this instance is to edit the proxy for localhost1. To see it in the Solution Explorer, make sure that the Show All Files button is selected as shown in Figure 3.
Figure 3. The Show All Files button
Now, you should be able to see a file located at localhost1/Reference.map/Reference.vb. Open it up; we are about to make some minor changes. First, change the namespace declaration to read Namespace localhost. Second, delete the declaration of the Name class. That class is already declared in the proxy for Service1.asmx. Finally, edit the Main function to read as follows:
Sub Main() Dim svc1 As New localhost.Service1() Dim svc2 As New localhost.Service2() Dim theName As localhost.Name theName = svc1.GetName("1234") svc2.AddNameToList(theName) End Sub
That's it. The bad news is that if you ever update the Web references, you will need to implement these changes again.
I only presented the previous solution because a lot of people like to use the development environment. I have found that relying on Add Web Reference will bite back more often than not. If you want to reduce the chances of getting scarred, use WSDL.EXE directly. It only gets called when you ask it to and never does anything behind your back.
First, delete the two Web references from the project. You've just decided to abandon that approach. Next, open up the Visual Studio .NET command prompt. (This is available from the Windows Start menu: Start→All Programs→Microsoft Visual Studio .NET→Visual Studio .NET Tools→Visual Studio .NET Command Prompt.) Navigate to a directory that you can find easily, like c:\temp. Once there, invoke WSDL.EXE to generate the proxies for you. On my machine, I ran the following two commands (each command appears as one command line):
wsdl /l:VB /n:proxy /o:Service1.vb http://localhost/AYS07162002/Service1.asmx
wsdl /l:VB /n:proxy /o:Service2.vb http://localhost/AYS07162002/Service2.asmx
What these lines do is invoke WSDL.EXE, tell it to generate a proxy in Visual Basic .NET, and place that proxy in the CLR namespace called proxy. The file should be stored as either Service1.vb or Service2.vb. With the proxies generated, you can add them to the consuming application. To do this, go to the Solution Explorer and select the console application node. Right-click and select Add, Add Existing Item. Navigate to c:\temp and select both Service1.vb and Service2.vb and click Open. To get rid of the duplicate instances of the Name class, open either Service1.vb or Service2.vb and delete the declaration of the class. Finally, edit Main so that it calls the appropriate classes (now in the proxy namespace, just to mix things up).
Sub Main() Dim svc1 As New proxy.Service1() Dim svc2 As New proxy.Service2() Dim theName As proxy.Name theName = svc1.GetName("1234") svc2.AddNameToList(theName) End Sub
Once again, the one Name data type can be used across the two Web services. Cool!
In order to share a data type across two or more Web services, the Web services themselves must agree on two items:
- The structure of the data type, when represented as XML.
- The fully qualified name of any XSD data types in use.
This is done to make sure that the XML representation of the data type is the same for any SOAP endpoint. Once this agreement has been made, a .NET client needs to make sure that the generated proxies that share the data type also share the same CLR namespace. A declaration for the shared data type can only appear once, so the developer will need to delete all but one declaration of that type. Once you know what is going on, it is fairly trivial to use a common data type across Web services.
At Your Service