Greetings, Habr!
This article will continue the topic of automatic code generation in Visual Studio using the T4 - Text Template Transformation Toolkit.
Part number 1 , a little earlier published in this blog, described the syntax T4 and the basic principles of its use. This time I decided to explore the blog of the respected
Oleg Sych in more detail and to adapt a few more of his ideas to the habrew Audience. In particular, today we will discuss the following topics:
- Creating reusable and parameterizable templates
- Creating Template Generators
- Debugging, testing and extending 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 may require generators. And, characteristically, not a far-fetched problem. The article also adheres to the principle “less words - more code”.
Hereinafter, it is assumed that you have Visual Studio 2005/2008/2010 installed, 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 Koshchey Immortal, there is a line that we wish to remove. Moreover, it is not just a delete, but a newly created stored procedure. Let, for simplicity, the parameters passed to the procedure — every single table field, by which the lines to be deleted are identified. To fill the request to create such a procedure manually is a thankless job, so we use T4 and create 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) is used to get information about table fields from a local SQL server
database . After saving the file, we get the required query at the output. 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: why are the database names and tables rigidly packed into the code? So, to create each new request, the programmer will have to climb into the template and change it manually in several places?
Actually not necessarily. It is enough to use another method of generation. Add a new file to the project, using the Template option instead of the File from the T4 Toolbox, let's call it, for example, DeleteProcedureTemplate.tt. As you can see, the environment has automatically created a template parameterization, that is, a file, which we will later include in other templates in order to use it in a generalized form.
<#+
// <copyright file=”DeleteProcedureTemplate.tt” company=”Your Company”>
// Copyright © Your Company. All Rights Reserved.
// </copyright>
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 the T4Toolbox.tt file does not make sense to include directly in the parameterized templates themselves. Therefore, each time DeleteProcedureTemplate.tt is saved, Studio will attempt to process it, generate an output file, crash and notify you with an error. This unpleasant behavior can be easily removed by looking into the Properties window for our edited file and setting the Custom Tool property to an empty line. Now attempts of implicit generation 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 pattern in the generator will ultimately be responsible. Therefore, without further ado, we simply transfer the ready code into 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 to which the template has been subjected is the addition of the open fields DatabaseName and TableName, which, in fact, perform the parameterization function. Now the data and logic files are separated. The only thing that remains is to use the include directive and run the same template alternately on different databases and tables, like this:
<#@ 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, a similar method can be used to create a universal template that creates, depending on the parameters, a stored procedure based on SELECT, INSERT and UPDATE, and not just DELETE. The code for the template, anyone can now make their own
A small retreat to the side. At first glance, all the material described seems elementary. Yes, in fact, he is so, like the whole T4 in itself. The question is different: these features are included in the standard delivery, and with such a simple compilation article I want to protect readers (and readers of Habr of different degrees of experience) from the danger of building their own bicycles. The pattern generators described below are another one of these pitfalls.
Pattern Generators
There is still one unaccounted bug in the parameterized templates. Yes, we have taken out the logic of launching a template with specific database names and tables into a separate file, but in the case of working with several possible databases and tables, such a solution is a replacement for soap. The programmer is still forced to produce separate template files for each specific table. Let these files now noticeably shrunk in size, but no one has canceled the copy-paste problem. Ideally, I would like to create separate requests for each table of this particular database in one movement. Is it possible Yes.
Add to the project a third useful template type, developed in the framework of the T4Toolbox - Generator. The generator will use our parameterized template in order to substitute in it different values ​​of parameters (database names and tables) and send the result of processing to different files. With the latter purpose, the Template class provides a great RenderToFile method.
So, let the generator file is called CrudProcedureGenerator.tt and the default procurement for it, as you can see for yourself, is as follows:
<#+
// <copyright file=”CrudProcedureGenerator.tt” company=”Your Company”>
// Copyright © Your Company. All Rights Reserved.
// </copyright>
public class CrudProcedureGenerator : Generator
{
protected override void RunCore()
{
}
}
#>
The generator itself does not carry out any processing of the text T4, it only launches in turn other, already written basic templates. Therefore, its corresponding basic methods instead of Render and RenderCore are called Run and RunCore, respectively. We arrange 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” );
}
}
}
#>
All tables in one given database are searched here and for each instance of the DeleteProcedureTemplate class creates a separate unique output file with a query. For complete happiness, all you need is to set a database and run a 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 the use of generators
Following the usual logic, the article about the generators would be necessary to complete the notes on how to debug them, test and modify them painlessly for advanced needs. Unfortunately, this one habrastatya simply can not stand such an excess of program code, but I don’t want to take it to the third part either, the material will turn out to be poor and cut off from the team. Therefore, I will confine 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.
- To test the generators in the T4 Toolbox has its own excellent billet called "Unit Test".
- If it is necessary to modify the heart of generation — the code created by the template — there is no need to directly edit the file with its class (in this case, DeleteProcedureTemplate). It is enough to import one more file into the generator in which to describe the successor of our template with the required corrections.
- The Generator class has a function called Validate, which is called first by the Run method, before you start directly generating code (RunCore). It can be used to check the input parameters of the generator.
- For error reporting, you can use the Error and Warning methods, similar to those discussed in the TextTransformation class.
Related Links:
Handling errors in code generatorsUnit testing code generatorsMaking code generators extensible