Showing posts with label T4 Templates. Show all posts
Showing posts with label T4 Templates. Show all posts

Saturday, July 23, 2011

Generate several files from one template with the T4 Toolbox

I have been playing with using T4Toolbox to generate more than one file from a single template within Visual Studio.

T4Toolbox.Template

The Template class is the T4Toolbox’s version of TextTransformation that has a very special method called RenderToFile(); this method writes the text to be generated to a file. So, If I wanted to generate a file for each item in a list (say a list of database tables), I could use a foreach loop to create a new instance of my Template class and call Its RenderToFile method with different file names.

<#@ template language="C#v3.5" hostSpecific="true" #>
<#@ output extension="txt" #>
<#@ include file="T4Toolbox.tt" #>
<#
    Write("This file (minimum.txt) is also generated");
    for (int i = 7; i <= 11; ++i)
    {
        string fileName = string.Format("file{0:00}.lam", i);
        var lt = new LameFileTemplate(fileName);
        lt.RenderToFile(fileName);
    }
#>
<#+
private class LameFileTemplate : Template
{
    public LameFileTemplate(string fileName)
    {
        this.FileName = fileName;
    }
    private string FileName {get; set;}
    public override string TransformText()
    {
        #><#=this.FileName#><#+
        return this.GenerationEnvironment.ToString();
    }
}
#>

When I save this file, Visual Studio runs the template and creates several files. Notice that creates a file that matches the file name of the template; this can’t be helped. Since the default output extension is “cs”, it is important to set this to something else.

image
My template and generated files

NOTE: In a failed demo that I gave at the Portland Code camp this spring I wrote a template that would create a SELECT stored procedure for each table in a given database (each in its own file). The demonstration was too complicated and hard to follow. It used the Generator class SMO and a bunch of other stuff.

Thursday, June 23, 2011

The GAX Property Directive

One of the things that really bother me about T4 templates is the lack of direct parameter support (at least in the Visual Studio 2008/.NET 3 timeframe).

In an earlier post, I passed parameter by writing the parameter data to a file outside of the template and reading the data from within. I used a common assembly linked to both sides of the divide. This works for me, but it is really hacky.

GAX Template Host

During my preparation for my Portland Code Camp presentation, I came across Guidance Automation Toolkit for Visual Studio 2008. It has its own TemplateHost that includes a Dictionary of parameters.

Parameters are exposed to the Template through a custom directive. On the template, the parameter is declared using a directive like this:

<#@ property processor="PropertyProcessor" name="MyName" type="String" #>
The directive contains the following parts:
processor Should be "PropertyProcessor" (there may be a way to write your own processor, I haven’t gone down that rat hole yet).
name The Variable name that can appear in your code. It works just like a variable, however, tangable T4 Editor doesn’t provide Intenesence (I wouldn’t have expected it).
type Any .NET Type that the template knows about. You should use the .NET type name (as opposed to C# or VB.NET type).
In C# the parameters are created like this:
var arguments = new Dictionary();
arguments.Add("MyProperty", new PropertyData("String", typeof(string)));
To pass parameters from your code you need to create a Dictionary<string, PropertyData>, add your parameters and pass them as the second argument into the TemplateHost constructor (the first can be any string as far as I can tell).

Simple Sample

The Template

<#@ template language="C#" debug="True" #>
<#@ output extension="txt" #>
<#@ assembly name="System.dll" #>
<#@ import namespace="System" #>
<#@ property processor="PropertyProcessor" name="MyProperty" type="Nullable<Int32>"#>
<#@ property processor="PropertyProcessor" name="MyName" type="String"#>
My Property Test
<# if (MyProperty.HasValue) {#>
Property Value: <#= MyProperty.Value #>!
<#}#>
Name: <#= MyName #>

C# code

static void Main(string[] args)
{
    // Prepare template parameters
    var arguments = new Dictionary<string, PropertyData>();
    arguments.Add("MyProperty", new PropertyData(42, typeof(int?)));
    arguments.Add("MyName", new PropertyData("Jack Stephens", typeof(string)));

    // Initialize GAX template host
    // The Template Host is from GAX, not the default host
    var host = new TemplateHost("Random String", arguments);
    host.TemplateFile = Path.Combine(Directory.GetCurrentDirectory(), 
        "PropertyTest.tt");

    // Transform template
    string template = File.ReadAllText(host.TemplateFile);

    ITextTemplatingEngine engine = new Engine();
    string output = engine.ProcessTemplate(template, host);

    Console.Write(output);
    Console.ReadKey();
}

References

Monday, June 13, 2011

Portland Code Camp 2011 Presentation Notes

Again, I'm late getting this up. I will give usual excuses of being busy. In the future I will not promise anything before the next weekend after the code camp.

Since I developed this talk on this blog, this entry will essentially be a link list. I put a copy of the slides in Google doc and you can reference it here

This presentation is similar, but not the same as a presentation I gave at the Boise Code Camp in February. In the interest of being DRY (Don’t Repeat Yourself) I will reference the notes for that presentation.

Tools

For this presentation, I used the Tangible T4 Editory and the T4 Toolbox.

Template Parts

I presented a slide that showed the basic template parts in a small mock template:

<#@ template language="C#" #>
<#@ directive property=“value” #>
<# var item = "Statement Block"; #>
Text Block 
<#= "Expression Block" #>
<#+ 
    string ClassFeatureBlock(string thingy)
    {
        return thingy;
    } 
#>

Hello World Monty

I showed some of the elementary features of T4 templates by refactoring a basic template that would create a text file that contained the infamous words “Hello World”. A script of that exercise is here

Basic Code Generation Demonstration

To demonstrate Code Generation I set out to generate a simple T-SQL Select Statement using Sql Management Objects (SMO).

Here I will refer to the Boise Code Camp Notes.

Generating Multiple Files using T4 Toolbox

I used the result of the Basic Code Generation Demonstration to attempt to generate select scripts for all the tables in a given database. This demonstration was based on a series of blog posts by Oleg Synch (Part 1, Part 2, Part 3 & Part 4).

I do intend to give my take on this process. Perhaps that is why I'm over a week late on these notes. I will add a link to these notes later.

GAX Property Directive

I discussed using Guidance Automation Extensions's property directive to overcome T4's lack of native support for passing parameters into templates. This part was based on another Oleg Synch article Understanding T4: <#@ property #> directive.

Again, I will add links to future blogs post(s) on this subject here.

Generating a Nullable ADO.NET DataSet Wrapper

This is when I went to Show and Tell Mode. I demonstrate a Nullable DataSet Wrapper that I wrote in these blog entries. I wrote the templates in VB.NET to generate code in C#.

Part 1, Part 2, Part 3 & Part 4

Last Word

This presentation reached too far and, like Icarus, I failed to hit the mark. The number of forward links is a sign of that. I intend to back-blog in the areas the missing material and add links when I am done.

Tuesday, April 12, 2011

Generate Linq To Sql .DBML Fragments

Another user for my post on getting the schema from a Stored Procedure. This time I am generating a Dbml fragment.

In the Column element, I am not providing a DbType. I don’t use Linq to Sql in such a way that it actually uses that value for anything important. I am also assuming that CanBeNull is true.

Yes, this is totally a message for work post. If you need detail as to what I’m doing, check this out.

<#@ assembly name="TsqlDesriptionUtil.dll" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#@ import namespace="TsqlDesriptionUtil" #>
<#@ import namespace="System.Collections.Generic" #>
<#+
    public class DbmlFunctionFromTSqlCmd : TextTransformation
    {
        public string FunctionName;
        public string FunctionMethod;
        public List<TableDescription> tables;

        public override string TransformText()
        {
#>    <Function Name="<#= FunctionName
                #>" Method="<#= FunctionMethod #>">
<#+ foreach(TableDescription table in tables)
    { #>        <ElementType Name="<#= FunctionMethod #>Result_<#= table.TableName #>">
<#+ foreach(FieldDescription field in table.Fields)
   { #>             <Column Name="<#= field.FieldName #>" 
                      Type="<#= field.FieldType.FullName  #>" 
                      CanBeNull="true" />
<#+ } #>
         </ElementType>
<#+ } #>
    </Function>
<#+ 
        return this.GenerationEnvironment.ToString();
    }
} #>

Again, for Demonstration purposes I wrote a simple T4 Template that includes the template above and uses the library I wrote in this post to get a description of the data.

<#@ template language="C#v3.5" debug="true" 
#><#@ output extension="txt" 
#><#@ include file="DbmlFunctionFromTSqlCmd.tt" 
#><#@ assembly name="TsqlDesriptionUtil.dll" 
#><#@ import namespace="TsqlDesriptionUtil" 
#><#
    // Since I'm generating XML, I will go to extreme measures to avoid new 
    // line characters like the strange formatting of declarations above ^
 
    // Get the table descriptions for given T-SQL
    string conString = @"Data Source=localhost;" +
      @"Initial Catalog=Northwind;Integrated Security=True";
    string commandString = "EXEC  CustOrderHist @CustomerID='ALFKI'";
    List<TableDescription> tables = 
      TsqlCommandSchema.GetFieldDescription(conString, commandString);
 
    // Generate DBML Fragment:
    var gen = new DbmlFunctionFromTSqlCmd();
    gen.FunctionName = "dbo.GetBigTableDataUseTempTab";
    gen.FunctionMethod = "MyNew";
    gen.tables = tables;
    Write(gen.TransformText()); 
#>

(If I were more of a studman programmer dude, I'd write a Visual Studio Add-in that would insert this fragment directly into my .dbml file.)

Tuesday, March 29, 2011

Generate DataSet .XDS

Last week on my Brochure, I wrote about getting the schema of a Stored Procedure or other T-SQL command. Now I am going to bring this a little farther and generate am .XDS file that can be used to generate a Typed DataSet. This T4 Template include file takes a List and a DataSet name and generates the .XSD.

<#@ assembly name="TsqlDesriptionUtil.dll" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#@ import namespace="TsqlDesriptionUtil" #>
<#@ import namespace="System.Collections.Generic" #>
<#+
    public class DataSetFromTSqlCmd: TextTransformation
    {
        public string dsName;
        public List<TableDescription> tables;

        public override string TransformText()
        {
#><?xml version="1.0" encoding="utf-8"?>
<xs:schema id="<#= dsName #>" 
  targetNamespace="http://jrcs3.blogspot.com/<#= dsName #>.xsd"
  xmlns:mstns="http://jrcs3.blogspot.com/<#= dsName #>.xsd" 
  xmlns="http://jrcs3.blogspot.com/<#= dsName #>.xsd" 
  xmlns:xs="http://www.w3.org/2001/XMLSchema" 
  xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"
  xmlns:msprop="urn:schemas-microsoft-com:xml-msprop" 
  attributeFormDefault="qualified" 
  elementFormDefault="qualified">
  <xs:annotation>
    <xs:appinfo source="urn:schemas-microsoft-com:xml-msdatasource">
      <DataSource DefaultConnectionIndex="0" 
          FunctionsComponentName="QueriesTableAdapter"
          Modifier="AutoLayout, AnsiClass, Class, Public" 
          SchemaSerializationMode="IncludeSchema" 
          xmlns="urn:schemas-microsoft-com:xml-msdatasource">
        <Connections />
        <Tables />
        <Sources />
      </DataSource>
    </xs:appinfo>
  </xs:annotation>
 <xs:element name="<#= dsName #>" 
        msdata:IsDataSet="true" 
        msdata:UseCurrentLocale="true" 
        msprop:Generator_UserDSName="<#= dsName #>" 
        msprop:Generator_DataSetName="<#= dsName #>" 
        msprop:EnableTableAdapterManager="true">
    <xs:complexType>
      <xs:choice minOccurs="0" maxOccurs="unbounded">
<#+ foreach(TableDescription table in tables)
    { #>
        <xs:element 
            name="<#= table.TableName #>"  
            msprop:Generator_UserTableName="<#= table.TableName #>" 
            msprop:Generator_RowDeletedName="<#= table.TableName #>RowDeleted" 
            msprop:Generator_RowChangedName="<#= table.TableName #>RowChanged" 
            msprop:Generator_RowClassName="<#= table.TableName #>Row" 
            msprop:Generator_RowChangingName="<#= table.TableName #>RowChanging" 
            msprop:Generator_RowEvArgName="<#= table.TableName #>RowChangeEvent" 
            msprop:Generator_RowEvHandlerName="<#= table.TableName 
               #>RowChangeEventHandler" 
            msprop:Generator_TableClassName="<#= table.TableName #>DataTable" 
            msprop:Generator_TableVarName="table<#= table.TableName #>" 
            msprop:Generator_RowDeletingName="<#= table.TableName #>RowDeleting" 
            msprop:Generator_TablePropName="<#= table.TableName #>">
          <xs:complexType>
            <xs:sequence>
<#+ foreach(FieldDescription field in table.Fields)
 { #>
                <xs:element 
                    name="<#= field.FieldName #>" 
                    msprop:Generator_UserColumnName="<#= field.FieldName #>"
                    msprop:Generator_ColumnVarNameInTable="column<#= field.FieldName 
                      #>" 
                    msprop:Generator_ColumnPropNameInRow="<#= field.FieldName #>" 
                    msprop:Generator_ColumnPropNameInTable="<#= field.FieldName 
                      #>Column" 
                    type="xs:<#= field.XsdTypeName #>" 
                    minOccurs="0" />
<#+ } #>
            </xs:sequence>
          </xs:complexType>
        </xs:element>
<#+ } #>
      </xs:choice>
    </xs:complexType>
  </xs:element>
</xs:schema>
<#+ 
  return this.GenerationEnvironment.ToString();
 }
} #>

For Demonstration purposes I wrote a simple T4 Template that includes the template above and uses the library I wrote in this post to get a description of the data.

<#@ template language="C#v3.5" debug="true" 
#><#@ output extension="xsd" 
#><#@ include file="DataSetFromTSqlCmd.tt" 
#><#@ assembly name="TsqlDesriptionUtil.dll" 
#><#@ import namespace="TsqlDesriptionUtil" 
#><#
    // Since I'm generating XML, I will go to extreme measures to avoid new line 
    // characters like the strange formatting of declarations above ^

    // Get the table descriptions for given T-SQL
    string conString = @"Data Source=localhost;" +
      @"Initial Catalog=Northwind;Integrated Security=True";
    string commandString = "EXEC  CustOrderHist @CustomerID='ALFKI'";
    List<TableDescription> tables = 
      TsqlCommandSchema.GetFieldDescription(conString, commandString);

    // Generate XSD:
    var gen = new DataSetFromTSqlCmd();
    gen.dsName = "MyNewDS";
    gen.tables = tables;
    Write(gen.TransformText()); 
#>

Since TsqlCommandSchema.GetFieldDescription supports multiple results sets and Stored Procedure that use temporary tables.

At work I will probably integrate this template into an existing code generation tool using my TTCommucator tool.

Monday, March 07, 2011

Boise Code Camp 2011 Presentation Notes

Sorry I’m late getting this up. I will give usual excuses of being busy.

Since I developed this talk on this blog, this entry will essentially be a link list. I put a copy of the slides in Google doc and you can reference it here

Template Parts

I presented a slide that showed the basic template parts in a small mock template:

<#@ template language="C#" #>
<#@ directive property=“value” #>
<# var item = "Statement Block"; #>
Text Block 
<#= "Expression Block" #>
<#+ 
    string ClassFeatureBlock(string thingy)
    {
        return thingy;
    } 
#>

Hello World Monty

I showed some of the elementary features of T4 templates by refactoring a basic template that would create a text file that contained the infamous words “Hello World”. A script of that exercise is here

Basic Code Generation Demonstration

To demonstrate Code Generation I set out to generate a simple T-SQL Select Statement using Sql Management Objects (SMO).

The Target Code looked something like this:

Include (SelectSpInclude.tt)
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#@ assembly name="Microsoft.SqlServer.ConnectionInfo" #>
<#@ assembly name="Microsoft.SqlServer.Smo" #>
<#@ import namespace="Microsoft.SqlServer.Management.Smo" #>
<#+
    public class SelectSpInclude: TextTransformation
    {
        public string ServerName;
        public string DbName;
        public string TableName;

        public override string TransformText()
        {
 
        Table table = getTable(ServerName, DbName, TableName);
#>
SELECT <#+ bool forgetComma = true;
 foreach (Column col in table.Columns)
{ #><#+if (!forgetComma) Write("      ,");#>[<#= col.Name #>]
<#+ forgetComma = false;
} #>
FROM <#= table.Name #><#+

        return this.GenerationEnvironment.ToString();
    } 
    Table getTable(string serverName, string dbName, string tableName)
    {
        Server server = new Server(serverName);
        Database database = new Database(server, dbName);
        Table table = new Table(database, tableName);
        table.Refresh();
        return table; 
    }
}
#>
Template (EmployeesSp.tt)
<#@ template language="C#v3.5" #>
<#@ output extension="sql" #>
<#@ include file="SelectSpInclude.tt" #>
<#
    SelectSpInclude gen = new SelectSpInclude();
    gen.ServerName = @"localhost\SQLEXPRESS";
    gen.DbName = "Northwind";
    gen.TableName = "Employees"; Write(gen.TransformText());
#>

Result (EmployeesSp.sql)
SELECT [EmployeeID]
      ,[LastName]
      ,[FirstName]
      ,[Title]
      ,[TitleOfCourtesy]
      ,[BirthDate]
      ,[HireDate]
      ,[Address]
      ,[City]
      ,[Region]
      ,[PostalCode]
      ,[Country]
      ,[HomePhone]
      ,[Extension]
      ,[Photo]
      ,[Notes]
      ,[ReportsTo]
      ,[PhotoPath]
FROM Employees

Somewhere in this part of the presentation, someone asked if it is possible to generate a series of files from a single template, a separate code file for each table in a database. Oleg Sych has a blog post that does just that here

Generating a Nullable ADO.NET DataSet Wrapper

This is when I went to Show and Tell Mode. I demonstrate a Nullable DataSet Wrapper that I wrote in these blog entries. I wrote the templates in VB.NET to generate code in C#.

Part 1, Part 2, Part 3 & Part 4

Sunday, March 06, 2011

T4 Template Tour of First Gear

At the Boise Code Camp, I lead off with a demonstration of some basic features of T4 Templates. The point of the presentation is to get you from no T4 to the level of most blog examples. I got good reviews from the few who attended my session, so I thought I would attempt to write it down. Here goes…

The Script

First, create a Text file and change the extension to "tt". We’ll call it "Hello.tt". I will add a template directive and tell T4 that I want to use C#. I add the text "Hello World" below the directive. When I save it, "Hello.cs" appears below the template.

Template (Hello.tt):
<#@ template language="C#" #>
Hello World
Output (Hello.cs):
Hello World

Also notice that Visual Studio complains that Hello.cs won’t compile. For this demonstration, I don’t want to create a C# code file, so I will tell the template to create a "txt" file with an output directive.

Template (Hello.tt):
<#@ template language="C#" #>
<#@ output extension="txt" #>
Hello World
Output (Hello.txt):
Hello World

Visual Studio is happy because it doesn’t try to compile "txt" files. (Since it doesn't change, I won't repeat the output again.)

Now, let’s add some actual C# template code, I’m going to create a variable called "name" in a Statement Block and reference it in an Expression Block.

Template (Hello.tt):
<#@ template language="C#" #>
<#@ output extension="txt" #>
<# string name = "World"; #>
Hello <#= name #>

Now I’m going to replace the variable with a function. The function will need to be in a Class Feature Block. Just to speed things up, I’m going to use a Write call in a Statement Block to render the name. You can write text to the output using either Expression Blocks OR calls to Write().

Template (Hello.tt):
<#@ template language="C#" #>
<#@ output extension="txt" #>
Hello <# Write(GetName()); #>
<#+ 
    string getName()
    {
        return "Word";
    }
#>

Now I want to move my function into an include file. I will create a new text file and call it "HelloFunctions.tt" I move the Class Feature Block into that file. Notice that there is no template directive. Also, you must save the include file BEFORE you save the template file.

Include (HelloFunctions.tt)
<#+ 
    string getName()
    {
        return "Word";
    }
#>
Template (Hello.tt):
<#@ template language="C#" #>
<#@ include file="HelloFunctions.tt" #>
<#@ output extension="txt" #>
Hello <# Write(GetName()); #>

Now, for a brief look under the hood, I can show you the source code for the Text Transformation. Here I add "debug="true"" to the template file, save it and look in the temp directory (you can get your temp directory by typing "set temp" in a command prompt)

Template (Hello.tt):
<#@ template language="C#" #>
<#@ include file="HelloFunctions.tt" #>
<#@ output extension="txt" #>
Hello <# 
// This comment will show up in .cs file
Write(GetName()); 
#>
Generated C# File (hcs3svyj.0.cs in this case)
namespace Microsoft.VisualStudio.TextTemplatingBFD11436D0A4595408B06EA4E1B2A636 {
    using System;
    using Microsoft.VisualStudio.TextTemplating.VSHost;

    
    #line 1 "c:\vsprojects\SimpleT4\Hello.tt"
    public class GeneratedTextTransformation : 
        Microsoft.VisualStudio.TextTemplating.TextTransformation {
        public override string TransformText() {
            try {
                this.Write("Hello ");

                #line 4 "c:\vsprojects\SimpleT4\Hello.tt"

// This comment will show up in .cs file
Write(getName()); 

          
                #line default
                #line hidden
            }
            catch (System.Exception e) {
                System.CodeDom.Compiler.CompilerError error = new 
                    System.CodeDom.Compiler.CompilerError();
                error.ErrorText = e.ToString();
                error.FileName = "c:\\vsprojects\\SimpleT4\\Hello.tt";
                this.Errors.Add(error);
            }
            return this.GenerationEnvironment.ToString();
        }

        #line 1 "c:\vsprojects\SimpleT4\HelloFunctions.tt"

 string getName()
 {
  return "World";
 }

  
        #line default
        #line hidden
    }

    #line default
    #line hidden
}

These source files have helped me understand what is going on behind the scenes. For example, I didn’t really get the difference between Statement Blocks (<# #>) and Class Feature Blocks (<#+ #>) until I saw the generated.

Afterthought

This isn’t a transcription! Written and spoken communication is different by nature.

In the presentation I would change the value of "World" with different words ("Earth", "Boise", "Boise Code Camp", etc); In a presentation, this added proofiness that the templates were run and some lame humor to lighten the mood of the talk. In the written form, it would force me to repeat the Output after each sample (and make this longer).

Tuesday, February 22, 2011

Boise Code Camp: Code Generation with T4 Templates

I will be presenting at the Boise Code Camp on Saturday, February 26, 2011, 9:30 AM in the Farnsworth room in the BSU SUB!

T4 Templates (Text Template Transformation Toolkit) is the code generator built into Visual Studio. This session is a basic introduction to code generation in C# and .NET using T4 Templates in Visual Studio 2008. I will takes you on a journey starting from the basic "Hello World" template into a world where you can wrote reusable templates and interrogate SQL Server or an ADO.NET DataSet for table descriptions.

Check it out!

Wednesday, February 16, 2011

Four Styles of T4 Template Code

As I work through T4 templates as I get ready for Boise Code Camp, I’ve noticed a few styles of template code. Just for kicks, I am going to name them and give an example of each using a code generation that I blogged about early last year. Each template uses an include file that I present after the four samples. Each template relies on its style to the point of absurdity; I didn’t go as far as to eliminate all text blocks.

Real life templates are mixtures of these styles applied at appropriate places.

I adapted the code generation that I did here

Embed

This can also be called the Classic ASP style. All generated code is added as text blocks or expression blocks (<#= #>). The logic is contained in code blocks mixed in with the text and expression blocks. For simple logic, this style is probably OK. It can be difficult to read and maintain as the logic gets more complex. This is the most concrete approach.

<#@ template language="C#v3.5" debug="true" #>
<#@ output extension="sql" #>
<#@ include file="TableDescriptionUtilCS.tt" #>
<#
    string ConnectionString = @"Data Source=localhost\SQLEXPRESS;" + 
        "Initial Catalog=Northwind;Integrated Security=True";
    string Table = "Employees"; 

    TableDescriptionUtil tdu = new TableDescriptionUtil();
    List<TableDescription> Columns = tdu.GetTableDescription(ConnectionString, Table); 

    int counter;
#>
CREATE PROCEDURE [dbo].[Insert<#=Table #>]
<#
    counter = 1;
    foreach (TableDescription item in Columns)
    {
        #>    @<#= item.ColName #> <#= item.FullTypeName #><#
        if (item.Status == 0x80)
            #> OUT<#
        if (item.ColIsNullable != 0) 
            #> = NULL<#
        if (counter < Columns.Count)
            #>,<#
        counter++;
        #><#= "\r\n"#><#
    }
#>

AS

INSERT INTO [<#=Table #>] (<#
    counter = 1;
    foreach (TableDescription item in Columns)
    {
        if (item.Status != 0x80)
        {
            if (counter > 1)
                #>, <#
            if (counter % 5 == 0)
                #><#= "\r\n    " #><#
            counter++;
            #>[<#= item.ColName #>]<#
        }
    }
#>)
VALUES (<#
    counter = 1;
    foreach (TableDescription item in Columns)
    {
        if (item.Status != 0x80)
        {
            if (counter > 1)
                #>, <#
            if (counter % 5 == 0)
                #><#= "\r\n    " #><#
            counter++;
            #>@<#= item.ColName #><#
        }
    }
#>)

SET @<#
    foreach (TableDescription item in Columns)
    {
        if (item.Status == 0x80)
        {
            #><#=item.ColName #><#
            break;
        }
    }
#> = @@IDENTITY

Inline Write

This style uses code blocks that contain Write(), WriteLine(), PushIndent(), PopIndent(), and other statements.

This approach is more abstract that Embed, it is easier for me to read than Embed or String Function.

<#@ template language="C#v3.5" debug="true" #>
<#@ output extension="sql" #>
<#@ include file="TableDescriptionUtilCS.tt" #>
<# 
    string ConnectionString = @"Data Source=localhost\SQLEXPRESS;" +
        Initial Catalog=Northwind;Integrated Security=True";
    string Table = "Employees";

    TableDescriptionUtil tdu = new TableDescriptionUtil();
    List<TableDescription> Columns = tdu.GetTableDescription(ConnectionString, Table);

    int counter;
#>
CREATE PROCEDURE [dbo].[Insert<# Write(Table); #>]
<#
    PushIndent("    ");
    counter = 1;
    foreach (TableDescription item in Columns)
    {
        Write("@" + item.ColName + " " + item.FullTypeName);
        if (item.Status == 0x80)
            Write(" OUT");
        if (item.ColIsNullable != 0)
            Write(" = NULL");
        if (counter < Columns.Count)
            WriteLine(",");
        counter++;
    }
    PopIndent();
#>


AS

INSERT INTO [<# Write(Table); #>] (<#
    PushIndent("    ");
    counter = 1;
    foreach (TableDescription item in Columns)
    {
        if (item.Status != 0x80)
        {
            if (counter > 1)
                Write(", ");
            if (counter % 5 == 0)
                WriteLine(String.Empty);
            counter++;
            Write(string.Format("[{0}]", item.ColName));
        }
    }
    PopIndent();
#>)
VALUES (<#
    PushIndent("    ");
    counter = 1;
    foreach (TableDescription item in Columns)
    {
        if (item.Status != 0x80)
        {
            if (counter > 1)
                Write(", ");
            if (counter % 5 == 0)
                WriteLine(string.Empty);
            counter++;
            Write(String.Format("@{0}", item.ColName));
        }
    }
    PopIndent();
#>)

SET @<#
    foreach (TableDescription item in Columns)
    {
        if (item.Status == 0x80)
        {
            Write(item.ColName);
            break;
        }
    }
#> = @@IDENTITY

String Function

Here I call a function in an expression block that returns a bit of generated code as a string. The functions are in feature blocks (or include file, linked assemblies, etc.).

Since I did some primitive string based code gen for a number of years, I find myself relying on this approach. This approach can lead to code just as ugly as the Embed approach. This approach is good for adapting old fashioned code gen to T4.

<#@ template language="C#v3.5" debug="true" #>
<#@ output extension="sql" #>
<#@ include file="TableDescriptionUtilCS.tt" #>
<# 
    string ConnectionString = @"Data Source=localhost\SQLEXPRESS;" + 
        "Initial Catalog=Northwind;Integrated Security=True";
    string Table = "Employees";

    TableDescriptionUtil tdu = new TableDescriptionUtil();
     List<TableDescription> Columns = tdu.GetTableDescription(ConnectionString, Table);

     int counter;
#>
CREATE PROCEDURE [dbo].[Insert<#= getTableName(Table) #>]
<#= getParameterList(Columns) #>

AS

INSERT INTO [<#= getTableName(Table) #>] (<#= getFieldNames(Columns) #>)
VALUES (<#= getValueNames(Columns) #>)


SET @<#=getIdentityFieldName(Columns) #> = @@IDENTITY
<#+
    string getTableName(string table)
    {
        // Yes, this is hard core overkill
        return table;
    }
    string getParameterList(List<TableDescription> columns)
    {
        string rVal = string.Empty;
        int counter = 1;
        foreach (TableDescription item in columns)
        {
           rVal += "    @" + item.ColName + " " + item.FullTypeName;
           if (item.Status == 0x80)
               rVal += " OUT";
           if (item.ColIsNullable != 0)
               rVal += " = NULL";
           if (counter < columns.Count)
               rVal += ",\r\n";
           counter++;
       }
       return rVal;
    }
    string getFieldNames(List<TableDescription> columns)
    {
        string rVal = string.Empty;
        int counter = 1;
        foreach (TableDescription item in columns)
        {
            if (item.Status != 0x80)
            {
                if (counter > 1)
                    rVal += ", ";
                if (counter % 5 == 0)
                    rVal += "\r\n    ";
                counter++;
                rVal += string.Format("[{0}]", item.ColName);
           }
        }
        return rVal;
    }
    string getValueNames(List<TableDescription> columns)
    {
        string rVal = string.Empty;
        int counter = 1;
        foreach (TableDescription item in columns)
        {
        if (item.Status != 0x80)
        {
            if (counter > 1)
                rVal += ", ";
            if (counter % 5 == 0)
                rVal += "\r\n    ";
            counter++;
            rVal += string.Format("@{0}", item.ColName);
        }
    }
    return rVal;
    }
    string getIdentityFieldName(List<TableDescription> columns)
    {
        string rVal = string.Empty;
        foreach (TableDescription item in columns)
        {
            if (item.Status == 0x80)
            {
                rVal =item.ColName;
                break;
            }
        }
        return rVal;
    }
#>

Write Function

Here I call a function to generate code a block of code with Write functions.

Unlike the String Function approach, the code generation is relying on a side effect of the function, so this approach isn’t functional.

<#@ template language="C#v3.5" debug="true" #>
<#@ output extension="sql" #>
<#@ include file="TableDescriptionUtilCS.tt" #>
<# 
    string ConnectionString = @"Data Source=localhost\SQLEXPRESS;" + 
        "Initial Catalog=Northwind;Integrated Security=True";
    string Table = "Employees";

    TableDescriptionUtil tdu = new TableDescriptionUtil();
    List<TableDescription> Columns = tdu.GetTableDescription(ConnectionString, Table);

    int counter;
#>
CREATE PROCEDURE [dbo].[Insert<# writeTableName(Table); #>]
<# writeParameterList(Columns); #>

AS

INSERT INTO [<# writeTableName(Table); #>] (<# writeFieldNames(Columns); #>)
VALUES (<# writeValueNames(Columns); #>)


SET @<# writeIdentityFieldName(Columns); #>  = @@IDENTITY
<#+
    void writeTableName(string table)
    {
        Write(table);
    }
    void  writeParameterList(List<TableDescription> columns)
    {
        PushIndent("    ");
        int counter = 1;
        foreach (TableDescription item in columns)
        {
            Write("@" + item.ColName + " " + item.FullTypeName);
            if (item.Status == 0x80)
                Write(" OUT");
            if (item.ColIsNullable != 0)
                Write(" = NULL");
            if (counter < columns.Count)
                WriteLine(",");
            counter++;
        }
        PopIndent();
    }
    void writeFieldNames(List<TableDescription> columns)
    {
         PushIndent("    ");
         int counter = 1;
         foreach (TableDescription item in columns)
         {
             if (item.Status != 0x80)
             {
                 if (counter > 1)
                     Write(", ");
                 if (counter % 5 == 0)
                     WriteLine(String.Empty);
                 counter++;
                 Write(string.Format("[{0}]", item.ColName));
             }
         }
         PopIndent();
    }
    void writeValueNames(List<TableDescription> columns)
    {
        PushIndent("    ");
        int counter = 1;
        foreach (TableDescription item in columns)
        {
            if (item.Status != 0x80)
            {
                if (counter > 1)
                    Write(", ");
                if (counter % 5 == 0)
                    WriteLine(string.Empty);
                counter++;
                Write(String.Format("@{0}", item.ColName));
            }
        }
        PopIndent();
    }
    void writeIdentityFieldName(List<TableDescription> columns)
    {
        foreach (TableDescription item in columns)
        {
            if (item.Status == 0x80)
            {
                Write(item.ColName);
                break;
            }
        }
    }
#>

Include File: TableDescriptionUtil.tt

I included this file in all the examples above.

<#@ assembly name="System.Data.dll" #>
<#@ import namespace="System.Data.SqlClient" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#+
public class TableDescriptionUtil : TextTransformation
{
    /// <remarks>
    /// I don't actually call this, but it is necessary for T4 Templates to work
    /// </remarks>
    public override string TransformText()
    {
        return "";
    }
    /// <summary>
    /// Gets a DataReader containing selected column information for all of the
    /// columns in the indecated table from SQL Server System Tables
    /// </summary>
    /// <param name="tableName">Get information on THIS table</param>
    /// <param name="conn">An existing open connection to be used </param>
    /// <returns>A DataReader pointing to the results</returns>
    private SqlDataReader GetTableDefReader(string tableName , SqlConnection conn )
    {
        SqlDataReader reader;// = new SqlDataReader();
        string getDataForTable = 
             " SELECT c.name AS col_name, " +
             "   t.name AS type_name, " +
             "   CAST(c.length AS INT) AS length, " +
             "   c.isnullable, " +
             "   CAST(c.status AS INT) AS status " +
             " FROM " +
             "  sysObjects o " +
             "  JOIN sysColumns c ON c.id = o.id " +
             "  JOIN systypes t on c.xtype = t.xtype AND t.status = 0 " +
             " WHERE o.name = @tableName " +
             " ORDER BY c.colid ";
        using (var cmd = new SqlCommand(getDataForTable, conn))
        {
            cmd.Parameters.AddWithValue("@tableName", tableName);
            reader = cmd.ExecuteReader();
        }
        return reader;
    }

    /// <summary>
    /// Gets a List of column descriptions for a given table
    /// </summary>
    /// <param name="ConnectionString">Connection string to the database</param>
    /// <param name="Table">The name of the table to describe</param>
    /// <returns>A list of column descriptions</returns>
    public List<TableDescription> GetTableDescription(string ConnectionString, 
         string Table) 
    {
        SqlDataReader reader;
        List<TableDescription> Columns = new List<TableDescription>();
        using (var conn = new SqlConnection(ConnectionString))
        {
            conn.Open();
            reader = GetTableDefReader(Table, conn);
            while (reader.Read())
            {
                TableDescription Column = new TableDescription();
                Column.ColName = reader.GetString(0);
                Column.TypeName = reader.GetString(1);
                Column.ColLength = reader.GetInt32(2);
                Column.ColIsNullable = reader.GetInt32(3);
                Column.Status = reader.GetInt32(4);
                Column.FullTypeName = GetTypeName(Column.TypeName, Column.ColLength);
                Columns.Add(Column);
            }
            conn.Close();
        }
        return Columns;
    }
    /// <summary>
    /// Gets the TypeName as it should appear in a SP Parameter List
    /// </summary>
    /// <param name="TypeName"></param>
    /// <param name="ColLength"></param>
    /// <returns>The processed Type Name as a string</returns>
    private string GetTypeName(string typeName , int ColLength )
    {
        switch (typeName.ToLower())
        {
        case "varchar":
        case "nvarchar":
            string lenString  = ColLength.ToString();
            if (ColLength == -1 )
                lenString = "MAX";
            return string.Format("{0}({1})", typeName, lenString);
        default:
            return typeName;
        }
    }
}
public class TableDescription
{
    public string ColName {get; set;}
    public string TypeName {get; set;}
    public string FullTypeName {get; set;}
    public int ColLength {get; set;}
    public int Status {get; set;}
    public int ColIsNullable {get; set;}
}
#>

Sunday, January 23, 2011

Referencing Assemblies: T4 Gotcha

When I run T4 Templates through Visual Studio 2008, I don't have to give the full path to reference GAC assemblies:

<#@ assembly name="System.Data.dll" #>

But when I run the template in my own executable using Microsoft.VisualStudio.TextTemplating.Engine, I have problems.

If the Template code is in VB.NET, I get:

vbc : Command line (0,0) : error BC2006: Compiling transformation: option 'r' requires ': <file_list>'

If the Template code is in C#, I get:

c:\Users\jacks\AppData\Local\Temp\rnftuopt.0.cs(3,18) : error CS0234: Compiling transformation: The type or namespace name 'Data' does not exist in the namespace 'System' (are you missing an assembly reference?)

To fix this, I referenced the full path of the assembly.

<#@ assembly name="C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.Data.dll" #>

NOTE: This doesn't seam to apply to non-GAC assemblies; I have noticed that I don't have give the full path of my own assemblies as long as they are in the same folder as my executable.

Sunday, December 19, 2010

Generating a Nullable ADO.NET DataSet Wrapper

Part 4: The Template and putting it all together

So, I have my target code, my DataSet describer and some T4 template tools (including the ITextTemplatingEngineHost class I took from MSDN), but I haven’t generated any DataSet Wrapper code! It is Christmas time, but that’s still a lame excuse.

Running the Template

In Part 3, I created a class to pass parameters to a T4 Template and an overly simple template that echoed the parameters to the generated document. Here I’m using the same basic from the Test Console Application, but I am referencing a template that will generate the DataSet Wrapper and passing the appropriate parameters to generate the wrapper I want. This code use a DataSet in an assembly.
// Set up some parameters:
var parms = new TTCommucator.GetParams("setting.txt");
parms.CurrentParameters.Add("DsSource", "Assembly");
parms.CurrentParameters.Add("AssembyPath", @"C:\GeneratorProject\MyAssembly.dll");
parms.CurrentParameters.Add("DataSetName", "MyDS");
parms.CurrentParameters.Add("FileNameSpace", "MyAssembly.DataSets");
parms.CurrentParameters.Add("DsNameSpace", "NewProject.DataSetWrappers");
parms.Save();

// Set the Template File
string templateFileName = "DataSetX.tt";

// Set up host and engine
CustomCmdLineHost host = new CustomCmdLineHost();
Engine engine = new Engine();
host.TemplateFileValue = templateFileName;

//Read the text template.
string input = File.ReadAllText(templateFileName);

//Transform the text template.
string result = engine.ProcessTemplate(input, host);
// Save logic would be here…

Notes on the Template

In the template, I am using TTCommucator.GetParams to read the parameter I’ve written in code above; same as in Part 3. I use the DataSetDesciber.ReflectDataSet.DataSetDef from Part 2 to get the tables, columns, types, etc. from the target DataSet. The generated code will be different from the target code in Part 1. For example:
  • I’m using .NET type names (like System.Int32) instead of C# type names (like int). That’s what Type.ToString() returns and I see no reason to convert it just to look pretty.
  • I’m not using Nullable’s ? operator. No good reason, it just seems more consistent with the .NET types in code (I don’t see System.Int32? in code all that often).
The purpose of the target code is to provide a starting point for generation. The code will change as a result of what you learn in writing the generation and testing. In the Template I am generating C# code, but the template code is in VB.NET; it is easier for me to keep straight which is which.

The Template

<#@ template debug="true" hostspecific="true" language="VB" #>
<#@ output extension=".cs" #>
<#@ assembly name="C:\GeneratorProject\TTCommucator.dll" #>
<#@ assembly name="C:\GeneratorProject\DataSetDesciber.dll" #>
<#@ import namespace="DataSetDesciber.ReflectDataSet" #>
<#@ import namespace="TTCommucator" #>
<# 
    ' Load the Parameter object
    Dim parms As new GetParams(Host.ResolvePath("setting.txt"))
    parms.Load()

    ' Get the namespace
    Dim FileNameSpace As String = parms.CurrentParameters.Item("FileNameSpace")
    Dim DsNameSpace As String = parms.CurrentParameters.Item("DsNameSpace")

    ' a description of the DataSet to be wrapped
    Dim refx As DataSetDef = GetDsDescripter(parms)
#>
// Generated with DataSetWrapper Version 1
using System;
using System.Collections;
using System.Collections.Generic;
using <#=DsNameSpace #>;

namespace <#=FileNameSpace #>
{
    public class <#=refx.DataSetName #>X
    {
        private <#=refx.DataSetName #> _ds;
        public <#=refx.DataSetName #>X(<#=refx.DataSetName #> ds)
        {
            _ds = ds;
        }
<#For Each table As TableDef In refx.TableDefList #>
        public <#=table.TableName#>DataTableX <#=table.TableName#>
        {
            get { return new <#=table.TableName#>DataTableX(_ds.<#=table.TableName#>); }
            //set { _ds.<#=table.TableName#> = value; }
        }
<#Next#>
        public <#=refx.DataSetName #> DataSet
        {
            get { return _ds; }
        }
    }

<#For Each table As TableDef In refx.TableDefList #>
    public class <#=table.TableName#>DataTableX: IEnumerable<<#=table.TableName#>RowX>
    {
        private <#=refx.DataSetName #>.<#=table.TableName#>DataTable _theTable;
        public <#=table.TableName#>DataTableX(<#=refx.DataSetName #>.<#=table.TableName#>DataTable table)
        {
            _theTable = table;
        }

        public <#=refx.DataSetName #>.<#=table.TableName#>DataTable Table
        {
            get { return _theTable; }
        }

        #region IEnumerable<<#=table.TableName#>RowX> Members

        public IEnumerator<<#=table.TableName#>RowX> GetEnumerator()
        {
            //return new <#=table.TableName#>RowXEnum(_rows);
            return new <#=table.TableName#>TableEnum(_theTable);
        }

        #endregion

        #region IEnumerable Members

        IEnumerator IEnumerable.GetEnumerator()
        {
            return (IEnumerator)this;
        }

        #endregion
    }

    public class <#=table.TableName#>TableEnum : IEnumerator<<#=table.TableName#>RowX>
    {
        private <#=refx.DataSetName #>.<#=table.TableName#>DataTable _collection;
        private int curIndex;
        private <#=table.TableName#>RowX curBox;

        public <#=table.TableName#>TableEnum(<#=refx.DataSetName #>.<#=table.TableName#>DataTable collection)
        {
            _collection = collection;
            curIndex = -1;
            curBox = default(<#=table.TableName#>RowX);
        }

        public bool MoveNext()
        {
            //Avoids going beyond the end of the collection.
            if (++curIndex >= _collection.Count)
                return false;
            else
                // Set current <#=table.TableName#>RowX to next item in collection.
                curBox = new <#=table.TableName#>RowX((<#=refx.DataSetName #>.<#=table.TableName#>Row)_collection.Rows[curIndex]);
            return true;
        }

        public void Reset() { curIndex = -1; }

        public void Dispose() { }

        public <#=table.TableName#>RowX Current
        {
            get { return curBox; }
        }

        object IEnumerator.Current
        {
            get { return curBox; }
        }
    }

    public class <#=table.TableName#>RowX
    {
        <#=refx.DataSetName #>.<#=table.TableName#>Row _row;
        public <#=table.TableName#>RowX(<#=refx.DataSetName #>.<#=table.TableName#>Row row)
        {
            _row = row;
        }
        /// <summary>
        /// The wrapped DataRow
        /// </summary>
        public <#=refx.DataSetName #>.<#=table.TableName#>Row Row
        {
            get { return _row; }
        }
        #region Properties
        <#For Each column As ColumnDef In table.ColumnDefList#>
            <#If column.AllowDBNull Then#>


        public <#=GetNullableType(column.DataType.ToString())#> <#=column.ColumnName#>
        {
            get
            {
                if (_row.Is<#=column.ColumnName#>Null())
                    return null;
                else
                    return _row.<#=column.ColumnName#>;
            }
            set
            {
                if (<#=GetHasValueFunction(column.DataType.ToString())#>)
                    _row.Set<#=column.ColumnName#>Null();
                else
                    _row.<#=column.ColumnName#> =  
                        <#=GetNullableValueName(column.DataType.ToString())#>;
            }
        }
        <#Else#>

        public <#=column.DataType.ToString()#> <#=column.ColumnName#>
        {
            get { return _row.<#=column.ColumnName#>; }
            set { _row.<#=column.ColumnName#> = value; }
        }
        <#End If#>
    <#Next#>

        #endregion Properties
    }
<#Next#>
}
<#+ 
    ''' <summary>
    ''' Gets a DataSetDef bases on parms passed in
    ''' </summary>
    ''' <param name="parms">
    ''' The GetParms object containing parameters needed to create DataSetDef:<br />
    ''' AssembyPath: The path of the assembly containig the DataSet<br />
    ''' DataSetName: The fully qualified name of the dataset
    ''' </param>
    ''' <returns>The DataSetDef</returns>
    Function GetDsDescripter(parms As GetParams) AS DataSetDef
        Dim AssembyPath As String = parms.CurrentParameters.Item("AssembyPath")
        Dim DataSetName As String = parms.CurrentParameters.Item("DataSetName")
        Return DataSetReflector.GetDataSetDef(AssembyPath, DataSetName)
    End Function
    ''' <summary>
    ''' Gets the nullable type name for a given type name
    ''' </summary>
    ''' <param name="typeName">
    ''' The type name as a string (as returned from Type.ToString())</param>
    ''' <returns>The Nullable type name</returns>
    ''' <remarks>
    ''' Since strings are nullable and not compatible with Generic Nullable class, 
    ''' must be treated special 
    ''' </remarks>
    Function GetNullableType(typeName As String) As String
        Dim rVal as string
        Select Case typeName
        Case "System.String"
            rVal = typeName
        Case Else
            rVal = "Nullable<" & typeName & ">"
        End Select
        Return rVal
    End Function
    ''' <summary>
    ''' Gets the string used to check nullablity in a set property method
    ''' </summary>
    ''' <param name="typeName">
    ''' The type name as a string (as returned from Type.ToString())
    ''' </param>
    ''' <returns>
    ''' string of an expression that will return true if value is null
    ''' </returns>
    ''' <remarks>
    ''' Most nullable types have a HasValue property but not all
    ''' </remarks>
    Function GetHasValueFunction(typeName As String) As String
        Dim rVal as string
        Select Case typeName
        Case "System.String"
            rVal = "System.String.IsNullOrEmpty(value)"
        Case Else
            rVal = "!value.HasValue"
        End Select
        Return rVal
    End Function
    ''' <summary>
    ''' Gets the string used on the right side of set property method
    ''' </summary>
    ''' <param name="typeName">
    ''' The type name as a string (as returned from Type.ToString())
    ''' </param>
    ''' <returns>The Value property name</returns>
    ''' <remarks>
    ''' Most nullable types have a Value property but not all
    ''' </remarks>
    Function GetNullableValueName(typeName As String) As String
        Dim rVal as string
        Select Case typeName
        Case "System.String"
            rVal = "value"
        Case Else
            rVal = "value.Value"
        End Select
        Return rVal
    End Function
#>

Generating a Nullable ADO.NET DataSet Wrapper: The Series

Thursday, December 09, 2010

Generating a Nullable ADO.NET DataSet Wrapper

Part 3: Detour hacking parameters for T4 Templates (VS 2008)

I decided to use T4 Templates instead of my home grown code generation techniques. I didn’t want to spend several pages on my way; besides, I probably need to get with the times and play with the same toys as the cool kids.

This post is to describe a punt. I couldn’t find a way to pass parameters to a VS 2008 .tt file. I know that VS 2010 has the parameter directive, but I don’t have VS 2010, so thus the hack below.

TTCommucator.GetParams

Since I can reference .NET assemblies, I am storing my parameters as a Dictionary to a file outside of the template and then reading it in from inside the template.

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

namespace TTCommucator
{
    /// <summary>
    /// Used to pass parameters to a T4 Template
    /// </summary>
    public class GetParams
    {
        private const char SEPERATOR_CHAR = '|';
        private string _fileName;
        /// <summary>
        /// The Dictionary containing the Parameters
        /// </summary>
        public Dictionary<string, string> CurrentParameters { get; set; }
        /// <summary>
        /// Creates a GetParmas with a Filename
        /// </summary>
        /// <param name="fileName">The name of the file to store the parameters</param>
        /// <remarks>Both sides of the transaction must agree on file name</remarks>
        public GetParams(string fileName)
        {
            CurrentParameters = new Dictionary<string, string>();
            this._fileName = fileName;
        }
        /// <summary>
        /// Loads the data from the file into the Dictionary CurrentParameters 
        /// </summary>
        public void Load()
        {
            CurrentParameters = new Dictionary<string, string>();
            string line;
            using (TextReader tr = new StreamReader(_fileName))
            {
                while ((line = tr.ReadLine()) != null)
                {
                    string[] parts = line.Split(new char[] { SEPERATOR_CHAR }, 2);
                    if (parts.Count() > 1)
                        CurrentParameters.Add(parts[0], parts[1]);
                }
                tr.Close();
            }
        }
        /// <summary>
        /// Saves the data in the Dictionary CurrentParameters to the file
        /// </summary>
        public void Save()
        {
            using (TextWriter tw = new StreamWriter(_fileName))
            {
                foreach (var item in CurrentParameters)
                {
                    tw.WriteLine(item.Key + SEPERATOR_CHAR + item.Value);
                }
                tw.Close();
            }
        }
    }
}

Test Template

I created a simple template to demonstrate that I can receive parameters using my GetParams class. Note that I use VB.NET for code in my templates. Since I usually generate C# code, I find it easier to read templates when the template code isn’t C#, besides “<#}#>” is downright nasty looking.

<#@ template debug="true" hostspecific="true" language="VB" #>
<#@ assembly name="C:\Projects\TTCommucator\TTCommucator.dll" #>
<#@ import namespace="TTCommucator" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".txt" #>
<# 
 ' Load the Parameters here:
 Dim parms As new GetParams(Host.ResolvePath("setting.txt"))
 parms.Load()
#>
Values in parms:
<#For Each item As KeyValuePair(Of String, String) In parms.CurrentSettings#>
    <#=item.Key#> = <#=item.Value#>
<#Next#>

Test Console Application

Here is a simple program to test with. It doesn’t do anything more than necessary. NOTE: You will need to install Visual Studio 2008 SDK v1.1 to run this code:

using System;
using System.IO;
using Microsoft.VisualStudio.TextTemplating;
using TTCommucator;

namespace TTCommunicatorConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            // Set up some parameters:
            var parms = new GetParams("setting.txt");
            parms.CurrentParameters.Add("Greeting", "Hello World!");
            parms.CurrentParameters.Add("Run Time", DateTime.Now.ToString());
            parms.Save();

            string templateFileName = "SimpleTemplate.tt";

            // Set up host and engine
            CustomCmdLineHost host = new CustomCmdLineHost();
            Engine engine = new Engine();
            host.SetTemplateFile(templateFileName);

            //Read the text template.
            string input = File.ReadAllText(templateFileName);

            //Transform the text template.
            string output = engine.ProcessTemplate(input, host);

            // Show your work
            Console.WriteLine(output);

            // Wait for me
            Console.ReadKey();
        }
    }
}

Host Class

I lifted this class from MSDN and removed the comments to save space. You can learn more about it by going here. NOTE: The MSDN code doesn't include the Serializable attribute, but I needed to add it to avoid errors. You will need to include a reference to Microsoft.VisualStudio.TextTemplating.

using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.VisualStudio.TextTemplating;

namespace TTCommucator
{
    [Serializable]
    public class CustomCmdLineHost : ITextTemplatingEngineHost
    {
        internal string _templateFile;
        public string TemplateFile
        {
            get { return _templateFile; }
        }
        private string _fileExtension = ".txt";
        public string FileExtension
        {
            get { return _fileExtension; }
        }
        private Encoding _fileEncoding = Encoding.UTF8;
        public Encoding FileEncoding
        {
            get { return _fileEncoding; }
        }
        private CompilerErrorCollection _errors;
        public CompilerErrorCollection Errors
        {
            get { return _errors; }
        }
        public IList StandardAssemblyReferences
        {
            get { return new string[] { typeof(System.Uri).Assembly.Location }; }
        }
        public IList StandardImports
        {
            get { return new string[] { "System" }; }
        }
        public bool LoadIncludeText(string requestFileName, out string content, 
            out string location)
        {
            content = string.Empty;
            location = string.Empty;
            if (File.Exists(requestFileName))
            {
                content = File.ReadAllText(requestFileName);
                return true;
            }
            else
                return false;
        }
        public object GetHostOption(string optionName)
        {
            object returnObject;
            switch (optionName)
            {
                case "CacheAssemblies":
                    returnObject = true;
                    break;
                default:
                    returnObject = null;
                    break;
            }
            return returnObject;
        }
        public string ResolveAssemblyReference(string assemblyReference)
        {
            if (File.Exists(assemblyReference))
                return assemblyReference;
            string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), 
                assemblyReference);
            if (File.Exists(candidate))
                return candidate;
            return string.Empty;
        }
        public Type ResolveDirectiveProcessor(string processorName)
        {
            if (string.Compare(processorName, "XYZ", StringComparison.OrdinalIgnoreCase) 
                == 0)
            {
            }
            throw new Exception("Directive Processor not found");
        }
        public string ResolvePath(string fileName)
        {
            if (fileName == null)
                throw new ArgumentNullException("the file name cannot be null");
            if (File.Exists(fileName))
                return fileName;
            string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), 
                fileName);
            if (File.Exists(candidate))
                return candidate;
            return fileName;
        }
        public string ResolveParameterValue(string directiveId, string processorName, 
            string parameterName)
        {
            if (directiveId == null)
                throw new ArgumentNullException("the directiveId cannot be null");
            if (processorName == null)
                throw new ArgumentNullException("the processorName cannot be null");
            if (parameterName == null)
                throw new ArgumentNullException("the parameterName cannot be null");
            return string.Empty;
        }
        public void SetFileExtension(string extension)
        {
            _fileExtension = extension;
        }
        public void SetTemplateFile(string templateFile)
        {
            _templateFile = templateFile;
        }
        public void SetOutputEncoding(Encoding encoding, 
            bool fromOutputDirective)
        {
            _fileEncoding = encoding;
        }
        public void LogErrors(CompilerErrorCollection errors)
        {
            _errors = errors;
        }
        public AppDomain ProvideTemplatingAppDomain(string content)
        {
            return AppDomain.CreateDomain("Generation App Domain");
        }
    }
}

Generating a Nullable ADO.NET DataSet Wrapper: The Series