Export (0) Print
Expand All
Debugger Visualizations, Garbage Collection
Use the Visual Studio 2005 Bootstrapper to Kick-Start Your Installation
CLR Inside Out: Improving Application Startup Time
Unit Testing Tips: Write Maintainable Unit Tests That Will Save You Time And Tears
Program Customized Testing Environments Without Trashing Your Machine
Bridge the Gap Between Development and Operations with Whitehorse
Visual Studio 2005: Create Reusable Project And Item Templates For Your Development Team
Expand Minimize

Calling an Arbitrary Web Service

Visual Studio 2005
 

Scott Golightly, Microsoft Regional Director
Keane, Inc.

April 2006

Applies To:
   Visual Studio 2005
   Web Services

Summary: Test Web Services quickly and more efficiently without having to write full-blown applications each time by using .NET reflection and the CodeDOM to dynamically generate a proxy to a Web Service. (10 printed pages)

Contents

Introduction
Dynamic Proxy Library
A Sample Windows Application
Conclusion

Introduction

As companies move more to using Web services for application integration and Service Oriented Architecture (SOA) the number of Web services inside an organization will naturally grow. The need to manage these services as well as testing the Web service in development before the client is written has proved to be an issue. There are many free and commercial products to help fill this need. These tools all generate a proxy to the Web service to send a message to the Web service. This article looks at a method of using reflection and the code DOM to dynamically generate a proxy to a Web service. An application is also created that will allow the user to specify a WSDL file, choose a Web method, specify parameters, and then see the results returned by the proxy class.

As most experienced Web developers know, ASP.NET will generate a Web page explaining how to call the Web service when you navigate to an ASP.NET Web service. If you are accessing the Web service from the machine that hosts the Web service and the parameters are all simple types, a page like the one in Figure 1 will be created that also contains a form that lets you call the Web service and view any result that is returned. This page can come in handy when you need to determine if the Web service is working correctly. Unfortunately this has some problems as the page uses HTTP and will bypass any custom SOAP handlers. If you cannot get direct access to the machine (as is common in most production environments) then the generated page will not have the form to call the Web service. A developer could open Visual Studio 2005 and use the "add web reference" functionality to create a proxy to the Web service and in a few minutes create a simple application to call the Web service. As your organization uses more Web services managing all of the different applications, to call an individual Web service can become a configuration and management nightmare. One solution is to have a single application that will allow you to call many different Web services and to provide the correct parameters. There are several tools that will allow you to not only call a Web service, but also monitor and manage it. Regardless of whether you eventually choose to use the ASP.NET page or a tool, the secret to calling a Web service is to generate the proxy to the Web server at run time. In the rest of this article I will show a class library that will create a proxy for a Web service specified in a WSDL (Web Services Description Language) file at runtime. Using this class library I am able to call many different Web services from the same application. I will also show a Windows application that will use the library to get information about a Web service and allow you to call it.

Click here for larger image

Figure 1. ASP.NET Web Service Test Page (Click on the image for a larger picture)

Dynamic Proxy Library

In order to separate the functionality of calling the Web service from the user interface I decided to create a class library that generates a proxy to handle all of the interaction with the Web service that I want to call. The class library will also allow me to reuse this functionality in many different applications. To interact with the Web service the class library that generates the dynamic proxy needs to be able to perform five basic functions. Those functions are to parse a valid WSDL file, generate a proxy class for the Web service, return a list of Web methods supported by the Web service, enumerate the parameters for a Web method, and finally allow the user to call the method on the Web service.

To simplify coding later on I decided to have the constructor of my class take the URI of the WSDL file as a parameter. By limiting each instance of the class to a single URI, I avoid having to reset member variables if the user decides to call a different Web service. The URI is stored in a private variable for later use. The constructor also sets up a ServerCertificateValidationCallback that will handle any issues with invalid SSL certificates. The constructor for my class looks like this:

Public Sub New(ByVal uri As Uri)
    _uri = uri
    ServicePointManager.ServerCertificateValidationCallback = _ 
    New RemoteCertificateValidationCallback(AddressOf SSLResult)
End Sub

I found that many development Web services (and some production sites) use SSL certificates that are self signed or that have expired. When using a Web browser this might cause a security dialog like the one in Figure 2 to appear. With a Web service call there is no user interface to allow the user to decide if the certificate should be trusted so the caller must programmatically decide whether to accept certain certificate errors. The ServerCertificateValidationCallback lets you determine which class of errors to ignore by returning true for that type of error. In this implementation of ServerCertificateValidationCallback all certificate errors are ignored.

Public Function SSLResult(ByVal sender As Object, _
ByVal c As System.Security.Cryptography.X509Certificates.X509Certificate, _
ByVal chain As System.Security.Cryptography.X509Certificates.X509Chain, _
ByVal sslPolicyErrors As System.Net.Security.SslPolicyErrors) As Boolean
    Return True
End Function

Aa730835.callwebserv02(en-US,VS.80).gif

Figure 2. Internet Explorer Security Alert

Parsing the WSDL

Once the foundation for communication with the Web service is set up, the next step is to get some basic information about the Web service from the WSDL file. To do this I create a Web request and pass in the URI to the WSDL file. The WebRequest class will handle all the details of parsing and validating the WSDL file. Calling the GetResponse method on the WebRequest object gets a stream that can be passed to the shared Read method of the ServiceDescription object. The read method returns a ServiceDescription object that has information about the Web service defined in the WSDL file.

Dim webReq As WebRequest = WebRequest.Create(_uri)
Dim reqStrm As Stream = webReq.GetResponse.GetResponseStream()
_serviceDesc = ServiceDescription.Read(reqStrm)
_serviceName = _serviceDesc.Services(0).Name

The name of the Web service will be needed for several other method calls in the class library so I store it in a local variable.

Generate the Proxy Class

Now that I have a description of the Web service I can generate the proxy class that will allow me to call the methods of the Web service. The purpose of generating the proxy class is to create the same code that would be put into a reference.vb or refernce.cs file when you add a Web reference in Visual Studio and then compile that code into an assembly that we can use immediately from the application that calls our class library.

The first step is to create an instance of the ServiceDescriptionImporter class. The purpose of the ServiceDescriptionImporter class is to allow you to easily import the information in a WSDL file into a CodeDom.CodeCompileUnit. After creating the ServiceDescriptionImporter I call the AddServiceDescription method and pass it in the ServiceDescription object that contains the information about the Web service I want to call. I also set the ProtocolName property on the ServiceDescriptionImporter object to request that I communicate with the Web service using SOAP. I also set the CodeGenerationOptions property to instruct the CodeDom to generate properties for any simple data types exposed by the Web service.

Dim servImport As New ServiceDescriptionImporter
servImport.AddServiceDescription(_serviceDesc, String.Empty, String.Empty)
servImport.ProtocolName = "Soap"
servImport.CodeGenerationOptions = CodeGenerationOptions.GenerateProperties

In version 1.0 and 1.1 of the .NET Framework pair of methods for asynchronous calls would be generated for each Web method. The pair of methods would be named with a begin and an end before the method name. In version 2.0 of the .NET Framework there is an option to create events to invoke asynchronous methods. To generate the asynchronous methods with the begin/end pair use the CodeGenerationOption.GenerateOldAsync. To use events to invoke asynchronous methods use CodeGenerationOption.GenerateNewAsync. Since I only want to call synchronous methods I will not specify either option.

Next I want to generate a CodeDom CodeCompileUnit tree. The CodeCompileUnit class provides a container to store the program graph in. After adding a namespace to the CodeCompileUnit I import the service description into it.

Dim ns As New CodeNamespace
Dim ccu As New CodeCompileUnit
ccu.Namespaces.Add(ns)
Dim warnings As ServiceDescriptionImportWarnings
warnings = servImport.Import(ns, ccu)

I check the ServiceDescriptionImportWarnings. If the import produced either a NoCodeGenerated warning or a NoMethodsGenerated warning then I stop processing the data since there will be nothing for the user to call later on.

If the import was successful then I generate an instance of a CodeDomProvider (in the code that follows it is a Visual Basic provider but could just as well be C# since the languages are interoperable). I then generate the code for the proxy class using the GenerateCodeFromNamespace method. The first parameter is the namespace to use. In the second parameter I provide a StringWriter that will store the code so I can compile it into an assembly later on. It would be possible to write the code in the StreamWriter to a file if you wanted to store it for later inspection. The third parameter is for options such as indenting nested blocks of code, whether to place the opening brace of a block on the same line as the start of the block or on a different line, and other formatting options. Since I will not be directly viewing the code I do not bother to set any of the options.

Dim sw As New StringWriter(CultureInfo.CurrentCulture)
Dim prov As New VBCodeProvider
prov.GenerateCodeFromNamespace(ns, sw, Nothing)

The last step in generating the assembly is to create a set of compiler parameters and then invoke the compiler on the code that is stored in the StringWriter. The constructor for the CompilerParameters class takes an array of strings as an argument. The strings in the array represent DLLs that should be referenced by the compiler. I then set the GenerateExecutable property to false so it will generate a class library. I set the GenerateInMemory property to true since I do not need to persist the proxy assembly to disk. The next two lines of code instruct the complier to only break on serious errors. I next create a CompilerResults object to store the results of comiling the code. From the CompilerResults I am able to access the CompiledAssembly property, which is the compiled proxy assembly. I store the assembly in a local variable for later use.

Dim param As New CompilerParameters(New String() _
{"System.dll", "System.Xml.dll", _
"System.Web.Services.dll", "System.Data.dll"})
param.GenerateExecutable = False
param.GenerateInMemory = True
param.TreatWarningsAsErrors = False
param.WarningLevel = 4
Dim results As New CompilerResults(Nothing)
results = prov.CompileAssemblyFromSource(param, sw.ToString())
_proxyAssembly = results.CompiledAssembly

List WebMethods

Now that I have an assembly that lists the public properties and methods of the Web service I can use reflection to retrieve the information about the methods on the class. All of the methods that appear in the proxy class are methods that have been decorated with the WebMethod attribute in the original code. This class library has a method that will return an array of MethodInfo objects to the program using it.

I first get a reference to the class in the proxy assembly that represents the Web service. I then call GetMethods to get the Web methods.

Dim service As Type
service = _proxyAssembly.GetType(_serviceName)
Return service.GetMethods(BindingFlags.DeclaredOnly Or _
BindingFlags.IgnoreCase Or BindingFlags.Instance Or _
BindingFlags.InvokeMethod Or BindingFlags.Public)

Enumerate Parameters

For the class library to be useful it must also provide the ability to enumerate the parameters for a given method on the proxy class. I created a simple function that takes the name of the Web method and will return an array of ParameterInfo objects. The code to accomplish this is shown below.

I first get the proxy to the Web service as a type and then call GetMethod to get a reference to the method I want to call. I am then able to call GetParameters to return the ParameterInfo array.

Dim serviceType As Type = _proxyAssembly.GetType(_serviceName)
Return serviceType.GetMethod(methodName).GetParameters()

Invoke Method

Once the user of the class library has identified a Web method and the parameters on that method they would probably like to call the Web method. To call the Web method I need to first instantiate the proxy class. I do that by calling Activator.CreateInstance and pass it the type information for the Web service. I then get a MethodInfo object for the method that the user wants to call and return the results of calling the Invoke method on the proxy for that Web method. I have to pass the Object array of parameters that the Web method is expecting as the second parameter to the Invoke method.

Dim assemblyType As Type = _proxyAssembly.GetType(_serviceName)
Dim instance As Object = Activator.CreateInstance(assemblyType)
Dim methodInfo As MethodInfo = assemblyType.GetMethod(methodName)
Return methodInfo.Invoke(instance, params)

By returning an object as either a single object or an array of objects, this library differs from the Web page generated by ASP.NET, which shows the XML message returned by the Web service. You can use the tracing capability of Microsoft Web Service Extensions (WSE) to capture the actual messages being sent between the client and Web server. A full discussion of WSE is beyond the scope of this article.

Return Remote Type

While testing the class library I came across a Web method that required a parameter that was an enumeration. I had to get an instance of the enumeration to parse the value supplied by the user. I added a method to my class library to return a Type object for any one of the objects in the proxy assembly. The method takes as a parameter the name of the type that the user would like returned. The method then finds and returns that type from the proxy assembly.

Return _proxyAssembly.GetType(typeName)

Code Access Security

This class library is dynamically generating an assembly and then instantiating it and calling code in the assembly. It requires full trust permissions in order to do that. I have added the following line to the AssemblyInfo.vb file to ensure that the class library is never instantiated with less than full trust permissions.

<Assembly: PermissionSet(SecurityAction.RequestMinimum,Name:="FullTrust")> 

I also need to make sure that any calling program also acquires full trust permissions before instantiating the class library so I will add a similar line to the AssemblyInfo file of any program that uses this class library.

That completes the functionality of the class library that will interact with the Web service. In the next section I will demonstrate a sample application that you can use to call an arbitrary Web service.

A Sample Windows Application

For a sample application that will use the class library I created the Windows application shown in Figure 3. The application contains a text box for entering the URI to the WSDL file. This could be a file that resides on the hard drive or the URL to a Web service that is half way around the world. There is a DataGridView control to show the methods on the Web service and another DataGridView to display the parameters of the currently selected method. I have also added a RichTextBox control to display the data returned from the Web method. Finally, there are two buttons: the first will instantiate the class library and retrieve the method and parameter information, and the second will invoke the Web method using the values the user has provided for the parameters. The rest of this article describes the implementation details of this application.

Click here for larger image

Figure 3. Windows Application to Call An Arbitrary Web Service (Click on the image for a larger picture)

Configuring the GridView Controls

As you recall when providing information about methods and parameters, the class library returns an array of MethodInfo or ParameterInfo objects, respectively. The data binding features in the .NET Framework allow us to bind a DataGridView to the array, and it will generate a row for each element in the array and a column for each public property on the class. While this works very well, it tends to make a cluttered interface with a lot of information that is not needed for this application. When the form loads I call a method to display only the necessary columns in each DataGridView. To display just the columns that I want I first set the AutoGenerateColumns property of the DataGridView to false. I then create a new DataGridViewTextBoxColumn, and set some properties for that column. The properties allow me to set many of the visual attributes of the column, like its header text value, whether the text can be changed, the minimum width that the column can be sized to, and the initial width of the column. The most important property is the DataPropertyName. This property tells the column which property from the object it will be displaying. After I have set all the properties that I need to for the column I add it to the Columns collection of the DataGridView.

The following code shows setting the properties for the first column of the DataGridView that displays method information. The column will have a header of "Return Type" and will show the type of data returned by the Web method.

dgvMethods.AutoGenerateColumns = False
Dim newColumn As New DataGridViewTextBoxColumn
newColumn = New DataGridViewTextBoxColumn
With newColumn
    .Name = "returntype"
    .DataPropertyName = "returntype"
    .HeaderText = "Return Type"
    .ReadOnly = True
    .MinimumWidth = 100
   .Width = 300
End With
dgvMethods.Columns.Insert(0, newColumn)

I repeat the process for each of the columns that I want to display. For the column that allows the user to provide a value for the parameter in a Web method I do something a little different. I want the column to be editable. For a column in a DataGridView to be editable, the column must have the ReadOnly property value set to false, and the underlying data store must be updateable, as well. The ParameterInfo class doesn't have a writeable property for storing the value. To allow the user to add a value I create a DataGridTextBoxColumn and also set ReadOnly to false. I do not set the DataPropertyName property, so it is not bound to a property on the ParmameterInfo object. When this column is added to the DataGridView it will be editable.

newColumn = New DataGridViewTextBoxColumn
With newColumn
    .Name = "ParamValue"
    .HeaderText = "Value"
    .ReadOnly = False
    .MinimumWidth = 100
    .Width = 398
End With
dgvParameters.Columns.Insert(2, newColumn)

Retrieving the Methods

After the form has loaded, users will enter the path to the WSDL file. They will then click the button labeled "Get Service" to retrieve information about the methods that are available on the Web service. The first thing I do is instantiate the class library and pass in the URI entered by the user. I then retrieve the array of MethodInfo objects that are the public interface for the Web server. I could just bind that array to the DataGridView.

Retrieving the Parameters

I set up an event handler so that every time a different method is selected, the corresponding parameter information is shown in the DataGridVeiw that shows parameters. This is easily accomplished by retrieving the name of the current method and passing it to the GetParameters method on the class library. I can bind the ParameterInfo array directly to the DataGridView.

Dim methodName As String = dgvMethods.CurrentRow().Cells("name").Value
dgvParameters.DataSource = _wsp.GetParameters(methodName)

Generating the Parameter Array

Once the user has entered in values for all of the parameters, they must be converted from a string back into the data type that the Web method is expecting, and then packaged as an array of objects to send off to the Web method. I use a function to convert the parameter type from its string representation in the DataGridView into the type that the Web method is expecting. If the parameter is a string I return the value passed in since it is already a string. If the parameter is a primitive type (Int32, Boolean, Double, and so on) then I call ChangeType to convert the parameter to the correct type. If the parameter is an enumeration I use the GetRemoteType method on the class library to retrieve the definition for the enumeration and call the Parse method to convert the text representation of the enumeration into the correct value. Finally, if the parameter is an object or other complex data type, I throw an exception. I probably could have done some more work to use reflection and figure out how to instantiate an object and get its public methods, but most of the Web services that I have used do not have objects as parameters so I haven't spent the time to investigate how much work this would be.

Private Function ConvertParameterDataType(ByVal paramValue As String, _
ByVal pi As ParameterInfo) As Object
    If paramValue Is Nothing Then Return Nothing
    If pi.ParameterType.FullName = "System.String" Then
        Return paramValue
    End If
    If pi.ParameterType.IsPrimitive Then
        Return Convert.ChangeType(paramValue, pi.ParameterType)
    End If
    If pi.ParameterType.IsEnum Then
        Dim enumType As Type = _wsp.GetRemoteType(pi.ParameterType.Name)
        Return System.Enum.Parse(enumType, paramValue, True)
    End If
    Throw New ArgumentException( _
    "Unable to convert parameter to a simple type.", pi.Name)
End Function

Calling the Web Service

To call the Web service I merely need to call the Invoke method on the class library. It takes the name of the method and an object array as parameters and returns an object back.

When the results are returned I check to see if the object is Nothing (null in C#), and if so write a message to the user that the call succeeded but did not return any results. If the result is an array I use a For Each loop to call ToString() on each element of the array and add it to the text in the RichTextBox control. For most objects this will print out the class name. If the return value is not an array I add its value to the RichTextBox control. This will show the actual value of the object.

Conclusion

While not a perfect replacement for the page generated by ASP.NET, the class library shown in this article will allow you to call most Web services. The class library has the advantage of using SOAP to communicate with the Web service and not be restricted to the local machine. This application does not have all the functionality of many commercial applications for managing Web services, but it does show how they are able to call any Web service. I have used this application successfully during development and testing to call various Web services. It has helped me when looking into exceptions logged by a production Web service to determine if the exception was caused by invalid parameters. By harnessing the power of reflection and the CodeDOM in the .NET framework, it is relatively easy to generate a proxy for an arbitrary Web service.

 

About the author

Scott Golightly is Microsoft Regional Director and a Senior Principal Consultant with Keane, Inc. in Salt Lake City. He has over 13 years experience helping his clients design and build systems that meet their business needs. When Scott is not working he enjoys fishing, camping, hiking, and spending time with his family. You can reach Scott at Scott_J_Golightly@keane.com.

Show:
© 2014 Microsoft