Text Template Transformation Toolkit (T4), Part 2: Template Generators
Greetings, Habr!
This article will continue the topic of automatic code generation in Visual Studio with T4 - Text Template Transformation Toolkit. Part number 1 , a little earlier published on this blog, described the T4 syntax and elementary principles of its use. At the same time, I decided to take a closer look at the blog of the respected Oleg Sych and adapt some of his achievements to the habra audience even a little more. In particular, today we will discuss the following topics:
I did not invent any special examples. The history of the development of a query with the creation of a stored procedure described by Oleg is ideal for illustrating a problem that might require generators. Moreover, which is characteristic, is not a far-fetched problem. Also, the article adheres to the principle of "less words - more code."
Hereinafter, it is assumed that you have installed Visual Studio 2005/2008/2010, as well as T4 Toolbox .
So we have a database. There is a table in this database. And in the table, in the best traditions of Koshchei the Immortal, there is a line that we want to delete. And to delete not just like that, but with a freshly created stored procedure. For simplicity, let the parameters passed to the procedure - all to a single field in the table, by which the rows to be deleted are identified. Filling a request to create such a procedure manually is a thankless job, so we use T4 and create a code similar to the one below:
Here, SQL Server Management Objects (SMO) are used to get information about table fields from the database of the local SQL server . After saving the file to the output, we get the search request. For example, it might look like this:
The first question that should arise in the head of a person who has looked at this code is: why are the database names and tables hard-coded into the code? It turns out that to create each new request, the programmer will have to crawl into the template and change it manually in several places?
Not really necessary. It is enough to use another method of generation. We add a new file to the project, now using the Template option instead of the File from the T4 Toolbox, let's name it, for example, DeleteProcedureTemplate.tt. As you can see, the environment automatically created a blank of a parameterized template, that is, a file, which we will then include in other templates in order to use it in a generalized form.
Do not try to find the Template class in the Microsoft.VisualStudio.TextTemplating namespace: it is not there. This is an abstract class defined in T4Toolbox, and it makes no sense to connect the T4Toolbox.tt file directly in the parameterized templates themselves. Therefore, each time you save DeleteProcedureTemplate.tt, the Studio will try to process it, generate an output file, crash and notify you of this error. This unpleasant behavior can be easily removed by looking at the Properties window for our editable file and setting the Custom Tool property there to an empty line. Now, implicit generation attempts do not occur.
The RenderCore () method of the Template class is the main point of operation of a parameterized template. It is in it that the part of the text is generated for which our template in the generator will ultimately be responsible. Therefore, without further ado, just transfer the ready-made code to it.
The main change that the template has undergone is the addition of the open fields DatabaseName and TableName, which, in fact, perform the function of parameterization. Now the data and logic files are separated. The only thing left is to use the include directive and run the same template alternately on different databases and tables, as here:
If desired, using the same method, you can create a universal template that creates, depending on the parameters, a stored procedure based on SELECT, INSERT and UPDATE, and not just DELETE. Everyone can now compose the template code on their own.
A slight retreat to the side. At first glance, all the material described seems elementary. Yes, in fact, it is and is, like the whole T4 in itself. The question is different: these features are included in the standard package, and with this simple article-compilation I want to protect readers (and readers from Habr of varying degrees of experience) from the danger of piling up their own bicycles. The template generators described below are another such pitfall.
In parameterized templates, there is still one unaccounted for defect. Yes, we took the logic of launching the template with specific database and table names to a separate file, but in the case of working with several possible databases and tables, such a solution is a replacement for sewn soap. The programmer is still forced to produce separate template files for each specific table. Let these files now noticeably shrink in size, but nobody canceled the copy-paste problem. Ideally, I would like to create separate queries for each table of this particular database in one movement. Is it possible? Yes.
Add to the project the third useful type of template developed as part of T4Toolbox - Generator. The generator will use our parameterized template in order to substitute different parameter values (database names and tables) into it and send the processing result to different files. For the latter purpose, the Template class provides a wonderful RenderToFile method.
So, let the generator file be called CrudProcedureGenerator.tt and the default stock for it, as you can see for yourself, looks like this:
The generator itself does not do any processing of the T4 text, it only starts in turn other, already written basic templates. Therefore, its corresponding main methods instead of Render and RenderCore are called Run and RunCore, respectively. Let's adjust them for ourselves:
Here, all the tables in one given database are sorted and for each instance of the DeleteProcedureTemplate class it creates a separate unique output file with the request. For complete happiness, it is not enough just to set the database and start the full processing cycle:
Result on your screens:
Following ordinary logic, an article on generators should be completed with notes on how to debug, test, and painlessly modify them for advanced needs. Unfortunately, this one habrastatya just can’t stand such an amount of extra program code, but I don’t feel like putting it into the third part, the material will turn out to be poor and divorced from the team. Therefore, I will limit myself to advice on the basis of which, as well as the source code from Oleg Sych’s blog, the reader will be able to use generators in life without any problems.
Handling errors in code generators
Unit testing code generators
Making code generators extensible
This article will continue the topic of automatic code generation in Visual Studio with T4 - Text Template Transformation Toolkit. Part number 1 , a little earlier published on this blog, described the T4 syntax and elementary principles of its use. At the same time, I decided to take a closer look at the blog of the respected Oleg Sych and adapt some of his achievements to the habra audience even a little more. In particular, today we will discuss the following topics:
- Create reusable and parameterizable templates
- Creating Template Generators
- Debugging, testing and extension of generators (links)
I did not invent any special examples. The history of the development of a query with the creation of a stored procedure described by Oleg is ideal for illustrating a problem that might require generators. Moreover, which is characteristic, is not a far-fetched problem. Also, the article adheres to the principle of "less words - more code."
Hereinafter, it is assumed that you have installed Visual Studio 2005/2008/2010, as well as T4 Toolbox .
Parameterizable Templates
initial stage
So we have a database. There is a table in this database. And in the table, in the best traditions of Koshchei the Immortal, there is a line that we want to delete. And to delete not just like that, but with a freshly created stored procedure. For simplicity, let the parameters passed to the procedure - all to a single field in the table, by which the rows to be deleted are identified. Filling a request to create such a procedure manually is a thankless job, so we use T4 and create a code similar to the one below:
<#@template language=“C#v3.5” #>
<#@output extension=“SQL” #>
<#@assembly name=“Microsoft.SqlServer.ConnectionInfo” #>
<#@assembly name=“Microsoft.SqlServer.Smo” #>
<#@import namespace=“Microsoft.SqlServer.Management.Smo” #><#
Server server = new Server();
Database database = new Database(server, “Northwind”);
Table table = new Table(database, “Products”);
table.Refresh();
#>
create procedure <#= table.Name #>_Delete
<#
PushIndent(”\t”);
foreach (Column column in table.Columns)
if (column.InPrimaryKey)
WriteLine(”@” + column.Name + ” ” + column.DataType.Name);
PopIndent();
#>
as
delete from <#= table.Name #>
where
<#
PushIndent(”\t\t”);
foreach (Column column in table.Columns)
if (column.InPrimaryKey)
WriteLine(column.Name + ” = @” + column.Name);
PopIndent();
#>
Here, SQL Server Management Objects (SMO) are used to get information about table fields from the database of the local SQL server . After saving the file to the output, we get the search request. For example, it might look like this:
create procedure Products_Delete
@ProductID int
as
delete from Products
where ProductID = @ProductID
Parameterization
The first question that should arise in the head of a person who has looked at this code is: why are the database names and tables hard-coded into the code? It turns out that to create each new request, the programmer will have to crawl into the template and change it manually in several places?
Not really necessary. It is enough to use another method of generation. We add a new file to the project, now using the Template option instead of the File from the T4 Toolbox, let's name it, for example, DeleteProcedureTemplate.tt. As you can see, the environment automatically created a blank of a parameterized template, that is, a file, which we will then include in other templates in order to use it in a generalized form.
<#+
//
// Copyright © Your Company. All Rights Reserved.
//
public class DeleteProcedureTemplate : Template
{
protected override void RenderCore()
{
}
}
#>
Do not try to find the Template class in the Microsoft.VisualStudio.TextTemplating namespace: it is not there. This is an abstract class defined in T4Toolbox, and it makes no sense to connect the T4Toolbox.tt file directly in the parameterized templates themselves. Therefore, each time you save DeleteProcedureTemplate.tt, the Studio will try to process it, generate an output file, crash and notify you of this error. This unpleasant behavior can be easily removed by looking at the Properties window for our editable file and setting the Custom Tool property there to an empty line. Now, implicit generation attempts do not occur.
The RenderCore () method of the Template class is the main point of operation of a parameterized template. It is in it that the part of the text is generated for which our template in the generator will ultimately be responsible. Therefore, without further ado, just transfer the ready-made code to it.
<#@assembly name=“Microsoft.SqlServer.ConnectionInfo” #>
<#@assembly name=“Microsoft.SqlServer.Smo” #>
<#@import namespace=“Microsoft.SqlServer.Management.Smo” #>
<#+
public class DeleteProcedureTemplate : Template
{
public string DatabaseName;
public string TableName;
protected override void RenderCore()
{
Server server = new Server();
Database database = new Database(server, DatabaseName);
Table table = new Table(database, TableName);
table.Refresh();
#>
create procedure <#= table.Name #>_Delete
<#+
PushIndent(”\t”);
foreach (Column column in table.Columns)
{
if (column.InPrimaryKey)
WriteLine(”@” + column.Name + ” ” + column.DataType.Name);
}
PopIndent();
#>
as
delete from <#= table.Name #>
where
<#+
PushIndent(”\t\t”);
foreach (Column column in table.Columns)
{
if (column.InPrimaryKey)
WriteLine(column.Name + ” = @” + column.Name);
}
PopIndent();
}
}
#>
The main change that the template has undergone is the addition of the open fields DatabaseName and TableName, which, in fact, perform the function of parameterization. Now the data and logic files are separated. The only thing left is to use the include directive and run the same template alternately on different databases and tables, as here:
<#@ template language=”C#v3.5” hostspecific=”True” #>
<#@ output extension=”sql” #>
<#@ include file=”T4Toolbox.tt” #>
<#@ include file=”DeleteProcedureTemplate.tt” #>
<#
DeleteProcedureTemplate template = new DeleteProcedureTemplate();
template.DatabaseName = “Northwind”;
template.TableName = “Products”;
template.Render();
#>
If desired, using the same method, you can create a universal template that creates, depending on the parameters, a stored procedure based on SELECT, INSERT and UPDATE, and not just DELETE. Everyone can now compose the template code on their own.
A slight retreat to the side. At first glance, all the material described seems elementary. Yes, in fact, it is and is, like the whole T4 in itself. The question is different: these features are included in the standard package, and with this simple article-compilation I want to protect readers (and readers from Habr of varying degrees of experience) from the danger of piling up their own bicycles. The template generators described below are another such pitfall.
Template Generators
In parameterized templates, there is still one unaccounted for defect. Yes, we took the logic of launching the template with specific database and table names to a separate file, but in the case of working with several possible databases and tables, such a solution is a replacement for sewn soap. The programmer is still forced to produce separate template files for each specific table. Let these files now noticeably shrink in size, but nobody canceled the copy-paste problem. Ideally, I would like to create separate queries for each table of this particular database in one movement. Is it possible? Yes.
Add to the project the third useful type of template developed as part of T4Toolbox - Generator. The generator will use our parameterized template in order to substitute different parameter values (database names and tables) into it and send the processing result to different files. For the latter purpose, the Template class provides a wonderful RenderToFile method.
So, let the generator file be called CrudProcedureGenerator.tt and the default stock for it, as you can see for yourself, looks like this:
<#+
//
// Copyright © Your Company. All Rights Reserved.
//
public class CrudProcedureGenerator : Generator
{
protected override void RunCore()
{
}
}
#>
The generator itself does not do any processing of the T4 text, it only starts in turn other, already written basic templates. Therefore, its corresponding main methods instead of Render and RenderCore are called Run and RunCore, respectively. Let's adjust them for ourselves:
<#@ assembly name=”Microsoft.SqlServer.ConnectionInfo” #>
<#@ assembly name=”Microsoft.SqlServer.Smo” #>
<#@ import namespace=”Microsoft.SqlServer.Management.Smo” #>
<#@ include file=”DeleteProcedureTemplate.tt” #>
<#+
public class CrudProcedureGenerator : Generator
{
public string DatabaseName;
public DeleteProcedureTemplate DeleteTemplate = new DeleteProcedureTemplate();
protected override void RunCore()
{
Server server = new Server();
Database database = new Database(server, this.DatabaseName);
database.Refresh();
foreach (Table table in database.Tables)
{
this.DeleteTemplate.DatabaseName = this.DatabaseName;
this.DeleteTemplate.TableName = table.Name;
this.DeleteTemplate.RenderToFile(table.Name + “_Delete.sql”);
}
}
}
#>
Here, all the tables in one given database are sorted and for each instance of the DeleteProcedureTemplate class it creates a separate unique output file with the request. For complete happiness, it is not enough just to set the database and start the full processing cycle:
<#@ template language=”C#v3.5” hostspecific=”True” debug=”True” #>
<#@ output extension=”txt” #>
<#@ include file=”T4Toolbox.tt” #>
<#@ include file=”CrudProcedureGenerator.tt” #>
<#
CrudProcedureGenerator generator = new CrudProcedureGenerator();
generator.DatabaseName = “Northwind”;
generator.Run();
#>
Result on your screens:
P.S. Features of using generators
Following ordinary logic, an article on generators should be completed with notes on how to debug, test, and painlessly modify them for advanced needs. Unfortunately, this one habrastatya just can’t stand such an amount of extra program code, but I don’t feel like putting it into the third part, the material will turn out to be poor and divorced from the team. Therefore, I will limit myself to advice on the basis of which, as well as the source code from Oleg Sych’s blog, the reader will be able to use generators in life without any problems.
- For testing generators, the T4 Toolbox has its own excellent blank under the name “Unit Test”.
- If it is necessary to modify the generation heart — the code generated by the template — there is no need to directly edit the file with its class (in this case, DeleteProcedureTemplate). It is enough to import another file into the generator, in which to describe the heir to our template with the required adjustments.
- The Generator class has a Validate function, which is called by the Run method in the first place, before dealing directly with code generation (RunCore). It can be used to check the input parameters of the generator.
- To report errors, you can use the Error and Warning methods, similar to those discussed in the TextTransformation class.
Handling errors in code generators
Unit testing code generators
Making code generators extensible