Article Options
Premium Sponsor
Premium Sponsor

 »  Home  »  Windows Development  »  CodeDOM: How to achieve code generation in .NET - Part 1
CodeDOM: How to achieve code generation in .NET - Part 1
by Paul Nichols | Published  01/29/2003 | Windows Development | Rating:
Paul Nichols

Based in the UK I've been an IT contractor for over 5 years specialising in VB.NET and C# both on both Windows and Web based projects. Please feel free to contact me at mail@pandesolutions.co.uk or see my online CV at www.pandesolutions.co.uk

View all articles by Paul Nichols...

CodeDOM: How to achieve code generation in .NET - Part 1

Article source code: codedom_1.zip

Hopefully over the following weeks these articles will, if nothing else, bring the possibility of generating code with .NET to your attention. More than that though I hope you will actually find a use for The CodeDOM and follow these articles to generate code and complied assemblies. Along the way I'll try to highlight some pitfalls that I have become aware of though being part of a company that has VSIP membership. The code samples will be in VB, the terminology in the most part will be VB orientated too. The generated code will be in both languages though, demonstrating the main use of the CodeDOM. Sorry C# developers, I thought it would make a change!

Requirements

  1. Visual Studio.NET Professional, or Enterprise Architect Edition
  2. VB.NET or C#.NET Programming Knowledge

CodeDOM Overview

Before reading this document you may want to brush up on some of the concepts and terminology found at the following sites:

System.CodeDom Namespace
CodeDOM Quick Reference

This week is simply an introduction, as I've already said hopefully you will find a use for the CodeDOM which will make any following articles more useful.

Introduction to the Code DOM

To start with I will explain some of the advantages of the CodeDOM and times where I can imagine it being of use. We'll then go on to explaining the first steps into creating simple hieratical objects know as CodeDOM graphs, or CodeDOM trees, that can be taken and built into assemblies or simply used to output source code as shown in the following examples.

The main CodeDOM advantage is language independent code generation.

I've found the CodeDOM useful in projects I been involved in where tools have been created which generate repetitive code, allowing some of the laborious nature of coding an extensible database program to be alleviated. Whenever I create a database program I usually have to create many maintenance screens that have a one-to-one relationship with certain database tables. These are required to allow users after delivery to add maybe another category to certain sections of a program etc. Time is best spent working on the more complicated screens and a tool can be created that "churns out" these similar screens in seconds, and also allows a single point of call when an alteration is required, and therefore re-generation is needed.

Another is the ability to create, compile and execute an assembly at runtime, and in utilising that advantage I have also seen the CodeDOM used quite differently to the first example when used to speed file access. An assembly created on the fly can be populated from maybe an XML configuration file; it is then far faster then to interrogate the assembly for consequent requests.

Lets get started.

Download Test Application

We will gradually build our examples up so that we can generate a class that utilises as many code structure types as I feel necessary to allow you to understand the CodeDOM.

The example I'll be using is a very simplistic bank account class with properties:

  • Account Holder
  • Account Number
  • Current Balance (Read only)

A function that transfers money to another account and returns a success value will be included.

The example will raise an event when the balance goes below zero. The Account class implements an interface called IAccount. The class also has a constructor that allows initiation of the class with an account number and starting balance.

One you've downloaded the sample the first point of interest is the main form frmTest.

From frmTest you'll be able to build up the sample class step-by-step. The buttons on this main form basically call the functions that I will go on to explain in a moment. The return objects from the functions are then passed to the CodeWriter class.

You'll see that I've repeated a lot of function calls on the click events; this is just to ease understanding of the example. I also apologise for the order that members of the class appear; to the best of my knowledge you cannot correct this without having a file output and using the CodeLinePragma object (please contact me if you know any different)?

The example output language defaults to VB but by using the language combo you can change this to C#, showing the beauty of the CodeDOM.

The actual code for IAccount doesn't exist in my example it's simply an imaginary interface that has been referenced for demonstration purposes.

CodeWriter Class

This class will be expanded on through out the course of the articles but for now its purpose is to take CodeDOM objects placed in a hierarchical order of namespace, class, members and return code back to us as a string in our chosen language.

The shared/static class (doesn't require instantiation) contains the public property Language , this allows the choice of output code language. Also there's only one function at the moment call Write that allows a parameter of type CodeNamespace. This parameter contains the whole code hierarchy. Inside the Write function is a select statement that chooses between output language (in this case VB or C#), the code then creates either a VBCodeProvider or a CSharpCodeProvider object. The provider objects then are used to create a Generator. The Providers allow access to code generators and compilers for the relevant language.

The rest of the functionality populates a StringBuilder from a StringWriter stream that is passed to the Generator's GenerateCodeFromNameSpace method, and the contents of the StringBuilder is returned (this being the actual code).

Invoking 'Generate Class' and the CreateClass Function

Next we'll take a look line-by-line at the first function.

Private Sub cmdGenerateClass_Click(ByVal sender As System.Object_
    ByVal e As System.EventArgsHandles cmdGenerateClass.Click

Dim NewClass As CodeTypeDeclaration = CreateClass
Dim CodeNamespace As New CodeNamespace("Bank")

    CodeNamespace.Types.Add(NewClass)
    txtcode.Text = CodeWriter.Write(CodeNamespace)

End Sub

The first line of the code runs the function CreateClass. The simple job of CreateClass is to create a class (no surprise there then), by either calling the VB or C# class creation functions. This is the only time in my examples at present that we really need to do something a little different depending on the output language. Next the returned class object is added to a Namespace object, CodeNamespace. The namespace is then passed to the CodeWriter class.

First we will take a look at the function CreateVBClass.

Private Function CreateVBClass() As CodeTypeDeclaration
Dim AccountClass As New CodeTypeDeclaration()

    With AccountClass
        .IsClass = True
        .Name = "BankAccount"
        .Members.Add(New CodeSnippetTypeMember("Implements IAccount"))
        .Comments.Add(New CodeCommentStatement_
            "This VB Class has been generated by the CodeDOM"))
        .Comments.Add(New CodeCommentStatement(""))
    End With
    Return AccountClass

End Function

I've tried to comment my code as much as I can, trying to make as readable as possible (it's probably the most commenting I've done for a while!), but I've left the comments out of the article code segments, they can be found in the actual download.

Firstly we create a CodeTypeDeclaration object state that we wish it to signify a Class, as this property also allows Enum, Interface or Structure to be chosen. We then name the class accordingly.

Next we add some comments and the base types. We are implementing an interface called IAccount, to do this is a little different in VB than C#. In the C# we wouldn't need to inherit from Object but we need to do this in VB so that when we add our further basetypes the CodeDOM knows that they must be interfaces as VB can only inherit from on base class.

Finally we return the class object to the calling function.

Looking at CreateCSharpClass we can see the subtle differences required by C#.

Private Function CreateCSharpClass() As CodeTypeDeclaration
Dim AccountClass As New CodeTypeDeclaration()

    With AccountClass
        .IsClass = True
        .Name = "BankAccount"
        .BaseTypes.Add(New CodeTypeReference("IAccount"))
        .Comments.Add(New CodeCommentStatement_
            "This C# Class has been generated by the CodeDOM"))
        .Comments.Add(New CodeCommentStatement(""))

        .Members.Add(New CodeEntryPointMethod())
    End With
    Return AccountClass

End Function

The first of these differences is the possible need for an Entry Point method. This is achieved by creating a CodeMemberMethod object (as shown else where in this article).

The second is the ability to utilise the BaseTypes collection to add a reference to the interface, IAccount, that the class implements.

Other than those two additions the code is identical for either of the two languages. The class creation is the only time that I have had to make a differentiation between the two output languages within the whole example.

Invoking 'With Fields' and the AddFields Function.

To add field members to the class creation, we have just looked at, requires the following code which calls the AddFields function creating a CodeMemeberFieldObject.

NewClass.Members.AddRange(AddFields)

Beneath is the code for the AddFields function

Private Function AddFields() As CodeMemberField()

Dim AccountHolderField As New CodeMemberField()

    With AccountHolderField
        .Name = "mAccountHolder"
        .Type = New CodeTypeReference(GetType(String))
        .Attributes = MemberAttributes.Private
    End With

Dim AccountNumberField As New CodeMemberField()

    With AccountNumberField
        .Name = "mAccountNumber"
        .Type = New CodeTypeReference(GetType(Int32))
    End With

Dim StartBalanceField As New CodeMemberField()

    With StartBalanceField
        .Name = "mStartBalance"
        .Type = New CodeTypeReference(GetType(Decimal))
    End With

Dim CurrentBalance As New CodeMemberField()

    With CurrentBalance
        .Name = "mCurrentBalance"
        .Type = New CodeTypeReference(GetType(Decimal))
    End With

    Return New CodeMemberField() {AccountHolderFieldAccountNumberField_
        StartBalanceFieldCurrentBalance}

End Function

The account class has three private fields, mAccountHolderField, mAccountNumberField, mStartBalanceField, mCurrentBalance. The function of these fields is to store the state of there corresponding properties or in the case of the mStartBalanceField it's job is simply to store the initial value passed into the constructor.
For each of the fields a CodeMemberField object is created and properties Name and Type are set, also it is necessary to set the scope of the field using the Attributes property. Finally a CodeMemberField array is passed back to the calling code to be added to our class.

Invoking 'With Properties' and the AddProperties Function.

Firstly we need to add the function call -

NewClass.Members.AddRange(AddProperties)

Then looking at the AddProperties function, we see it's one of the larger functions in this example, it mostly just repeats the same code for each of the three properties so we'll just take a look at defining the AccountHolderProperty property –

    Dim AccountHolderProperty As New CodeMemberProperty()
    Dim AccountInterface As New CodeTypeReference("IAccount")

    With AccountHolderProperty
        .Name = "AccountHolder"
        .Type = New CodeTypeReference(GetType(String))
        .Attributes = MemberAttributes.Public
        .HasGet = True
        .HasSet = True
        .ImplementationTypes.Add(AccountInterface)

        Dim AccountHolderReference As New CodeFieldReferenceExpression()
        AccountHolderReference.FieldName = "mAccountHolder"

        .GetStatements.Add(New CodeMethodReturnStatement_
            AccountHolderReference))

        Dim AccountHolderAssignment As New CodeAssignStatement()
        AccountHolderAssignment.Left = AccountHolderReference
        AccountHolderAssignment.Right = New CodeArgumentReferenceExpression_
            "value")
        .SetStatements.Add(AccountHolderAssignment)
        AccountHolderAssignment = Nothing
    End With

To start with we create a CodeMemberProperty called 'AccountHolderProperty' to be returned from this function along with the other properties. Next we create a new instance of a CodeTypeReference, the type we will be referenceing is IAccount as the properties of this class will be implementing members of this interface. As I mentioned earlier I haven't actually gone into defining the structure of IAccount as it's really just for demonstration purposes as it could just confuse matters?

Now we have the property instance, we need to set a few details namely, Type, Attributes (Scope), HasGet (Has a Get accessor), HasSet (has a set accessor), Name and the any types that are implemented (in this case we add the reference to IAccount as mentioned before).

Before we return the Property from this function we need to build the the Get and Set accessors. First the Get, in which we need to create a reference to the private field member mAccountHolder, using the CodeDOM object, CodeFieldReferenceExpression. Now we have a refrence to the field, we need to return it from the function so we use a CodeMethodReturnStatement object, passing the field reference in as the required parameter. The return statement is then taken and added to the Statements collection of the Get accessor. Our generated code will now return the value of the private field, which is exactly what we want.

Now the Set accessor, we'd like to just create a pretty standard set that takes the value assigned to the property and set mAccountHolder to be equal to that value. So to do this we are going to create a CodeAssignStatement, this object has two important properties, they are the left and right hand side of the assignment statement. For the left hand side we can reuse or reference to the private field, mAccountHolder. The right hand side requires us to create a CodeArgumentReferenceExpression to the default 'value' property of a Set accessor (well in VB anyway).

The last step of property creation is to add the return statement to the GetStatement's Statement collection, and the value Assignment statement to the SetStatement's Statement collection.

Invoking 'With Constructor' and the CreateConstructor Function.

    Dim Constructor As New CodeConstructor()
    With Constructor
        .Parameters.Add(New CodeParameterDeclarationExpression(GetType_
            Int32), "accountNumber"))
        .Parameters.Add(New CodeParameterDeclarationExpression(GetType_
            Decimal), "startBalance"))

        Dim FieldReference As New CodeFieldReferenceExpression()
        Dim ArgumentRef As New CodeArgumentReferenceExpression()
        
        ArgumentRef.ParameterName = .Parameters(0).Name
        FieldReference.FieldName = "mAccountNumber"
        Dim AccountNumberAssign As New CodeAssignStatement(FieldReference_
            ArgumentRef)
        .Statements.Add(AccountNumberAssign)

        FieldReference = New CodeFieldReferenceExpression()
        ArgumentRef = New CodeArgumentReferenceExpression()
        ArgumentRef.ParameterName = .Parameters(1).Name
        FieldReference.FieldName = "mStartBalance"
        Dim StartBalanceAssign As New CodeAssignStatement(FieldReference_
            ArgumentRef)

        FieldReference = Nothing
        ArgumentRef = Nothing
        .Statements.Add(StartBalanceAssign)
        StartBalanceAssign = Nothing
    End With
    Return Constructor

In order to create a Constructor we need the aptly named CodeDOM CodeConstructor object. The statements within the Constructors will take the two defined parameters and assign them to their corresponding private field members. In order to understand the code I will just take you through one of the assignments.

To start with then we need to define the parameters, we could have used AddRange but I've added them one at a time for simplicity. The parameters are accountNumber (Int32) and startBalance (Decimal ).

We have created a CodeAssignStatement before but we'll go through it again. The first step in this case is to create a CodeArgumentReferenceExpression (ArgumentRef) that refers to the constructors' parameter accountNumber. Next using a CodeFieldReferenceExpression object we need to make reference to the field mAccountNumber. Using a CodeAssignStatement we assign these two objects to each other and then add the assignment to the constructors' Statements collection. This process is then repeated for each parameter. The Constructor is then returned to the calling code and passed to the CodeWriter class.

Invoking 'With Function' and the AddFunction Function

At this stage we are not including any event demonstration code, notice the overloaded version of the AddFunction, so I've removed that section for now from the following code:

Private Function AddFunction_
    ByVal includeEvent As BooleanAs CodeMemberMethod

Dim FunctionMember As New CodeMemberMethod()
    
    With FunctionMember
        .Name = "TransferFunds"
        .Statements.Add(New CodeCommentStatement_
            "Put the body of the function here that commits the transfer."))
        .Attributes = MemberAttributes.Public

        .Statements.Add(New CodeMethodReturnStatement_
            New CodeSnippetExpression("true")))

        .ReturnType = New CodeTypeReference(GetType(Boolean))
    End With

    Return FunctionMember

End Function

Once a CodeMemberMethod has been created, (a CodeMemberMethod defines both a sub or a function, the differentiation is made by the ReturnType property), we need to set a few basic properties such as Name and Attributes (scope) and the ReturnType (Boolean).

We also have two Statements to add to the function, the first is just a comment using the CodeCommentStatement object, that states any futher function code should go here, in this case the code would action the transfer of money, if we were going into that much detail. The second is the return statement, not surprisingly using the CodeDOM CodeMethodReturnStatement object, and a CodeSnippetExpression (which allows us to add any text we wish) containing the return value of 'True'. The CodeMemberMethod is then returned from this function.

Invoking 'With Event and the AddEvent Function.

There are three parts to adding the demonstration of events. Invoking the OnOverDrawn sub which raises the Event OnOverDrawn (the addition to the previous function that calls the OnOverDrawn sub). Having a protected sub that raises the event means that if the class is inherited from a different action can be taken and also any logic required around raising the event can be placed within the sub which means there is only one section of code to maintain/change and all the calling code will benefit from the change. Finally the definition of the OverDrawn Event.

If we look at these three steps in a sensible order we should start with the definition of the Event:

Private Function AddEvent() As CodeMemberEvent

Dim OverDrawnEvent As New CodeMemberEvent()

    With OverDrawnEvent
        .Name = "OverDrawn"
        .Attributes = MemberAttributes.Public
        .Type = New CodeTypeReference(GetType(EventHandler))
    End With
    Return OverDrawnEvent

End Function

This time we use a CodeMemberEvent object and set the name and Attributes properties. The type of the Event will be an EventHandler, this is set by creating a CodeTypeReference object. Then we just return the CodeMemberEvent back to the calling code.

Lets look at the definition of the OnOverDrawn sub:

Private Function AddOnOverDrawnSub() As CodeMemberMethod

Dim OnOverDrawnSub As New CodeMemberMethod()

    With OnOverDrawnSub
        .Attributes = MemberAttributes.FamilyOrAssembly
        .Name = "OnOverDrawn"
        .Parameters.Add(New CodeParameterDeclarationExpression(GetType_
            EventArgs), "e"))

        Dim OverDrawnEventRef As New CodeEventReferenceExpression_
            New CodeThisReferenceExpression(), "OverDrawn")
        Dim OverDrawnEventInvoke As New CodeDelegateInvokeExpression()
        With OverDrawnEventInvoke
            .TargetObject = OverDrawnEventRef
            .Parameters.Add(New CodeThisReferenceExpression())
            .Parameters.Add(New CodeVariableReferenceExpression("e"))
        End With
        .Statements.Add(OverDrawnEventInvoke)

        OverDrawnEventInvoke = Nothing
        OverDrawnEventRef = Nothing
    End With
    Return OnOverDrawnSub

End Function

As with AddFunction we have created a CodeMemberMethod but this time with no return type and the Attributes property is set to FamilyOrAssembly meaning it is protected. This method accepts a parameter called e and is of type EventArgs as you can see this is achieved by the use of a CodeParameterDeclarationExpression object. We then need to reference the Event OverDrawn using a CodeEventReferenceExpression. This Event reference is added to the Target Property of the CodeDelegateInvokeExpression object, also parameters of Me and e are added to the object. This Delegate invocation is added to the methods Statement collection and the Sub is passed back to the calling code.

Here is the extra section of AddFunction that is added by the overloaded AddFunction with a parameter value of True -

Dim CodeConditionStatement As New CodeConditionStatement()

    With CodeConditionStatement
        .Condition = New CodeBinaryOperatorExpression_
                    New CodeFieldReferenceExpression_
                    New CodeThisReferenceExpression(), "mCurrentBalance"), _
                        CodeBinaryOperatorType.LessThan_
                        New CodePrimitiveExpression(0))

        Dim OnOverDrawnMethodInvoke As New CodeMethodInvokeExpression()

        OnOverDrawnMethodInvoke.Method = New CodeMethodReferenceExpression_
            New CodeThisReferenceExpression(), "OnOverDrawn")

        OnOverDrawnMethodInvoke.Parameters.Add(New CodeSnippetExpression_
            "System.EventArgs.Empty"))

        .TrueStatements.Add(OnOverDrawnMethodInvoke)
    End With
    .Statements.Add(CodeConditionStatement)

We need to create a CodeConditionStatement that will decide if the OnOverDrawn sub should be called. The condition needs to contain a CodeBinaryOperatorExpression that checks to see if the field reference to mCurrentBalance is less than zero (I've used CodePrimitiveExpression to include the integer value of '0' in the comparison).

Next we need to create the Statement to be added to the True part of the comparison (we have no need to define a False statement). This will mean building a CodeMethodInvokeExpression object. The Method property of the CodeMethodInvokeExpression will reference the method OnOverDrawn. Also we need to pass the required parameter into the method, we'll do this with the following line of code -

    CodeSnippetExpression("System.EventArgs.Empty"))

The condition is now complete and is added to the functions Statement collection.

The explanation of the Event calling code means that all sections of the test application have now been covered.

I hope that this small demonstration helps illustrate how to utilise much of the functionality included in the CodeDOM to achieve the code structures that you will undoubtedly require in any CodeDOM projects you embark on yourselves.

To conclude this section of building CodeDOM graphs it has to be said that there is a lot of work required to build event the simplest of elements such as an event of property. But in order to achieve a totally language independent representation of the code you wish to output it is obvious that this is something we have to put up with.

The examples, I feel so far, haven't been complicated but have covered a lot of ground.

Next we'll go one to look a list of the limitations I have experienced so far within the CodeDOM.

Limitations

Below I have listed (in no particular order), limitations that I have encounted in version 1 of the .NET Framework (and in some cases 1.1).Where ever posible I have tied to add workarounds.

LimitationWorkaround
Adding Comments other than atop methods, classes or namespaces is a known limitation.Add a CodeSnippetTypeMember containing the literal text for the comment to the Members collection (in the appropriate order to appear in the proper location). The drawback of this approach is that the comment syntax will not be translated across languages. Build your graph dynamically to substitute comment snippets using language-specific syntax if robust multiple language support for comments between type members is important.
IParser is not implemented or throws errors for both C# or VBNo workaround is needed, as this certainly does not stop you from compiling. The limitation is that you have to ensure the code that you try to compile is statically correct rather than allowing IParser to do the checking for you.
Different languages need specific elements sometimesA Case statement to insert extra code for certain languages such as Start Point in C# (as mentioned in this articles example).
Using the Alias directiveNested namespaces
The CodeDom namespaces contain classes to conceptually represent most programming constructs. Examples of these include: declarations, statements, iterations, arrays, casts, comments, error handling, and others. However, there are limitations to the current implementation of the CodeDOM, which will remain until Microsoft updates the CodeDom namespacesTo represent constructs not presently supported in CodeDOM you can use the "Snippet" classes. These classes allow you to insert literal lines of source code into a compile unit. The classes available for handling snippets include: the CodeSnippetCompileUnit, CodeSnippetExpression, CodeSnippetStatement, and CodeSnippetTypeMember classes. You can use the snippet classes as a generic "catch all" for implementing presently unsupported programming constructs in the CodeDom.
Variable/Field declaration list (int i,j,k;)Change to individual variable declarations. Really to keep code totally readable this is a preferred method anyway.
Pointer typeJagged array type (array of arrays).

Also:

  • Unsafe modifier not supported.
  • ReadOnly modifier not supported.
  • Volatile modifier not supported.
  • Add and Remove accessors for Event not supported.

Useful links

An example of taking C# code and building a CodeDOM tree.
Microsoft .NET CodeDom Technology - Part 1
Microsoft .NET CodeDom Technology - Part 2
If you do not need source code, you can use the classes in System.Reflection.Emit This namespace is designed to be used by script engines and compilers to generate MSIL code or complete assemblies on disk.

Next Time, Part 2: Introduction to the Code Model

How would you rate the quality of this article?
1 2 3 4 5
Poor Excellent
Tell us why you rated this way (optional):

Article Rating
The average rating is: No-one else has rated this article yet.

Article rating:3.27586206896551 out of 5
 29 people have rated this page
Article Score40385
Sponsored Links