Advanced Basics

Automatically Generating Proxy Classes

Ken Spencer

Code download available at:AdvancedBasics0302.exe(76 KB)

QThe Web Services generator shown in the January 2003 Advanced Basics column left me wondering if I could create a tool that automatically generates proxy classes for an assembly. Is there any support in the Microsoft® .NET Framework for me to do this directly without using brute-force string concatenation for the entire process?

QThe Web Services generator shown in the January 2003 Advanced Basics column left me wondering if I could create a tool that automatically generates proxy classes for an assembly. Is there any support in the Microsoft® .NET Framework for me to do this directly without using brute-force string concatenation for the entire process?

AYes, you can and as you'll see, it's a good example of the flexibility and power of the .NET Framework. Not only can you dynamically load applications (including those that use Windows® Forms), but you can generate code on the fly, compile it, then load it while the application is running.

AYes, you can and as you'll see, it's a good example of the flexibility and power of the .NET Framework. Not only can you dynamically load applications (including those that use Windows® Forms), but you can generate code on the fly, compile it, then load it while the application is running.

This functionality is provided by the Code Document Object Model (CodeDOM) namespace in the .NET Framework. The CodeDOM allows you to create various elements that represent the code you want to generate and then links them together in what is known as a code graph. The CodeDOM generates the code from this code graph. Let's walk through an example to see how.

The new application I'll create allows you to point at an assembly and then automatically generate a proxy class from that assembly. I used the Web Services generator from last month's column as the basis for this application, changing the code generation routines. You will notice that there's less code in this new application than there was in the previous column and that I concatenate many fewer strings.

In the previous application, the user would press a button, which would execute a call to GenerateWebService to generate the code. This button has been rewired to call the GenerateProxyClass subroutine shown in Figure 1. This code loads the assembly specified in sFileName, then loads the public types found in it by calling GetExportedTypes. Then the For Each loop processes each class by calling GenerateClassCode. Note that to this procedure I pass not only the type that references the assembly, but also the folder path for the generated code. This allows the GenerateClassCode function to actually write the output files, making this class quite reusable. For instance, you can call this function and pass in a reference to a class and a folder path, and it will generate the new proxy class for you.

Figure 1 GenerateProxyClass Subroutine

Sub GenerateProxyClass() Dim t As Type Dim othis As Object Dim oAssembly As [Assembly] Dim arrayOfTypes() As Type StatusBar1.Text = "" If sFileName = "" Then Exit Sub End If Try oAssembly = [Assembly].LoadFrom(sFileName) arrayOfTypes = oAssembly.GetExportedTypes() For Each t In arrayOfTypes If t Is Nothing Then MsgBox("Type was not found — exiting") Exit Sub End If txtClassesGenerated.Text &= t.Name & vbCrLf 'Generate a class If GenerateClassCode(t, txtOutputPath.Text) Then StatusBar1.Text = _ "Class Generation Complete" End If Next Catch exc As Exception StatusBar1.Text = "File " & sFileName & _ "error occurred. " & exc.Message Finally oAssembly = Nothing GC.Collect() End Try End Sub

Now let's look at the code in the GenRoutines module in Figure 2. The first step in creating this module is to define the Imports. In addition to standard imports like System and System.IO, there are three key imports you need to take advantage of for the CodeDOM features, plus an import for the Reflection namespace: System.CodeDom, System.CodeDom.Compiler, Microsoft.VisualBasic, and System.Reflection.

Figure 2 GenRoutines Module

Imports System Imports System.IO Imports System.Reflection Imports System.CodeDom Imports System.CodeDom.Compiler Imports Microsoft.VisualBasic Module GenRoutines Public Function GenerateClassCode(ByVal t As Type, _ ByVal CodePath As String) As Boolean Dim localCompileUnit As New _ CodeCompileUnit() Dim localNamespace As New _ CodeNamespace(t.Namespace & "Client") Dim localNewType As New _ CodeTypeDeclaration(t.Name & "Client") Dim localNewConstructor As New CodeConstructor() Dim localMethodInfo As MethodInfo Dim localSnippet As CodeSnippetStatement Dim localVBcodeProvider As New VBCodeProvider() Dim gen As ICodeGenerator = _ localVBcodeProvider.CreateGenerator() Dim localMethodCall As String Dim localObjectName As String Dim bReturn As Boolean Dim localAssemblyName As String Dim localOutput, localObjectRef As String Dim i As Integer Dim localTempString As String Dim bAtLeastOne As Boolean = False Dim tabLevel1 As String = vbTab Dim tabLevel2 As String = vbTab & vbTab Dim tabLevel3 As String = vbTab & vbTab & vbTab Dim tabLevel4 As String = vbTab & vbTab & vbTab & vbTab Dim sw As New _ StreamWriter(CodePath & "\" & t.Name & _ "Client.vb", False) localObjectName = "o" & t.Name localAssemblyName = t.AssemblyQualifiedName.ToString i = InStr(localAssemblyName, ",") localAssemblyName = Left(localAssemblyName, i - 1) localObjectRef &= tabLevel1 & _ "Dim " & localObjectName & " as New " & _ localAssemblyName & "()" & vbCrLf Try localNamespace.Imports.Add(New _ CodeNamespaceImport("System.IO")) localCompileUnit.Namespaces.Add(localNamespace) localNewConstructor.Attributes = _ MemberAttributes.Public localNewConstructor.Name = _ t.GetConstructor(System.Type.EmptyTypes).Name localNewType.Members.Add(localNewConstructor) ' For Each localMethodInfo In _ t.GetMethods(( _ BindingFlags.Public Or BindingFlags.Instance _ Or BindingFlags.DeclaredOnly Or _ BindingFlags.InvokeMethod)) localMethodCall = "" localOutput = "" If localMethodInfo.Attributes = _ MethodAttributes.Public Then If localMethodInfo.ReturnType.ToString = _ "System.Void" Then bReturn = False End If Dim localNewMethod As New CodeMemberMethod() Dim localMethodParams As String = "" localNewMethod.Name = localMethodInfo.Name localNewMethod.Attributes = _ MemberAttributes.Public localNewMethod.ReturnType = New _ CodeTypeReference( _ localMethodInfo.ReturnType.FullName) localTempString = "" localMethodCall = "Try" & vbCrLf If Not bReturn Then localMethodCall &= tabLevel3 & _ localObjectName & "." & _ localMethodInfo.Name Else localMethodCall &= tabLevel3 & _ "localReturn = " & localObjectName _ & "." & localMethodInfo.Name End If bAtLeastOne = False Dim locaParameter As ParameterInfo For Each locaParameter In _ localMethodInfo.GetParameters() localNewMethod.Parameters.Add(New _ CodeParameterDeclarationExpression( _ locaParameter.ParameterType, _ locaParameter.Name)) 'Build string for method call output If bAtLeastOne Then localTempString = "," localTempString &= " _ " & vbCrLf _ & tabLevel4 Else localTempString = "(" End If localTempString &= locaParameter.Name If localTempString <> "" Then bAtLeastOne = True localMethodParams &= localTempString End If Next locaParameter localNewType.Members.Add(localNewMethod) localSnippet = New _ CodeSnippetStatement(localObjectRef _ & vbCrLf) localNewMethod.Statements.Add(localSnippet) localSnippet = Nothing localMethodCall &= localMethodParams If bReturn Then localSnippet = New _ CodeSnippetStatement(_ "Dim localReturn as " & _ localMethodInfo.ReturnType.ToString _ & vbCrLf) localNewMethod.Statements.Add(localSnippet) localSnippet = Nothing If localMethodParams = "" Then localMethodCall &= "()" & vbCrLf & _ vbCrLf Else localMethodCall &= ") as _ " & localMethodInfo.ReturnType.ToString _ & vbCrLf & vbCrLf End If Else If localMethodParams = "" Then localMethodCall &= "()" & vbCrLf & vbCrLf Else localMethodCall &= ")" & vbCrLf & vbCrLf End If End If localMethodCall &= tabLevel1 & _ "Catch Exc as Exception" & vbCrLf localMethodCall &= tabLevel2 & _ "Throw New Exception( _ ""Error in "", _ Exc.InnerException)" & vbCrLf localMethodCall &= tabLevel1 & "End Try" _ & vbCrLf & vbCrLf If bReturn Then localMethodCall &= tabLevel1 & "Return " _ & "localReturn" & vbCrLf End If localOutput = localMethodCall & vbCrLf localSnippet = New _ CodeSnippetStatement(localOutput) localNewMethod.Statements.Add(localSnippet) End If Next localMethodInfo localNamespace.Types.Add(localNewType) gen.GenerateCodeFromCompileUnit( _ localCompileUnit, sw, _ New CodeGeneratorOptions()) sw.Close() Catch exc As Exception Throw New Exception(exc.Message) End Try Return True End Function End Module

The GenerateClassCode function starts out by defining a number of variables. Let's take a look at the variables that are directly part of the CodeDOM namespace. The first is localCompileUnit, which represents the new class I'm going to create. This is the only element that's used during code generation. The next variable is localNamespace and, as you might guess, it represents a namespace. Its name is created by using the name of the namespace of the class being generated and appending "Client" or "Proxy" to it.

Next, I define localNewType (of type CodeTypeDeclaration); this represents the class I'm going to create. The constructor to CodeTypeDeclaration takes the name of the class which, like the namespace, has "Client" appended to it.

The next three variables define elements of the code I will generate. The localNewConstructor variable (of type CodeConstructor) represents the constructor for the class. The localMethodInfo variable (of type MethodInfo) represents the detail attributes of the method. The localSnippet variable (of type CodeSnippetStatement) represents code that you want to add to the generated class. This variable is used to build strings of code that can be dropped into the output by adding the snippet to a method.

The variable localVBcodeProvider (of type VBCodeProvider) defines a reference to an instance of the VBCodeProvider that allows you to generate code in Visual Basic®. (There is also a C# provider for those who are so inclined.) Finally, the variable gen (of type ICodeGenerator) provides a reference that will be used later to actually generate the code. The other variables are standard types that are used for strings or other generic purposes.

Also in Figure 2, the six statements just before the Try block create a StreamWriter object that will be used to write the code file later and to create a string (localObjectRef) that defines the reference original class.

The first two statements in the Try block add the Imports statement to the Imports collection, then add the namespace to localCompileUnit.Namespace. You could add any number of Imports statements in this manner. The next three statements create the constructor and then add it to the Members collection of the new type (the new class).

The bulk of the work is performed in the For Each loop that loops through each of the methods. The first If statement makes sure you're looking at a Public method. The second checks to see if the return value is System.Void, which indicates that the method is a subroutine and not a function. If this is true, a Boolean variable is set to restrict generation of the output code.

The next statement is a reference to a new object of type CodeMemberMethod, which represents the method I'm going to generate. The statement that follows sets the method's name by pulling the name of the method referenced by localMethodInfo. Then this statement sets the attributes of the method to Public:

localNewMethod.Attributes = MemberAttributes.Public

You can set more than one attribute if necessary by anding them together. Next, the return type of the method is set by accessing the ReturnType property of localMethodInfo.

Now it's time to generate the code within the class using one of two approaches. You can either determine elements of the CodeDOM to use and construct everything this way or you can create and add code snippets, which are simply strings representing your code. The advantage of the first approach is that it works across different Microsoft .NET-compliant languages. The second approach is usually language-specific, but it allows you to quickly reuse code you may already have in your arsenal. This is the approach I used most frequently in my example.

The localMethodCall variable stores the code that will be output within the method. The If block sets up the correct call to the original classes method, which will depend on whether or not the method returns data.

The inner For Each block loops through the method's parameters, performing two tasks. The first statement in the block adds the current parameter to the method definition (localNewMethod). The rest of the code adds the parameters to the original method call of the class. When the loop has finally completed, localMethodParams will contain all of the parameters for the method call, properly delimited.

After the inner loop ends, the localNewMethod object is added to the Members collection of the new type. Next, a new instance of the CodeSnippetStatement class is created with the string required to instantiate the original class. Then the snippet is added to the Statements collection of the new type. The rest of the code in the outer loop builds the balance of code for the method and adds the code snippet(s) to the new type.

After this loop ends, the new type is added to the Types collection of the namespace with this statement:

localNamespace.Types.Add(localNewType)

Now comes the fun part. The next statement takes the compile unit containing your namespace and the string writer, and then generates this code:

gen.GenerateCodeFromCompileUnit(localCompileUnit, sw, _ New CodeGeneratorOptions())

Pretty sweet. The only bits of code generated with strings are the code snippets for the code's method. If I didn't use any code snippets, I could have allowed an option to generate code for different languages. Note that this sample application is extensible. For instance, right now it does not add the public properties of the original class to the proxy class, but it could.

As you can see, it's easy to automate many tasks using the .NET Framework. In fact, automating various parts of the application creation process will allow you to build applications in record time, yet still build the types of user interface, middle tier, and other customizations that you require.

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

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