基于模板的代码生成






4.48/5 (9投票s)
一篇关于基于模板的代码生成文章,并演示了如何快速生成存储过程的包装类。
引言
在客户端-服务器项目中工作时,总会有大量重复性的编码工作。例如,使用存储过程需要一遍又一遍地编写相同的数据访问代码。因此,借助工具快速生成数据访问代码是非常可取的。本文介绍了一个代码生成工具。它的理念并不新颖。它涉及到处理模板,正如我们在 ASP、ASP.NET 或 JSP 技术中所了解到的那样。这里有一个熟悉的 ASP 页面示例
<%
var name = "John";
%>
Dear <%= name %>
对这个模板的处理是,ASP 引擎将其转换为 JScript 文档,方法是简单地剥离标签 <%
和 %>
,并将文本“ Dear ”和 `<%= name%>` 部分包装到 `Response.Write(" Dear " + name)` 中。生成的 JScript 文件(如下所示)然后传递给脚本宿主引擎进行处理。
var name = "John";
Response.Write(" Dear " + name);
这里的技术只是将模板转换为代码,然后经过编译或解释后生成目标文档。ASP 和 ASP.NET 技术生成 HTML 输出,但也可以生成任何类型的文档,包括 C# 源代码。
本文是关于基于模板的代码生成的。首先,我想介绍我开发的工具,然后通过生成 SQL Server 存储过程访问的包装类来展示它。这个工具的工作方式如下。
基于模板的代码生成器
要输出目标文档,该工具必须执行以下操作:
- 读取模板文件并将其解析为各个部分。
- 使用解析后的模板部分将 C# 类写入临时文件。
- 将临时 C# 代码文件编译成程序集。
- 加载已编译的程序集,创建已编译类的实例,并调用其渲染方法。
渲染方法将生成所需的目标文档。就是这样。让我们看看这四个步骤中的每一个。
- 读取模板文件并将其解析为各个部分:
解析模板文件应为工具提供所有必要的信息,以便它能够编写一个合适的 C# 类,将其编译成一个合适的程序集,然后加载并执行该类的渲染函数。为了尽可能轻松地完成所有这些工作,我发明了一些需要解释的模板部分标记。
给定上面的模板文本,该工具将像这样编写一个类
/* Here is a C# version of the sample again <% string name = "John"; %> Dear <%= name %> */ using System; using System.IO; public class RenderClass { private TextWriter Writer; public void Render(TextWriter writer) { Writer = writer; string name = "John"; Writer.Write(" Dear " + name); } }
该工具可以在编写类代码时做出一些有限的假设。但为了最有效,模板作者应该能够指定类成员、引用程序集以及使用的命名空间。我们可以使用模板部分标签来完成所有这些。考虑这个模板:。
<%-namespaces using DatabaseCatalogReader; %> <%-class DatabaseCatalog catalog; string GetDatabaseName() { if(catalog == null) catalog = new DatabaseCatalog() return catalog.Name; } %> The name of the database is <%=GetDatabaseName()%>
有两个标签将模板分成两个不同的部分。标签中不能包含任何空格。
<%-namespaces
- 渲染类所需的命名空间列表。<%-class
- 渲染类的成员变量和方法。
该工具会将此模板转换为以下可编译类
using System; using System.IO; using DatabaseCatalogReader; public class RenderClass { DatabaseCatalog catalog; string GetDatabaseName() { if(catalog == null) catalog = new DatabaseCatalog() return catalog.Name; } private TextWriter Writer; public void Render(TextWriter writer) { Writer = writer; Writer.Write(" The name of the database is "); Writer.Write(GetDatabaseName()); } }
在这里,我们看到带标签的模板部分有助于该工具在类定义内部和外部编写一些代码。所有不带标签的模板部分都转换为 `Render` 方法的实现。
- 使用解析后的模板部分将 C# 类写入临时文件:
该工具在编写类之前会解析整个模板。您可以以混合顺序组织带标签和不带标签的模板部分。但最可取的做法是按此顺序进行
<%-namespaces // the using statements inside this section // will be written outside the class definition %> <%-class // this is a C# code section // all methods and class member variables must show up here %> <% // this is a no-tagged template section // and all C# code will be written as part of the // implementation of the class's'Render' method. %> This is one line of template text and will ultimately end up in the target document. <% // this again is a non-tagged template section %> This is another line of <%= "template text." %>
任何模板都可以包含多个不带标签的模板部分,这些部分与模板文本交替出现。也可以有多个带标签的模板部分,以任何顺序出现。例如
<%-class // this is a C# code section // all methods and class member variables must show up here %> <%-namespaces // the using statements inside this section // will be written outside the class definition %> <% // this is a no-tagged template section // and all C# code will be written as part of the // implementation of the class's 'Render' method. %> This is one line of template text and will ultimately end up in the target document. <% // this again is a non-tagged template section %> This is another line of <%= "template text." %> <%-class // this is a C# code section // all methods and class member variables must show up here %>
该工具将在编写渲染类之前收集和合并所有模板部分。但是,出于样式和可读性的考虑,请将带标签的模板部分放在一个地方。
该工具将类写入一个 C# 文件,该文件以模板文件名加上扩展名 '.cs' 命名。名为 'Template.txt' 的模板文件将有其渲染类写入 'Template.txt.cs'。
- 将临时 C# 文件编译成程序集:
命名空间 `Microsoft.CSharp` 提供了一个托管包装器给 C# 编译器,用于编译 C# 文件。创建文件后检查该文件,您会发现一系列 ` #line ` 数字。这些数字对应于模板文件中的代码部分。在编译器报告错误的情况下,这是识别模板文件中错误代码的一种有用的方法。错误消息与 C# 编译器发出的消息类似。行号应有助于定位编译器在模板文件的哪个位置遇到错误。
- 加载已编译的程序集,创建已编译类的实例并调用其渲染方法:
一旦 C# 文件被编译成程序集,它就会被加载,并且 `RenderClass.Render(TextWriter writer)` 方法被调用以渲染输出,其中一个 `StreamWriter` 打开到 `TargetFile`,并传递给 `Render` 方法。您可以将写入器重定向到任何您想要的内容。
<% // this will write the target document to the console // use it while developing the template Writer = Console.Out; %>
配置
基于模板的代码生成器依赖于配置文件提供的输入。在没有命令行参数的情况下启动该工具。您将看到此对话框。按“编辑配置文件...”按钮打开一个带有配置数据的小文本编辑器。如果需要,请编辑并保存它。
配置文件中有三个有趣的节供您编辑:`compiler-option`、`regular-expressions` 和 `references`。
只要您提供必要的程序集列表,您就有能力创建充分利用所有 .NET Framework 类库的模板。通过列出必要的程序集来编辑 `references` 部分。只需编写带 '.dll' 扩展名的文件名,例如 `System.Data`。
如果您包含私有程序集,您可能还想编辑 `compiler-options` 部分。该工具需要知道在哪里可以找到您的程序集。只需提供库路径作为 `lib` 选项,如下所示
The value must be a semi-colon separated list of full path names,
e.g. "C:\MyPrivateAssemblies; D:\"
<option name="lib" value="" type="string" />
还有一种添加程序集引用和库路径的方法。使用模板部分标签(现在介绍)`<%-references %>` 和 `<%-libpaths %>`。所以,它看起来是这样的
<%-references
System.Data.dll
DatabaseCatalogReader.dll
%>
<%-libpaths
C:\MyPrivateAssemblies
D:\
%>
配置文件中还有一个部分需要解释,即 `<regular-expressions>`。如果您在模板文件中指定了引用程序集,该工具将使用正则表达式求值器来解析模板文件中的信息。配置文件列出了两个正则表达式,一个用于解析列出的程序集,另一个用于解析库路径。如果需要,您可以用自己的更好的正则表达式替换提供的。
目标文档格式化
输出文档将与模板指定的完全一样。这包括所有空格。例如
<% string name="Joe";%>
My Name is <%= name%>.
My Name is <%= name%>.
My Name is <%= name%>.
此模板将被转换为此
My name is Joe.
My name is Joe.
My name is Joe.
但是通过不同地编写模板,如下所示
<%
string
name=
"Joe"; int
i=0; while(i++
< 3) {
%>
My
Name is <%= name%>.
<%
}
%>
生成此目标文档
My name is Joe.
My name is Joe.
My name is Joe.
附加的意外空白行是由于字符串前面的额外“\r\n”字符引起的,例如,“\r\n My name is Joe.”。额外的“\r\n”跟在闭合标签 %>
之后。要消除这个额外的“\r\n”,您可以在闭合标签中再添加一个 `>` 字符,如下所示 `%>`。下面是如何编写模板以实现所需格式
<%
string
name=
"Joe"; int
i=0; while(i++
< 3) {
%>>
My
Name is <%= name%>.
<%
}
%>
注释标签和模板标记
要介绍的最后一个模板部分标签用于用注释注释模板。
<%!
Anything
between these two tags is considered to be a comment. A
comment should be used to annotate the template with explanations. The
parser will strip it out.
%>
<%! ignore "\r\n" at the end of this comment %>>
<%!
One last remark. Template sections cannot be nested.
Don't do this: <%! Nested comment %>
%>
没有任何东西可以阻止您将任何文本文件作为模板运行。但是,该工具将读取文本的第一行,并检查它是否是正确的模板文件。这是解析器期望的标记:`@@Template@@`。
这里有一个模板结构的最终总结
@@Template@@ <%! indicates this to be a proper template %>>
<%! Note how all the '\r\n' characters are stripped from
the
target document %>> <%! list all your references here, e.g. %>> <%-references
MyAssembly.dll
%>> <%! help the tool find the assembly
%>>
<%-libpaths
C:\MyLibraries
%>> <%! add all the namespaces that the
render
class
expects
%>> <%-namespaces using System.Data; using MyAssembly; %>> <%! all the
code
that
may be
part of
the
class's definition %>> <%-class // this is a code section so comments are
allowed
%>>
<%! all the render code from here on %>>
存储过程包装器
示例应用程序使用基于模板的代码生成工具。它旨在检索 SQL Server 中存储过程的元数据,然后生成一个包装类。
如果您安装了 MSI 包,您可以从桌面运行它。通过初始对话框,您可以导航到“模板”目录以加载“StoreProcs.txt”模板。这是您应该看到的对话框序列
指定模板文件和目标文件。
选择数据库服务器和数据库。
选择感兴趣的存储过程。
您也可以从命令行启动该工具。在 Visual Studio 中开发模板时,我将代码生成器配置为外部工具。
命令行参数很简单
tcg -template:<filename> -target:<filename>
您可以将以下宏复制到“外部工具”参数编辑控件中。
-template:$(ItemPath) -target:$(ItemDir)$(ItemFileName).cs
结论
我希望你能好好利用这个工具。我决定在经过一段时间的研究另一种方法后进行开发。我最初认为 XSL 转换会提供一种快速完成同样事情的方法。但最终我意识到创建所需的 XML 输入并不总是那么容易和快捷。而且,编写 XSL 文档比我最初想的要费力得多。例如,如果目标文档本身包含保留字符 `<`、`>` 和 `&`,我发现编写特殊的 XSL 模板来打印这些保留字符存在额外的困难。
这里的基于模板的方法可以很好地集成到我们正常的开发流程中。我参与的每个项目都有大量的套路代码需要编写。
遗憾的是,调试模板不像调试 ASP.NET 页面那样容易。我找不到时间来找出一种方法来为这个工具添加调试支持。如果有人有想法,请分享给我。我想了解一下。
结语
当我发现自己厌倦了编写数据库访问代码时,我有了构建基于模板的代码生成器的动力。这是套路化的东西。与此同时,我了解了 SQL Server 2005 中的 CLR 托管。希望能得到真正的解脱,我去了 ' MSDN ' 学习它的一切。当我看到提出的编程模型时,我感到非常失望。我希望微软会发明一种 C# 或 VB.NET 的方言来接受嵌入的 SQL 语句。将 T-SQL 视为过程语言“T”的一种特殊方言。所以,我希望得到 C#-SQL 或 VB.NET-SQL。通过示例代码,我看到的代码非常像你在应用程序层中编写的数据访问代码。我还想知道如何调用用 .NET 语言编写并存储在数据库中的存储过程。似乎仍然需要传统的访问数据的方法。同样,我希望一个通用的接口程序集能够更好地与数据库接口。这是我希望的模型
// database stored procedures to get certain customers
// shared between client and server
public class Customer
{
string Name;
}
// shared between client and server
public interface IMyDatabase
{
SqlResultset GetCertainCustomers(string cityName);
Customer[] GetCertainCustomers(string cityName);
}
// defined as part of the database
class MyDatabase
{
// one way to get a customer list
public SqlResultset GetCertainCustomers(string cityName)
{
// note the embedded sql statement
return select Name from Customer where City = cityName;
}
// another way to get a customer list
public Customer[] GetCertainCustomers(string cityName)
{
// note the embedded sql statement
return select Name from Customer where City = cityName;
}
}
// client application use of stored procedure
// get a transparent proxy
string connectionString =
"Integrated Security=SSPI; Server=MyServer; Initial Catalog=MyDatabase";
IMyDatabase db = (IMyDatabase)SqlServer.Connect(connectionString);
// call the stored procedure
SqlResultset resultSet = db.GetCertainCustomers("Arlington");
while(resultSet.MoveNext())
{
Console.WriteLine("Name: {0}", resultSet["Name"]);
}
// do it again but more elegantly
Customer[] customers = db.GetCertainCustomers("Newton");
foreach(Customer customer in customers)
{
Console.WriteLine("Name: {0}", customer.Name);
}
如你所见,不需要处理 `SqlConnection` 和 `SqlCommand` 对象。你们对此有什么看法?