Advanced Basics
Automatically Generating a Web Service
Ken Spencer

Code download available at: AdvancedBasics0301.exe (99KB)

Q My team's design pattern divides business objects between the client and the server, so I need a way to communicate between them. I can use Microsoft® .NET Remoting, but for many reasons I would prefer to use XML Web Services instead. Is there a way to automate the creation of the Web Service that communicates between the client and the server?


A I know you have decided on Web Services, but for those of you who want more information on choosing between .NET Remoting and Web Services, see Tim Ewald's MSDN® article at: ASP.NET Web Services or .NET Remoting: How to Choose). Now, to answer your question.

You could do this by using reflection to generate a Web Service that would map to the interface of an object and then create a Web Service that calls that interface. It took me a while to get this going (about four hours), but finally I made it work.

As a foundation I used code from the custom attribute application that I wrote for the installment. Let's take a walk through this code.

The interface is a simple form. The ButtonClick for the toolbar looks like this:

Select Case ToolBar1.Buttons.IndexOf(e.Button)
   Case 0 : OpenFile()
   Case 1 : GenerateWebService()
   Case Else
End Select
The GenerateWebService method shown in
Figure 1 is the high-level method that does all the work. It calls functions that generate the code and output it to the new files. The rest of the code on this form is really simple and is not shown here, but the download for this column includes the complete code (see the link at the top of this article).

GenerateWebService calls the LoadFrom method to load the assembly you select, then loops through the classes it finds. For each class, it calls the GenerateWSHeader and GenerateWSCode methods to generate the code and save it to the .asmx and .vb files. It also adds the name of the class to the textbox. The WriteFile function outputs both the .asmx and the .vb files.

Now let's look at the code in the GenRoutines module. This file contains the code that generates the functions (see Figure 2). Programs that write code are not rocket science, but do they require patience. The problem is that every character and every space must be accounted for properly. You must also remember line breaks, tabs, and other formatting so you can understand the output code. Error handling and other housekeeping should also be incorporated into the code. It requires extra work, but it's really important.

Now, let's look at the code that generates the Web Service code. The first line in the module creates a constant for the Web Service URI. This allows you to easily change the URI to the standard that your organization has adopted.

The GenerateWSHeader function outputs the header for the Web Service. The name of the class is passed to this function and is used to name the Web Service. The code isn't tricky; it's just a string handling function. The results are passed back from the function to generate the header line.

Now, let's take a look at generating the Web Service code, as shown in Figure 2. This is where the real work takes place. First, the variables are declared. This includes variables that are generally used for string handling but can also reference information about a method. For instance, to get its parameters or return value type, the variable oMI is used to reference the current method in the For Next loop. Similarly, oCN is used for a constructor's information.

The first few lines start outputting the code. As you can see, you simply build up the code statements and set the localOutput variable to that string:

localOutput = "Imports System.Web.Services" & vbCrLf
localOutput &= "<WebService(Namespace:=""" & _
    localURI & """)> _" & vbCrLf

For creating code that generates variables and references, you can set your coding standards. For instance, this line creates a variable with the name of the object reference that will be used in the Web Service:

localObjectName = "o" & t.Name

The next couple of lines set the assembly name and then extract the first entry to the left of the first comma:

localAssemblyName = t.AssemblyQualifiedName.ToString
i = InStr(localAssemblyName, ",")
localAssemblyName = Left(localAssemblyName, i - 1)
This is because the AssemblyQualifiedName method returns more information than I wanted in a comma-delimited string. To generate the correct name, you simply extract the part of the string to the left of the first comma:
localOutput &= vbTab & "Private " & _
    localObjectName & " as New " & _
    localAssemblyName & "()" & vbCrLf

The code currently outputs only a default constructor with no parameters. If you need to add custom code to the constructor, you must do it manually.

The first For Each loop creates the parameters for each method as it walks through the loop. The code also tags the method name with the WebMethod attribute. The CheckName function is called to determine if the method is one that should be output. For instance, you do not usually want to output the ToString method that is a default method of a class. You can see how the oMI variable is used to obtain the method's name, its parameters, and return type. For instance, to output the method's name, I simply use the following code:

localOutput &= "<WebMethod()> _" & vbCrLf
localOutput &= "Public Function " & _
    oMI.Name & "("

The code that is generated from this snippet will include the localReturn variable, which the Web methods use to contain the return value of the method that it calls.

The inner For Each loop (again in Figure 2) processes the parameters. This one was tricky because it must account for all parameters, their data types, and the correct syntax when multiple parameters are used.

Finally, the method's output parameter type is picked up by the following code and then it is set to the output string:

localOutput &= ") as " & _
  oMI.ReturnType.ToString  _
  & vbCrLf

The following code generates the error handling code for the Web method:

localMethodCall &= vbTab & "Catch Exc as Exception" _
    & vbCrLf
localMethodCall &= vbTab & vbTab & "Throw New Exception(""Error in "", _
    Exc.InnerException)" & vbCrLf
localMethodCall &= vbTab & "End Try" & vbCrLf & vbCrLf

The last bit of code in the outer For Each loop finishes the Web method by outputting the code to send the return value and generate the End Class statement.

For the current version of this utility, you must change the reference in the .asmx file to reflect the namespace of your project. For instance, the Class attribute generated by the utility was changed by adding the namespace for the application it was used in, like so:

<%@ WebService Language="vb"  Codebehind="Order.asmx.vb" 
    Class="WSTesterForGeneratedWS.Order" %>

The tool is finished now, but I want to back up and think about the process for a moment. Once the code looked good, I put it into a Web Service project and looked for errors. Then I fixed each problem and regenerated the Web Service again. Then I tested, made changes, and tested again. When the Web Service showed no errors in Visual Studio® .NET, I ran it in test mode and tested each method. Using this process, it was fairly easy to walk through the coding and make sure it worked.

There is another way to generate coding using the .NET Framework's System.CodeDom class. This provides an alternate approach that I will demonstrate in next month's column. I also included a simple CodeDom sample (CodeDomSample.vb) in the download files for this month. The code has also been updated to fix a few bugs since this column was written.


Send your questions and comments for Ken to basics@microsoft.com.



Ken Spencerworks for 32X Tech (http://www.32X.com), where he provides training, software development, and consulting services on Microsoft technologies.


© 2007 Microsoft Corporation and CMP Media, LLC. All rights reserved; reproduction in part or in whole without permission is prohibited.