Using UDDI at Run Time, Part II
Karsten Januszewski
Microsoft Corporation
May 2002
Summary: Shows how UDDI can be used at run-time as an abstraction layer between Web services and their clients; explores the use of WSDL as an interface definition document and how to model these documents and their implementations in UDDI. Also covers writing UDDI-aware clients that issue run-time UDDI API queries in order to dynamically bind to Web services based on a common WSDL interface definition. (25 printed pages)
Contents
Introduction
Step 1. Writing an Interface WSDL Document
Step 2: Registering the Interface WSDL Document in UDDI
Step 3: Implementing the Interface
Step 4: Register the Implementation in UDDI
Step 5: Writing the Client Polling Application
Conclusion
Introduction
In my earlier article addressing the use of UDDI at run time, the concept of run-time UDDI as a means to provide failover and recovery for Web services was introduced. That article explores a methodology of how to insert UDDI as an abstraction layer between a Web service server and its clients, allowing for a more dynamic interaction between the two. Building off that basic design pattern, this article delves deeper into that methodology and introduces more sophisticated design patterns of using UDDI at run time.
Placing a run-time layer between a Web service client and server dramatically increases the options for writing smarter, more dynamic clients. More importantly, such a design methodology reduces the need for hard-coded dependencies within Web service clients. The end point for a Web service can be abstracted from the client's code base and instead, the client can query UDDI to determine the end point. Removing these dependencies enables a number of compelling scenarios. Clients can make decisions at run-time about the end points that they invoke. These decisions can be made based on a number of different variables, depending on the metadata that has been associated with the different services.
Note that it is possible to dynamically acquire the end point for a Web service using other means, such as configuration files, database look-ups, or by making a Web service call to an ad hoc broker service. The advantages of using UDDI as this broker is that (a) UDDI has industry-wide support (unlike an ad hoc solution) and (b) UDDI supports a very flexible taxonomy-based query mechanism that allows you to select the URL based on classification schemes (e.g., physical location, cost of use, QOS guarantees). This flexibility lives at a higher layer of abstraction than lower-level protocols such as WS-Routing and WS-Referral, which alone don't provide application-constraint-based end-point selection.
To provide a more concrete scenario, imagine the following problem set. Consider a company that has several subsidiary offices and branches that it has purchased over the years. Each subsidiary uses different technology platforms and reports sales data in different formats. This means that the Home Office IT department must massage several different data formats into a common format in order to put together a single view upon the sales data. Not only is this a laborious, time-consuming process, when new companies are purchased, even more formats must be absorbed into the process of providing an aggregated sales report. The CIO of the Home Office would like to have a way to get a real-time view of sales data aggregated across all the subsidiaries and would like the solution to be flexible enough to accommodate different technology platforms from different subsidiaries as they come and go.
Web services and run-time UDDI could solve such a problem as follows: The Home Office IT Department could define a common Web service interface that all branches must implement. This interface would be registered as a tModel in a private UDDI Server hosted on Microsoft® Windows® Server 2003. The branches would be free to implement this service on any technology platform, but they would have to comply with this strongly typed WSDL interface definition. Once a branch had implemented its sales data reporting service, the branch would register the Web service in private UDDI Services, creating a binding that pointed to the WSDL tModel. At this point, the Home Office could write a client application that queried UDDI to discover all the branches implementing the given interface, and then dynamically poll each branch, aggregating the results on the fly. As subsidiaries were added or removed, the client application would not need to change because it would discover these new Web services dynamically through querying UDDI.
The remainder of this article walks through how to build such a solution. The following steps need to be completed in order to address this scenario:
- Create an interface WSDL definition.
- Register that definition in UDDI as a tModel.
- Create a set of Web services that implement the abstract WSDL interface.
- Register those implementations in UDDI and mark them correctly as given implementations of the above interface.
- Write a polling application that queries UDDI at run-time to dynamically bind to the set of Web services which support that interface.
By following through the steps that will satisfy this scenario, this article demonstrates how UDDI can be used at run-time as an abstraction layer, a design methodology that could be applied broadly to many different situations within a Web services solution framework.
Step 1. Writing an Interface WSDL Document
In order to begin, a WSDL file that is decoupled from implementation information is needed. In fact, the notion of a WSDL that acts as an immutable interface, decoupled from its end point, is key to enabling run-time UDDI scenarios. Conceptualizing WSDL as interface promotes an object-oriented paradigm between Web services, and opens up the door to designing systems that reuse interfaces.
This perspective on WSDL is slightly different than WSDL files that are auto-generated by tools. Such auto-generated WSDL files can be described as "deployment documents" that contain both interface information and end-point information. Such WSDL files are always attached to a given end point—in fact, in some cases, one cannot get an auto-generated WSDL file without knowing the end point. For example, in a .NET Web service, a WSDL file can be auto-generated by appending ?WSDL to the URL of the .asmx page, i.e. http://localhost/Service1/HelloWorld.asmx?WSDL. This auto-generated WSDL file allows a client to create a proxy for this Web service if it knows the end point of the Web service itself.
This auto-generation feature is incredibly powerful and has promoted the adoption of Web services by making the ability to create client proxies very accessible. However, this kind of WSDL usage doesn't necessarily promote interface reuse within Web services. By conceptualizing WSDL as a pure interface definition as opposed to a deployment document, the possibility of a single WSDL file that has several implementations is made manifest. Because WSDL files are not required to have the <service> entity included, a WSDL file that contains no end-point information is perfectly valid. Consequently, you can approach WSDL files similar to IDL files in COM: as a pure interface definition that can be reused.
So, how do you write an interface WSDL file? You could certainly open Notepad (or whatever XML editor is preferred) and start writing the WSDL by hand. An alternate approach is to employ the System.Web.Services.Description classes in the .NET Framework to create WSDL programmatically, and then serialize the result to a file. A third option is to tweak a WSDL file that has already been auto-generated in order to produce a reusable interface document. This approach is not unlike using the Active Template Library (ATL) to generate an IDL file, and then tweaking and altering that IDL file. We'll use this approach for the purposes of this article. I will also look at how .NET Web services generate WSDL files and some options available in the Web.config file for modifying this behavior.
Let's begin by creating a very simple Web service. In fact, we will use the same Web service used in the prior UDDI run-time article for reporting sales data. It has one function, GetSalesTotalByRange, which passes two DateTime arguments and returns a double. You can get started by creating a new Web service application in Visual Studio .NET called SalesReportUSA and saving the code below as SalesReport.asmx into the application. Note that this Web service does not used the code-behind feature, but rather contains the entirety of the code for the Web service in the .asmx file. The previous article used C# for its code samples, so for the sake of balance, this article will use Microsoft Visual Basic— .NET.
<%@ WebService Language="vb" Class="SalesReportUSA.SalesReport" %>
Imports System
Imports System.Web.Services
Namespace SalesReportUSA
<WebService([Namespace] := "urn:myCompany-com:SalesReport-Interface")> _
Public Class SalesReport
Inherits System.Web.Services.WebService
<WebMethod()> Public Function GetSalesTotalByRange _
(startDate As System.DateTime, endDate As System.DateTime) As Double
Return 5000.0
End Function
End Class
End Namespace
When you browse this file with Microsoft Internet Explorer, you can click the Service Description file to take you to http://localhost/SalesReportUSA/SalesReport.asmx?WSDL. The auto-generated WSDL yields the following:
<?xml version="1.0" encoding="utf-8"?>
<definitions xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:s="http://www.w3.org/2001/XMLSchema"
xmlns:s0="urn:myCompany-com:SalesReport-Interface"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/"
xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"
targetNamespace="urn:myCompany-com:SalesReport-Interface"
xmlns="http://schemas.xmlsoap.org/wsdl/">
<types>
<s:schema elementFormDefault="qualified"
targetNamespace="urn:myCompany-com:SalesReport-Interface">
<s:element name="GetSalesTotalByRange">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1" name="startDate"
type="s:dateTime" />
<s:element minOccurs="1" maxOccurs="1" name="endDate"
type="s:dateTime" />
</s:sequence>
</s:complexType>
</s:element>
<s:element name="GetSalesTotalByRangeResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1"
name="GetSalesTotalByRangeResult" type="s:double" />
</s:sequence>
</s:complexType>
</s:element>
<s:element name="double" type="s:double" />
</s:schema>
</types>
<message name="GetSalesTotalByRangeSoapIn">
<part name="parameters" element="s0:GetSalesTotalByRange" />
</message>
<message name="GetSalesTotalByRangeSoapOut">
<part name="parameters" element="s0:GetSalesTotalByRangeResponse" />
</message>
<message name="GetSalesTotalByRangeHttpGetIn">
<part name="startDate" type="s:string" />
<part name="endDate" type="s:string" />
</message>
<message name="GetSalesTotalByRangeHttpGetOut">
<part name="Body" element="s0:double" />
</message>
<message name="GetSalesTotalByRangeHttpPostIn">
<part name="startDate" type="s:string" />
<part name="endDate" type="s:string" />
</message>
<message name="GetSalesTotalByRangeHttpPostOut">
<part name="Body" element="s0:double" />
</message>
<portType name="SalesReportSoap">
<operation name="GetSalesTotalByRange">
<input message="s0:GetSalesTotalByRangeSoapIn" />
<output message="s0:GetSalesTotalByRangeSoapOut" />
</operation>
</portType>
<portType name="SalesReportHttpGet">
<operation name="GetSalesTotalByRange">
<input message="s0:GetSalesTotalByRangeHttpGetIn" />
<output message="s0:GetSalesTotalByRangeHttpGetOut" />
</operation>
</portType>
<portType name="SalesReportHttpPost">
<operation name="GetSalesTotalByRange">
<input message="s0:GetSalesTotalByRangeHttpPostIn" />
<output message="s0:GetSalesTotalByRangeHttpPostOut" />
</operation>
</portType>
<binding name="SalesReportSoap" type="s0:SalesReportSoap">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http"
style="document" />
<operation name="GetSalesTotalByRange">
<soap:operation soapAction="urn:myCompany-com:SalesReport-Interface
/GetSalesTotalByRange" style="document" />
<input>
<soap:body use="literal" />
</input>
<output>
<soap:body use="literal" />
</output>
</operation>
</binding>
<binding name="SalesReportHttpGet" type="s0:SalesReportHttpGet">
<http:binding verb="GET" />
<operation name="GetSalesTotalByRange">
<http:operation location="/GetSalesTotalByRange" />
<input>
<http:urlEncoded />
</input>
<output>
<mime:mimeXml part="Body" />
</output>
</operation>
</binding>
<binding name="SalesReportHttpPost" type="s0:SalesReportHttpPost">
<http:binding verb="POST" />
<operation name="GetSalesTotalByRange">
<http:operation location="/GetSalesTotalByRange" />
<input>
<mime:content type="application/x-www-form-urlencoded" />
</input>
<output>
<mime:mimeXml part="Body" />
</output>
</operation>
</binding>
<service name="SalesReport">
<port name="SalesReportSoap" binding="s0:SalesReportSoap">
<soap:address location="http://localhost/salesreportusa/salesreport.asmx" />
</port>
<port name="SalesReportHttpGet" binding="s0:SalesReportHttpGet">
<http:address location="http://localhost/salesreportusa/salesreport.asmx" />
</port>
<port name="SalesReportHttpPost" binding="s0:SalesReportHttpPost">
<http:address location="http://localhost/salesreportusa/salesreport.asmx" />
</port>
</service>
</definitions>
First, note how the auto-generated WSDL created three different bindings for the Web service: one for HTTP-POST, one for HTTP-GET and one for SOAP. .NET facilitates this in order to provide the auto-generated HTML form that appears when a user browses to the access point of the Web service itself, in this case SalesReport.asmx. This HTML page allows a user to invoke the Web service by passing parameters either through a query string or an HTTP POST, and .NET then renders the result, which is displayed in Internet Explorer. This ability to invoke the Web service through browsing its access point is a very convenient mechanism provided by .NET to help users get a sense of the Web service and how it works.
However, there might be cases where this facility would not be appropriate. Perhaps the custodian of this Web Service only wants to provide a binding through SOAP exclusively. Or, in our case with our goal of writing an interface WSDL document that only uses SOAP, the other bindings are not appropriate.
If one wanted to disallow such behavior, the Web.config file of the project can be altered to disable this feature. Or, one could turn off this feature globally across all Web projects by modifying the Machine.config file. By adding the following XML within the <system.web> entity, HTTP-POST and HTTP-GET on .asmx files will be turned off:
<webServices>
<protocols>
<remove name="HttpPost" />
<remove name="HttpGet" />
</protocols>
</webServices>
By making this alteration, the .asmx page will still auto-generate a helper page; however, it will no longer allow a user to invoke the Web service through an HTTP GET or an HTTP POST. The auto-generated WSDL will also reflect this modified behavior. Let's take a look at the newly generated file. Note how there is now only one operation and binding, as opposed to three.
<?xml version="1.0" encoding="utf-8"?>
<definitions xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:s="http://www.w3.org/2001/XMLSchema"
xmlns:s0="urn:myCompany-com:SalesReport-Interface"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/"
xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"
targetNamespace="urn:myCompany-com:SalesReport-Interface"
xmlns="http://schemas.xmlsoap.org/wsdl/">
<types>
<s:schema elementFormDefault="qualified"
targetNamespace="urn:myCompany-com:SalesReport-Interface">
<s:element name="GetSalesTotalByRange">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1" name="startDate"
type="s:dateTime" />
<s:element minOccurs="1" maxOccurs="1" name="endDate"
type="s:dateTime" />
</s:sequence>
</s:complexType>
</s:element>
<s:element name="GetSalesTotalByRangeResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1"
name="GetSalesTotalByRangeResult" type="s:double" />
</s:sequence>
</s:complexType>
</s:element>
</s:schema>
</types>
<message name="GetSalesTotalByRangeSoapIn">
<part name="parameters" element="s0:GetSalesTotalByRange" />
</message>
<message name="GetSalesTotalByRangeSoapOut">
<part name="parameters" element="s0:GetSalesTotalByRangeResponse" />
</message>
<portType name="SalesReportSoap">
<operation name="GetSalesTotalByRange">
<input message="s0:GetSalesTotalByRangeSoapIn" />
<output message="s0:GetSalesTotalByRangeSoapOut" />
</operation>
</portType>
<binding name="SalesReportSoap" type="s0:SalesReportSoap">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http"
style="document" />
<operation name="GetSalesTotalByRange">
<soap:operation soapAction="urn:myCompany-com:SalesReport-Interface
/GetSalesTotalByRange" style="document" />
<input>
<soap:body use="literal" />
</input>
<output>
<soap:body use="literal" />
</output>
</operation>
</binding>
<service name="SalesReport">
<port name="SalesReportSoap" binding="s0:SalesReportSoap">
<soap:address location="http://localhost/salesreportusa
/salesreport.asmx" />
</port>
</service>
</definitions>
While this auto-generated WSDL is cleaner that what we started with, it still does not address our initial requirement of creating an interface WSDL file that decouples from implementation details and reuses existing schema. Notice how there is still a <service> entity (which has been highlighted in bold) within the document, which specifies where to call the service.
This information remains because WSDL files are designed to contain two main pieces of information: the interface signature for this Web service (which would include the XML schema for payload of the SOAP as well as request/response behavior that all messages must comply with) and the end point for the Web service itself. Such a WSDL file contains everything a client needs to invoke the Web service and acts as deployment document, as discussed above.
However, our goal is write a pure interface WSDL file. Such a modification can be easily made: simply remove the deployment information from the WSDL file. By saving the auto-generated WSDL as a file called salesreport.wsdl and then tweaking that file by deleting the entirety of the <service> entity, one now has a pure Web service interface description.
Also notice how the schema for our Web service is embedded in the WSDL file. Another enhancement to our WSDL file that could be made is to extract the embedded schema (also depicted in bold) out of the WSDL, and using the <import> directive within WSDL to pull the schema in from elsewhere. This is quite useful in several situations. A schema might have uses outside of just the Web service. For example, the schema might be used for database storage information or for validating XML that comes into the system through means other than a Web service. By importing the schema rather than embedding it, the schema can have a lifetime outside of the WSDL and, thus, maintenance and reuse is achieved.
By removing the <service> information and importing the schema information, our final interface WSDL document would look as follows:
<?xml version="1.0" encoding="utf-8"?>
<definitions xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:s="http://www.w3.org/2001/XMLSchema"
xmlns:s0="urn:myCompany-com:SalesReport-Interface"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/"
xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"
targetNamespace="urn:myCompany-com:SalesReport-Interface"
xmlns="http://schemas.xmlsoap.org/wsdl/">
<import namespace="urn:myCompany-com:SalesReport-Interface"
location="salesreport.xsd"/>
<types/>
<message name="GetSalesTotalByRangeSoapIn">
<part name="parameters" element="s0:GetSalesTotalByRange" />
</message>
<message name="GetSalesTotalByRangeSoapOut">
<part name="parameters" element="s0:GetSalesTotalByRangeResponse" />
</message>
<portType name="SalesReportSoap">
<operation name="GetSalesTotalByRange">
<input message="s0:GetSalesTotalByRangeSoapIn" />
<output message="s0:GetSalesTotalByRangeSoapOut" />
</operation>
</portType>
<binding name="SalesReportSoap" type="s0:SalesReportSoap">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http"
style="document" />
<operation name="GetSalesTotalByRange">
<soap:operation soapAction="urn:myCompany-com:SalesReport-Interface
/GetSalesTotalByRange" style="document" />
<input>
<soap:body use="literal" />
</input>
<output>
<soap:body use="literal" />
</output>
</operation>
</binding>
</definitions>
We would place the schema in the same directory as the WSDL file, so that the import command knows where to fetch the schema. The accompanying schema would look as follows:
<?xml version="1.0" encoding="utf-8" ?>
<s:schema xmlns="urn:myCompany-com:SalesReport-Interface"
xmlns:s="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
targetNamespace="urn:myCompany-com:SalesReport-Interface">
<s:element name="GetSalesTotalByRange">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1" name="startDate"
type="s:dateTime" />
<s:element minOccurs="1" maxOccurs="1" name="endDate"
type="s:dateTime" />
</s:sequence>
</s:complexType>
</s:element>
<s:element name="GetSalesTotalByRangeResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1"
name="GetSalesTotalByRangeResult" type="s:double" />
</s:sequence>
</s:complexType>
</s:element>
</s:schema>
</types>
With this new set of documents, a pure interface now exists for usage by multiple implementations.
A question then arises: how do you prevent users from continuing to generate client proxies based on auto-generated WSDL? How can you lead users to the custom written, interface WSDL? Consider our existing salesreport.asmx file. It exists as an implementation of salesreport.wsdl file, yet it is still generating its own WSDL. What if the desired behavior is for Web service access points to simply be SOAP listeners, and not provide any documentation information? Or, what if we want to write our own documentation when users attempt to browse our Web service access point?
.NET handles this through further tweaking of the Web.config file. If no documentation is desired and the sought-after behavior is for access points to be just SOAP access points, the following change can be made:
<customErrors mode="On" defaultRedirect="error.htm"/>
<webServices>
<protocols>
<remove name="HttpPost" />
<remove name="HttpGet" />
<remove name="Documentation" />
</protocols>
</webServices>
Turning on custom errors is not required, but it does present something friendlier to the user. Without turning on customErrors, .NET will throw an exception if someone tries to browse an .asmx file.
If you would like to present a custom page any time a user tries to access the Web service end point through an HTTP-GET, change the Web.config file as follows:
<webServices>
<protocols>
<remove name="HttpPost" />
<remove name="HttpGet" />
</protocols>
<wsdlHelpGenerator href="documentation.htm"/>
</webServices>
Such a documentation page might discuss how to implement this abstract interface, how to find implementations of the interface and how to write clients for the interface. In fact, the documentation page would be a perfect place to direct the user to UDDI.
Step 2: Registering the Interface WSDL Document in UDDI
At this point, we have completed step one, albeit through a rather circuitous route! The next step will be much less involved. Remember, given our scenario, each branch office will need to use this WSDL file to generate instances of this Web service. How will they find the document?
This is where using UDDI at design-time comes into play. By registering this WSDL file—as well as all interface WSDL documents within an organization—as a tModel in UDDI, a common registry of interfaces is made available. Given our scenario, it would not make sense for these interfaces to be available to the public at large, so the use of an internal UDDI service, as opposed to the public UDDI Business Registry, would be more appropriate. UDDI Services is provided in Microsoft Windows Server 2003 as an optional add-in and serves our purposes perfectly. More information about UDDI Services in Windows Server 2003 is available.
Once UDDI Services is installed on a Windows Server 2003, one can register WSDL files through either the Web user interface that comes with UDDI Services or programmatically using the UDDI .NET SDK. For more on registering information in UDDI, as well as a code sample in Visual Basic .NET and C#, see Web Service Description and Discovery in UDDI, Part I.
After being registered, the Salesreport.wsdl file has a UDDI entry marked by a globally unique identifier generated by UDDI. This identifier can then be used programmatically to retrieve both the URL location of the WSDL file as well as any metadata (description information, classification information) that has been associated with the WSDL file in UDDI. In fact, this identifier will be critical in our polling application written in Step 5, because we will use it to find all the implementations of this given interface.
At this point, a developer could now discover this WSDL file by searching UDDI through a Web user interface and issuing either categorical or keyword searches. This design-time usage of UDDI calls out its importance as a centralized place to find and use the WSDL files.
Step 3: Implementing the Interface
Recall that in order to generate our WSDL file, we started with a Web service (an .asmx file) and then worked our way backward, ultimately ending up with an abstract WSDL interface document. Let's take a look now at the opposite approach: implementing a Web service based on an existing WSDL file. In fact, this approach is central to our scenario, in which each subsidiary branch will have to implement the interface dictated by the Home Office.
To provide a .NET implementation of this Web service, we will need to use a tool available in the .NET Framework SDK called WSDL.exe. We cannot use Add Web Reference through Visual Studio .NET to achieve this task because it only generates client proxies. In this case, we need a server stub that we can use as a base for our implementation. From a C++ perspective, this is like using MIDL.exe to generate classes based on an IDL file or, from a Visual Basic perspective, using the Implements directive to create classes based on a type library.
As such, you launch the Visual Studio .NET command prompt by clicking Program Files, and then clicking Tools (or, alternately, open a command line shell and browse to <install directory>\Microsoft Visual Studio .NET\FrameworkSDK\Bin). If you type wsdl without any switches, you will see a list of options available for use with the tool. In this case, we will need to use the /server switch, which will generated an abstract stub class as opposed to a proxy class. Adding some other switches for the output file and programming language preference, we end up with the following:
wsdl.exe /out:c:\salesreport_stub.vb /language:vb /server
http://localhost/salesreportusa/salesreport.wsdl
Once we have this stub class, we can implement our Web service. To implement this abstract class, first create a new Web service project. In this case, the virtual directory will be named SalesReportEurope, because, in our scenario, the European division is implementing the interface discovered in UDDI. Once the new project has been created, the abstract class, salesreport_stub.vb, should be added to the project. Then, a new Web service file can be written called Service1.asmx that inherits from the abstract class. Instead of Service1 inheriting from System.Web.Services.WebService, we will inherit from our stub class generated by WSDL.exe, SalesReportSoap:
Public Class Service1
Inherits SalesReportSoap
Visual Studio .NET provides help in determining the methods that need to be overridden, as well as providing the requisite attributes and method signatures.
By selecting the methods that need to be overridden from the drop-down, a developer can complete the stub. In our case, we will complete it as follows:
<System.Web.Services.WebMethodAttribute(),
System.Web.Services.Protocols.SoapDocumentMethodAttribute
("urn:myCompany-com:SalesReport-Interface/GetSalesTotalByRange",
RequestNamespace:="urn:myCompany-com:SalesReport-Interface",
ResponseNamespace:="urn:myCompany-com:SalesReport-Interface",
Use:=System.Web.Services.Description.SoapBindingUse.Literal,
ParameterStyle:=System.Web.Services.Protocols.
SoapParameterStyle.Wrapped)> _
Public Overrides Function GetSalesTotalByRange _
(ByVal startDate As Date, ByVal endDate As Date) As Double
Return 8000.0
End Function
The developer doesn't have to worry about correctly attributing the Web service; the necessary WebMethodAttribute is provided by the tools. Rather, the developer only needs to perform the necessary actions to retrieve the data based on the input parameters. Of course, our sample returns static data and a real-world application would make a database call of some sort. Also note that we probably want to alter the Web.config file of this new Web project, based on the recommendations made earlier in this article.
Step 4: Register the Implementation in UDDI
Now that one of the subsidiaries has written an implementation of the interface WSDL document, the implementation needs to be correctly registered in UDDI. Paramount to correctly registering this service is the need to signify within UDDI that this service is in fact compliant with the WSDL interface. This is done by creating a tModelInstanceInfo entry beneath the binding of this service that properly references the tModel key of the WSDL interface that was registered in Step 2. (For more information about modeling and registering Web Services in UDDI, see Web Service Description and Discovery in UDDI, Part I.
Within UDDI Services in Windows .NET Server Beta 3, such an entry might look like that in Figure 1:

Figure 1. Example of a tModelInstanceInfo entry
Notice how the access point for the Web service has an interface pointer to the WSDL it implements, visually represented by the classic UML interface lollipop symbol with a pointer arrow. We can also see the tModel key, a GUID which can be used within a UDDI API call to represent that interface.
If each different subsidiary branch registers its implementation and correctly points to the same tModel, one can issue a UDDI API call to find all the implementations of a given interface definition.
This call is provided when UDDI passes the tModelKey in a tModelBag within a find_business, find_service or find_binding API call. The XML for such a find_service call would look as follows in XML:
<find_service businessKey="" generic="1.0" xmlns="urn:uddi-org:api">
<tModelBag>
<tModelKey>uuid:f1bff5d1-c53d-4279-ab13-02b51889c611</tModelKey>
<tModelBag>
</find_service>
Note that we are passing an empty businessKey attribute, because we want to search across all businesses and providers to find any implementations of this WSDL file. We could pass an actual businessKey and narrow our search to only query for services underneath a given business or provider.
You could also issue this query through the Web user interface. In such a way, at design time, the CIO of the Home Office could discover which branches have a working implementation up and running that use the sales reporting interface WSDL document.
Things get most interesting, however, when programmatically issuing such a query in our client polling application using the UDDI .NET SDK, which leads us to Step 5.
Step 5: Writing the Client Polling Application
We can now pull our scenario together. We need to write an application that (1) queries UDDI to determine all the branches that have implemented the interface and (2) generates a report that invokes all the active instances of this Web service. In this case, we will write a Visual Basic .NET Windows Form for the purposes of reporting the data.
To begin, create a new Windows application in Visual Basic .NET. Download the UDDI .NET SDK and add it as a reference to the project. Then, add the following Imports directives to the top of the project:
Imports Microsoft.Uddi Imports Microsoft.Uddi.Binding Imports Microsoft.Uddi.Business Imports Microsoft.Uddi.Service Imports Microsoft.Uddi.ServiceType Imports System.Configuration
Before coding any further, we will create an App.config file to store information about the UDDI Services we will be accessing and about the tModelKey GUID that we are interested in.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="UDDI_URL" value="http://localhost/uddipublic/inquire.asmx" />
<add key="tModelKey" value="uuid:f1bff5d1-c53d-4279-ab13-02b51889c611" />
</appSettings>
</configuration>
Next, we initialize globally scoped variables for the form with values from the App.config file. We will also initialize a DataTable used later to display information:
Private InquiryURL As String = ConfigurationSettings.AppSettings("UDDI_URL")
Private SalesReportTModel As String = ConfigurationSettings.AppSettings("tModelKey")
private DataTable dt = new DataTable("SalesReport");
We can create some controls on the form, including two groupBoxes, a checkedListBox, a dataGrid, two dateTimePickers, a label, and two buttons, such that we have the following shown in Figure 2:

Figure 2. Creating controls on the Windows Form
Next, we are going to create a custom class that we will use to store information returned by UDDI in our checkedListBox. It looks as follows:
'storage class for UDDI data
Public Class UDDIInfo
Private m_name As String
Private m_accesspoint As String
Private m_serviceKey As String
Public Property Name() As String
Get
Return m_name
End Get
Set(ByVal Value As String)
m_name = value
End Set
End Property
Public Property Accesspoint() As String
Get
Return m_accesspoint
End Get
Set(ByVal Value As String)
m_accesspoint = value
End Set
End Property
Public Property ServiceKey() As String
Get
Return m_serviceKey
End Get
Set(ByVal Value As String)
m_serviceKey = value
End Set
End Property
End Class
Before adding the code to our On_Click event handlers, we first need to take care of some code for when our form loads:
Public Sub New()
MyBase.New()
' This call is required by the Windows Form Designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call
' set UDDI inquiry location
Inquire.Url = InquiryURL
' set up data grid
dt.Columns.Add("Name")
dt.Columns("Name").Caption = "Name"
dt.Columns("Name").ColumnName = "Name"
dt.Columns.Add("Access Point")
dt.Columns("Access Point").Caption = "Access Point"
dt.Columns("Access Point").ColumnName = "Access Point"
dt.Columns.Add("Sales")
dt.Columns("Sales").Caption = "Sales"
dt.Columns("Sales").ColumnName = "Sales"
DataGrid1.DataSource = dt
DataGrid1.ReadOnly = True
' uses name property of UDDIInfo in display
CheckedListBox1.DisplayMember = "Name"
End Sub
We then can fill the Button1 _Click event handler with code that will query UDDI for all the services that are implemented given a tModelKey. First, we will call the find_service API with a tModelBag containing the tModelKey that represents the WSDL interface. This will give us a list of serviceInfos that support that interface. In UDDI, because each service can contain multiple access points, we then need to call find_binding on each service, again passing the tModelKey. The results from the find_binding calls will contain the correct access points. We will store the access point information, as well as some other display information, to our custom UDDI object and then add that object to the checkedListBox.
Private Sub Button1_Click(ByVal sender As System.Object,
ByVal e As System.EventArgs) Handles Button1.Click
Cursor.Current = Cursors.WaitCursor
'clear checkbox
CheckedListBox1.Items.Clear()
' do a findService with the tModelKey
' this will return a list of services that implement
' the tModel -- we will then have to iterate over
' that list and make additional calls to UDDI
Dim fs As New FindService()
' pass the interface tModel key
fs.TModelKeys.Add(SalesReportTModel)
' pass an empty BusinessKey so that we search across all providers
' in the UDDI Services registry
fs.BusinessKey = ""
Dim sl As New ServiceList()
Try
sl = fs.Send()
Catch ue As UddiException
MessageBox.Show(ue.StackTrace)
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
' now that we have the list of services that do in fact implement
' that tModel, we can issue a find_binding,
' passing the appropriate serviceKey
Dim fbind As New FindBinding()
Dim bindd As New BindingDetail()
fbind.TModelKeys.Add(SalesReportTModel)
Dim si As ServiceInfo
For Each si In sl.ServiceInfos
fbind.ServiceKey = si.ServiceKey
Try
bindd = fbind.Send()
Catch ue As UddiException
MessageBox.Show(ue.Message)
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
'grab the accessPoints
Dim bt As BindingTemplate
For Each bt In bindd.BindingTemplates
'create a UDDIInfo custom class
Dim ui As New UDDIInfo()
ui.Accesspoint = bt.AccessPoint.Text
ui.Name = si.Name
ui.ServiceKey = si.ServiceKey
'add it to the collection
CheckedListBox1.Items.Add(ui, True)
Next bt
Next si
Label1.Text = "UDDI Cache Updated With Latest Access Points From UDDI"
Cursor.Current = Cursors.Default
End Sub
At this point, the first half of our application is complete. In terms of caching, we are using an in-memory cache to store the information collected from the UDDI API queries. This cache will live throughout the lifetime of the application. For a more persistent cache, we could serialize this data out to an XML file, so that when the user launches the application, the set of access points are pulled from a file and UDDI does not need to be re-queried.
Regardless of the technique used, caching the results from UDDI is critical to any run-time UDDI scenario. The calls made to UDDI are relatively expensive and should only be made when absolutely necessary.
Now, we can complete the second half of our application: invoking the Web services based on a common interface. In order to code this piece of the application, we will first need to use Add Web Reference or WSDL.exe to generate a proxy class for our application. By either browsing to UDDI to find the URL of the file or typing in the file name itself, we can generate a proxy class based on http://localhost/SalesReportUSA/salesreport.wsdl. If you take a look at the code generated for the proxy, you will notice that there is no URL property set of the class. We will be setting the URL property at run-time instead.
Now that we have a generic proxy classes based on the interface WSDL document, we can write the code that dynamically invokes each Web service. When the user clicks the Generate Report button, the code will loop through the checked listbox, extracting the information from each UddiInfo object, and dynamically insert the correct access point into the proxy class.
Private Sub Button2_Click _
(ByVal sender As System.Object,
ByVal e As System.EventArgs) Handles Button2.Click
Cursor.Current = Cursors.WaitCursor
' clear the datagrid
dt.Clear()
' create an instance of our proxy class
Dim salesReport As New localhost.SalesReportSoap()
' create some variables for our report
Dim figure As Double = 0
Dim total As Double = 0
Dim arr(2) As String
'iterate through checked list box
Dim i As Integer
Dim ui As New UDDIInfo()
For i = 0 To CheckedListBox1.Items.Count - 1
' only use the checked boxes
If CheckedListBox1.GetItemChecked(i) Then
' extract UDDI object from list
ui = CType(CheckedListBox1.Items(i), UDDIInfo)
Try
' alter the proxy class, setting the URL property
salesReport.Url = ui.Accesspoint
' call the method to invoke the web service
figure = salesReport.GetSalesTotalByRange _
(DateTimePicker1.Value, DateTimePicker2.Value)
' update our total
total += figure
' add some properties to the array and insert the array into the datagrid
arr(0) = ui.Name
arr(1) = ui.Accesspoint
arr(2) = figure.ToString()
dt.Rows.Add(arr)
Catch err As Exception
MessageBox.Show _
(ui.Accesspoint + " failed:" + err.StackTrace + " ")
End Try
End If
Next i
' add a final row to the grid with the total aggregated information
arr(0) = "Total:"
arr(1) = ""
arr(2) = total.ToString()
dt.Rows.Add(arr)
End Sub
Notice how we can instantiate the proxy one time and then reuse that object again and again, simply setting a new URL property on the proxy for each access point we have.
Conclusion
This article demonstrates how UDDI can be used at run time as an abstraction layer between Web service clients and servers. In order to do so, it is essential to think of WSDL documents as interface definitions as opposed to deployment documents. By using WSDL as an interface definition, we created multiple Web services that shared a common interface. Then, by registering those Web services properly in UDDI, we took advantage of UDDI at run time to dynamically bind to Web services based on UDDI API calls. Also note that minimal UDDI queries resulted from caching the results of our query.
This article only scratches the surface on the possibilities of using UDDI as broker between clients and servers. Future articles will delve into this concept further, looking at how classification schemes can be applied to UDDI data, creating an ontology within a UDDI registry that can be used to reify data by run-time UDDI queries. The possibilities and scenarios enabled by this design methodology are unlimited as run-time queries based on these classification schemes are issued.