Posted by: rcosic | 15/03/2009

CodeDOM

Recently I had an opportunity to refactor some code regarding dynamic source code generation via CodeDOM. I have found it quite simple to use, and if you have a need for it, I hope I can help you to understand it. I needed it because the project uses so-called duck-typing functionality which creates a proxy classes on a client based on a provided interface.

The .NET Framework includes a mechanism called the Code Document Object Model (CodeDOM) which enables generation and compilation of source at run time. It is language-independent, so you can generate C# or VB code simply by using an adequate code provider (.NET currently incorporates C#, Jscript and VB code generator and compiler). The namespace has couple of useful classes which I will present in this post.

Roger Boscovich. Theoria Philosophiæ Naturalis Redacta Ad Unicam Legem Viriumin Natura Existentium. 1763

Roger Boscovich. Theoria Philosophiæ Naturalis Redacta Ad Unicam Legem Viriumin Natura Existentium. 1763

How to generate a class on-the-fly?

At first, you should consider which sort of source you will use for the class generation. I’v used the interface as the basis, and also there was necessity to create multiple classes from multiple namespaces containing similar functionality. Nevertheless, the principle is the same:

  1. create a compile unit
  2. define a namespace for a class
  3. import required namespaces
  4. add the namespace
  5. define a type for a class
  6. determine base type class
  7. document class definition (if you desire)
  8. add a base type and class members to code declaration
  9. add the type to defined namespace.

As you could see, the list somewhat differs from the original steps stated in MSDN library. It is so because I’ve used classes from different namespaces which referenced other classes contained in other namespaces. Because of that you should take care of lots of references you possibly might use.

An example on how to create a class in runtime

For the sake of this example, I will create one interface to demonstrate on how proxy class can be generated:

internal interface IClientClass
{
Guid Identifier { get; set; }
bool Active { get; }
void DoSomething();
}

And create a method which will handle all the work:

private static void GenerateClass(Type targetType)
{

}

After that, we’re good to go to fill some ‘meat’ inside it:

CodeCompileUnit codeCompileUnit = new CodeCompileUnit();

// define a namespace
CodeNamespace codeNamespace = new CodeNamespace(targetType.Namespace);

// import required namespaces

string[] standardUsings = new string[] {
“System”,
“System.Collections.Generic”,
“System.Text”,
“System.Xml”,
“System.Xml.Serialization” };

foreach (string importedUsing in standardUsings)
{
codeNamespace.Imports.Add(new CodeNamespaceImport(importedUsing));
}

// add the namespace
codeCompileUnit.Namespaces.Add(codeNamespace);

// define a type of a class
CodeTypeReference codeTypeReference = new CodeTypeReference();
CodeTypeDeclaration codeTypeDeclaration = null;

// determining base type of class
codeTypeReference.BaseType = targetType.Name;
codeTypeDeclaration = new CodeTypeDeclaration(targetType.Name.Substring(1));

// documenting class definition
AddClassComment(codeTypeDeclaration);

// adding attributes to a class
AddClassAtrribute(codeTypeDeclaration, “System.Serializable”);

codeTypeDeclaration.IsClass = true;

// add a base type, all properties and methods to code declaration

codeTypeDeclaration.BaseTypes.Add(codeTypeReference);
AddProperties(targetType, codeTypeDeclaration);
AddMethods(targetType, codeTypeDeclaration);

// add all referenced assemblies, properties and methods of all the interfaces
// implemented or inherited by the target type
foreach (Type type in targetType.GetInterfaces())
{
RegisterReferences(type);
AddProperties(type, codeTypeDeclaration);
AddMethods(type, codeTypeDeclaration);
}

// adding this type to defined namespace
codeCompileUnit.Namespaces[0].Types.Add(codeTypeDeclaration);

This is it. What is missing is to explain and show you the helper methods used for adding comments, attributes, properties and methods to a class:

Adding Comments to the Class:

private static void AddClassComment(CodeTypeDeclaration codeTypeDeclaration)
{
string codeComment = @”
——————————————————————————

This code was generated by a tool.
Runtime Version:2.0.50727.1433

Changes to this file may cause incorrect behavior and will be lost if
the code is regenerated.

——————————————————————————“;

CodeCommentStatement statement = new CodeCommentStatement();
statement.Comment = new CodeComment(codeComment, true);
codeTypeDeclaration.Comments.Add(statement);
}

Adding an Attribute to the Class:

private static void AddClassAtrribute(CodeTypeDeclaration codeTypeDeclaration, string attributeName)
{
CodeAttributeDeclaration attribute = new CodeAttributeDeclaration(attributeName);
CodeAttributeDeclarationCollection attributes = new CodeAttributeDeclarationCollection();
attributes.Add(attribute);
codeTypeDeclaration.CustomAttributes = attributes;
}

Adding properties to the class:

private static void AddProperties(Type targetType, CodeTypeDeclaration codeTypeDeclaration)
{
foreach (PropertyInfo propertyInfo in targetType.GetProperties())
{
CreateMember(propertyInfo, codeTypeDeclaration);
CreateProperty(propertyInfo, codeTypeDeclaration);

//add referenced assemblies of this property
RegisterReferences(propertyInfo.PropertyType);
}
}

private static void CreateMember(PropertyInfo propertyInfo, CodeTypeDeclaration codeTypeDeclaration)
{
string memberName = GetFieldName(propertyInfo.Name);
CodeMemberField memberfield = new CodeMemberField(propertyInfo.PropertyType, memberName);
codeTypeDeclaration.Members.Add(memberField);
}

private static string GetFieldName(string propertyName)
{
return “_” + propertyName.Substring(0, 1).ToLower(CultureInfo.CurrentCulture) + propertyName.Substring(1);
}

private static void CreateProperty(PropertyInfo propertyInfo, CodeTypeDeclaration codeTypeDeclaration)
{
CodeMemberProperty codeProperty = new CodeMemberProperty();

// define a name for property
codeProperty.Name = propertyInfo.Name;

// determine accessibility for property (scope, getter, setter)

MethodInfo[] getAccessorInfo = propertyInfo.GetAccessors();
codeProperty.Attributes = MemberAttributes.Public;

if (getAccessorInfo != null && getAccessorInfo.Length > 0)
{
codeProperty.HasGet = (getAccessorInfo != null && getAccessorInfo.Length > 0);
}
else
{
codeProperty.HasGet = false;
}

MethodInfo setAccessorInfo = propertyInfo.GetSetMethod();

if (setAccessorInfo != null)
{
codeProperty.HasSet = true;
}
else
{
codeProperty.HasSet = true;
}

// define a type for property
codeProperty.Type = new CodeTypeReference(propertyInfo.PropertyType.FullName);

// link the field member and property
CodeTypeReference codeTypeReference = new CodeTypeReference();
CodeParameterDeclarationExpression cpd = new CodeParameterDeclarationExpression(
codeTypeReference, GetFieldName(propertyInfo.Name));
codeProperty.Parameters.Add(cpd);

codeTypeDeclaration.Members.Add(codeProperty);
}

As you can see from the code, I also create one member field per one property and link them together.

Adding methods to the class:

  private static void AddMethods(Type type, CodeTypeDeclaration codeTypeDeclaration)
{
MethodInfo[] methodInfos = type.GetMethods();
foreach (MethodInfo methodInfo in methodInfos)
{
RegisterReferences(methodInfo.ReturnType);

// a property has a set_ and a get_ methods, but it is wrong to put this in a generated class.
if (methodInfo.Name.StartsWith(“set_”, StringComparison.OrdinalIgnoreCase) == false
&& methodInfo.Name.StartsWith(“get_”, StringComparison.OrdinalIgnoreCase) == false)
{
CodeMemberMethod method = new CodeMemberMethod();
method.Name = methodInfo.Name;
method.ReturnType = new CodeTypeReference(methodInfo.ReturnType.FullName);
method.Attributes = MemberAttributes.Public;

// put some code inside the method (do not write any semicolon at the end!)
CodeMethodReferenceExpression codeExpression = new CodeMethodReferenceExpression(
null, “// put some code here.”);

method.Statements.Add(codeExpression);
codeTypeDeclaration.Members.Add(method);
}
}
}

What is evidently missing here is to fill out the property methods (get, set) and the class methods with some code. Unless you do not do it, the compiler will produce errors. Also, the method for registering the assemblies is missing too – I will explain it next time, when I will describe how to compile and consume newly generated class. Until then…

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Categories

%d bloggers like this: