Thursday, March 26, 2009

The process of building a Code Generation template

More often than not, you’ll have several related classes and you’ll want to turn them into generated code. There is a process I like to use to make a generation template that’s based on this idea. You can also use the process even if you don’t have the classes, by building them manually.

First, you’ll need a class. For example, we’re going to use generation to create a strongly type representation of a directory structure and files on disk. In the example, these files will be configuration files spread throughout a few directories, but we’ll open them as simple strings to make the sample easier. Let’s look at the code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace MyCompany.MyProduct
{
class Configuration
{
private string baseDir = "Configuration";

public Configuration(string baseDir)
{
this.baseDir = string.Concat(baseDir, "\\Configuration");
}

public Network Network
{
get
{
return new Network(baseDir);
}
}
}

class Network
{
private string baseDir;

public Network(string baseDir)
{
this.baseDir = string.Concat(baseDir, "\\Network");
}

public string ProxiesConfig
{
get
{
return File.ReadAllText(string.Concat(baseDir, "\\Proxies.Config");
}
set
{
File.WriteAllText(string.Concat(baseDir, "\\Proxies.Config"), value);
}
}

public string HostsConfig
{
get
{
return File.ReadAllText(string.Concat(baseDir, "\\Hosts.Config");
}
set
{
File.WriteAllText(string.Concat(baseDir, "\\Hosts.Config"), value);
}
}
}

class Security
{
private string baseDir;

public Security(string baseDir)
{
this.baseDir = string.Concat(baseDir, "\\Security");
}

public string RolesConfig
{
get
{
return File.ReadAllText(string.Concat(baseDir, "\\Roles.Config");
}
set
{
File.WriteAllText(string.Concat(baseDir, "\\Roles.Config"), value);
}
}
}
}


Notice that this code already has a “generated” look and feel. The idea is to create a Configuration object with the base path for configuration, and to go down from there to all the configuration items. Network and Security will be directories within Configuration, and the other properties will refer to files in the directories. Notice also that at this point it’s not very important to have complete classes. A rough layout is enough.



The next step is to find varying parts. This is really simple: variables will generally be present as constants or as variable properties. Constants is far more common. In this step, we’ll replace the constants with representative code blocks:



    class Configuration
{
private string baseDir = <#= baseDirectoryName #>;

public Configuration(string baseDir)
{
this.baseDir = string.Concat(baseDir, “<#= baseDirectoryPath #>”);
}

public Network Network
{
get
{
return new Network(baseDir);
}
}
}

class Network
{
private string baseDir;

public Network(string baseDir)
{
this.baseDir = string.Concat(baseDir, “<#= baseDirectoryPath #>”);
}

public string ProxiesConfig
{
get
{
return File.ReadAllText(string.Concat(baseDir, “<#= configFileName #>”);
}
set
{
File.WriteAllText(string.Concat(baseDir, “<#= configFileName #>”), value);
}
}

public string HostsConfig
{
get
{
return File.ReadAllText(string.Concat(baseDir, “<#= configFileName #>”);
}
set
{
File.WriteAllText(string.Concat(baseDir, “<#= configFileName #>”), value);
}
}
}


Then we’ll also replace the varying property names (and class and method names if necessary) with code blocks too:



    class <#= baseDirectoryName #>
{
private string baseDir = <#= baseDirectoryName #>;

public Configuration(string baseDir)
{
this.baseDir = string.Concat(baseDir, “<#= baseDirectoryPath #>”);
}

public <#= childDirectoryName #> <#= childDirectoryName #>
{
get
{
return new <#= childDirectoryName #>(baseDir);
}
}
}

class <#= baseDirectoryName #>
{
private string baseDir;

public <#= baseDirectoryName #>(string baseDir)
{
this.baseDir = string.Concat(baseDir, “<#= baseDirectoryName #>”);
}

public string <#= configFileName #>
{
get
{
return File.ReadAllText(string.Concat(baseDir, “<#= configFileName #>”);
}
set
{
File.WriteAllText(string.Concat(baseDir, “<#= configFileName #>”), value);
}
}

public string <#= configFileName #>
{
get
{
return File.ReadAllText(string.Concat(baseDir, “<#= configFileName #>”);
}
set
{
File.WriteAllText(string.Concat(baseDir, “<#= configFileName #>”), value);
}
}
}


At this point, you should have some duplication. The next step is to remove that duplication by looping through duplicated constructs:



<#
for (int classes = 0; classes < n; classes++)
{
#>
class <#= baseDirectoryName #>
{
private string baseDir;

public <#= baseDirectoryName #>(string baseDir)
{
this.baseDir = string.Concat(baseDir, “<#= baseDirectoryName #>”);
}

<#
for (int props = 0; props < n; props++)
{
#>
public string <#= configFileName #>
{
get
{
return File.ReadAllText(string.Concat(baseDir, “<#= configFileName #>”);
}
set
{
File.WriteAllText(string.Concat(baseDir, “<#= configFileName #>”), value);
}
}
<#
}
#>
}
<#
}
#>


You should also add whatever’s left behind (in this case, the “directory” properties:



<#
for (int classes = 0; classes < n; classes++)
{
#>
class <#= baseDirectoryName #>
{
private string baseDir;

public <#= baseDirectoryName #>(string baseDir)
{
this.baseDir = string.Concat(baseDir, “<#= baseDirectoryName #>”);
}

<#
for (int childClasses = 0; childClasses < n; childClasses++)
{
#>
public <#= childDirectoryName #> <#= childDirectoryName #>
{
get
{
return new <#= childDirectoryName #>(baseDir);
}
}
<#
}
#>
<#
for (int props = 0; props < n; props++)
{
#>
public string <#= configFileName #>
{
get
{
return File.ReadAllText(string.Concat(baseDir, “<#= configFileName #>”);
}
set
{
File.WriteAllText(string.Concat(baseDir, “<#= configFileName #>”), value);
}
}
<#
}
#>
}
<#
}
#>


At this point, you should have a pretty good skeleton. The point now is to make it work:



<#
foreach(string directory in Directory.GetDirectories("myconfigurationdir"))
{
#>
class <#= directory #>
{
private string baseDir;

public <#= directory #>(string baseDir)
{
this.baseDir = string.Concat(baseDir, "<#= directory #>");
}

<#
foreach (string childDirectory in Directory.GetDirectories(directory))
{
#>
public Network <#= childDirectory #>
{
get
{
return new <#= childDirectory #>(baseDir);
}
}
<#
}
#>
<#
foreach (string file in Directory.GetFiles(directory))
{
#>
public string <#= file #>
{
get
{
return File.ReadAllText(string.Concat(baseDir, "<#= file #>");
}
set
{
File.WriteAllText(string.Concat(baseDir, "<#= file #>"), value);
}
}
<#
}
#>
}
<#
}
#>


At this point you should be added any imports that are needed for the generation code to work (System.IO in this case). You may also need to separate processes in methods, as to make them recursive. For instance, our classes need to be generated for each directory in the hierarchy. Finally, you must solve generation-specific problems. For instance, this generation will probably have many invalid class and property names (“Proxies.Config” is not a valid class name). You can change that by using the CleanName procedure that we already reviewed in this blog. The result will look something like this:



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

<#
GenerateClass("myconfigdir");
#>
<#+
void GenerateClass(string directoryName)
{
foreach(string directory in Directory.GetDirectories(directoryName))
{
#>
class <#= CleanName(directory) #>
{
private string baseDir;

public <#= CleanName(directory) #>(string baseDir)
{
this.baseDir = string.Concat(baseDir, "<#= directory #>");
}

<#+
foreach (string childDirectory in Directory.GetDirectories(directory))
{
#>
public <#= CleanName(childDirectory) #> <#= CleanName(childDirectory) #>
{
get
{
return new <#= CleanName(childDirectory) #>(baseDir);
}
}
<#+
}
#>
<#+
foreach (string file in Directory.GetFiles(directory))
{
#>
public string <#= CleanName(file) #>
{
get
{
return File.ReadAllText(string.Concat(baseDir, "<#= file #>");
}
set
{
File.WriteAllText(string.Concat(baseDir, "<#= file #>"), value);
}
}
<#+
}
#>
}
<#+
GenerateClass(directory);

}
}


string CleanName(string)

{

//some code cleaning here

}
#>


The final step is to separate the code into modules. For instance, you can put the CleanName procedure in an include T4 file to be used as a helper, put the header in a “main” T4 and the methods in generation T4 files, separate the properties into its own include, or whatever feels good to you.



--

Written by Joaquin, joj AT clariusconsulting DOT net. Disclaimer: opinions expressed in this post are my own and not necessarily reflect those of Clarius Consulting.

3 comments:

  1. This comment has been removed by a blog administrator.

    ReplyDelete
  2. I tried to download the version with intellisense but the download link is not a link...

    ReplyDelete
  3. Thanks for sharing great information. Keep up the great work, you are providing a great resource on the Internet here!

    nintendo dsi r4

    ReplyDelete