65.9K
CodeProject 正在变化。 阅读更多。
Home

基于模板的代码生成器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (31投票s)

2007年11月3日

CPOL

13分钟阅读

viewsIcon

95972

downloadIcon

4217

一个基于模板、面向命令行的 .NET 代码生成器

引言

本文提供的贡献是一个面向命令行的、基于模板的代码生成器。源代码和可执行文件都可供您使用、增强和扩展。

代码生成器提供以下功能

  • 可自定义的模板语法,默认在 T3 风格的模板语言(<%%> 分隔符用于代码,<%= %> 用于表达式等)或 T4 风格的模板语言(<##> 分隔符用于代码,<#= #> 用于表达式等)之间进行选择。
    默认采用 T3 风格语法。
  • 支持模板中的 C# 和 VB.NET 代码
  • 面向命令行,参数可以通过命令行或设置文件传递
  • 支持模板包含文件
  • 支持代码隐藏文件
  • 模板可以相互调用并传递参数
  • 支持调试模板
  • 可以生成任何文本输出,例如编程代码
  • 可自定义的文件写入器,允许从 Team System Source Control 或其他版本控制系统检出文件或执行其他操作。

在我们看示例之前,先来谈谈代码生成的一些想法。

为什么需要代码生成?

我使用代码生成已经有好几年了,并且认为它对项目的成功至关重要。通过生成代码,有时甚至占项目代码量的 20% 以上,您可以确保项目中的整个层都按照架构要求一致地编写,并且毫无例外地符合要求。

代码生成保证了代码的高质量,因为生成的代码不包含复制粘贴错误或其他在手动编写代码中经常出现的错误。

代码生成还为您提供了非常高的代码可维护性。如果您基于数据库结构生成代码,当数据库结构发生变化时,您只需“按一下按钮”即可使代码再次同步。

另一方面,如果您想执行架构更改,例如让所有数据对象都实现 INotifyPropertyChanged (在 System.ComponentModel 命名空间中),您只需更改生成模板,然后“再按一次按钮”……

但是,为了实现这些好处,在使用代码生成进行项目开发时,您必须遵守两条重要规则

规则 1:确保您始终能够重新生成所有生成的代码

这意味着您应该避免“交互式代码生成”(您在 IDE 中选择一个菜单选项,然后在一个表单中输入一些数据,生成一个代码文件,甚至更糟糕的是,生成一段代码)。交互式代码生成可以作为您编码工具集的有用补充,例如代码片段和其他 IDE 功能。它提高了您的输入速度,但没有提高代码的可维护性。

根据规则 1,您也不应该手动修改生成的代码。因此,生成的代码必须非常符合您的需求(另请参见规则 2),这样您就不需要更改它。

但是,您可以生成可适应和可扩展的代码。例如,将生成的 C# 方法声明为 `virtual`,这样您仍然可以创建一个子类并重写方法;或者将您的类声明为 `partial` - 这将允许您通过添加一个独立的、未生成的文件来扩展它们。

规则 2:确保您的代码是基于可扩展的源生成的

根据规则 1,生成的代码必须非常符合您的需求。因此,您必须确保所有当前和未来的需求都将得到支持。

违反此规则的一个常见情况是基于数据库表定义生成数据对象。虽然数据库表定义是生成数据对象的良好起点,但它不是一个可以指示哪个生成的属性应设置为“public”而哪个应设置为“internal”的地方。每当我有指定生成属性可见性修饰符的需求时,我都会遇到麻烦。
代码生成的好源包括

  • UML 模型,前提是建模工具有扩展功能(模型对象的自定义属性或对 UML 配置文件的支持)和读取模型的 API
  • XML 文件
  • 数据库(不是数据库定义,而是包含要生成的数据的表)

作为基于数据库定义生成的一种替代方案,您可以将数据库定义数据与存储在数据库本身中的“元数据”表中的信息混合。

这就引出了本文提供的代码生成器。它是按照这两条规则设计的。

为了符合第一条规则,生成器不与您的 IDE 进行交互式集成。相反,它是一个命令行工具,并且所有参数都可以存储在一个文件中。这样,您就可以通过命令提示符上的一个命令来重新运行项目中的代码生成!

至于第二条规则,很简单,代码生成器对生成源不做任何假设。因此,它不提供默认的生成源,您必须创建并提供自己的源。

请记住,无论您选择哪种源,都应该是一个您完全控制的源。您必须能够扩展源中存储的信息,以扩展代码生成的功能。

生成器的制作

本文不是关于代码生成器本身的制作。但是,源代码可以在本页下载,为了让您了解代码结构,这里有一些关于代码生成器实现的信息。

在解析器的核心,您会发现我之前在另一篇文章中介绍的混合内容解析器的增强版本。

Arebis.CodeGenerator 项目的 *Resources* 文件夹包含生成代码(C# 和 VB.NET)的模板。

整个生成过程在一个启用了 ShadowCopyFiles 的单独 AppDomain 中运行。这是为了允许删除生成的程序集文件。如果将 GenerateInMemory 设置为 `true` 传递给用于编译模板的 CodeDomProvider,则可以避免这种情况,但这似乎会影响模板的调试功能。

通过向生成的代码添加行号指令(C# 中的 #line,VB.NET 中的 #ExternalSource),可以根据原始模板源行号报告编译时和运行时错误。

一个示例项目

作为示例项目,我们将根据 XML 文件中存储的信息生成代码。

输入文件

例如,考虑以下 XML 文件,它定义了一个 Person

Person.xml file

引用的 Address 类也可以定义,例如使用以下 XML

Address.xml file

第一个模板(BuildClasses.cst)

通过“一键操作”,我们希望能够为这些 XML 文件生成类定义。要实现这一点,我们需要编写一些模板。我们编写的第一个模板将读取 XML 文件并调用另一个模板来生成类

BuildClasses.cst file

我们将此模板命名为 *BuildClasses.cst*。(我喜欢 CST 扩展名,代表“C Sharp Template”。)

第 1 到 5 行包含指令。第一个指令 `CodeTemplate` 是必需的,它告诉生成器模板中的代码是用 C# 编写的。

ReferenceAssembly 提供了要引用的程序集的引用。提供程序的完整路径、如果程序集位于模板附近,则提供相对路径,如果程序集在 GAC 中或与代码生成器本身在同一目录中,则只需提供文件名。请注意,*mscorlib.dll*、*System.dll* 和 *Arebis.CodeGeneration.dll* 是默认引用的,但是,重复引用 *System.dll* 也无妨。

Import 指令将命名空间导入到代码中,这样您就不需要指定完整的类名。

模板的其余部分,第 6 到 20 行,是一个脚本块 — 一段内联执行的代码。代码是用 C# 编写的,如 CodeTemplate 指令所指定。

在第 8 行,使用了 this.Settings。提供给代码生成器的所有设置(无论是在设置文件中还是在命令行上)都可以通过模板的 Settings 属性访问。该属性的类型为 System.Collections.Specialized.NameValueCollectionNameValueCollection 有趣之处在于它将键(字符串)关联到单个字符串或字符串数组。

第 13 行的 this.Host 提供了对 Arebis.CodeGeneration.IGenerationHost 的访问。CallTemplateToFile() 方法允许调用另一个模板,并将输出写入单独的文件。可以传递其他参数来匹配被调用模板的参数。在本例中,我们传递一个 XmlElement ,它匹配 XML 文件的文档元素。

IGenerationHost 定义了以下调用模板的方法

void CallTemplate(string templatefile, params object[] pars);
void CallTemplateToFile(string templatefile, string outputfile, 
    params object[] pars);

模板本身将被编译成一个继承自 CodeTemplate 的类。主类和接口的概述定义在 Arebis.CodeGeneration

Screenshot - codegen_fig1.gif

第二个模板(Class.cst)

第二个模板 *Class.cst* 将生成单个类文件。模板如下

Class.cst

再次从指令开始。CodeTemplate 指令包含一些附加项(ClassName CodeFile),稍后会详细介绍。

我们还有一个 Parameter 指令,告诉我们此模板需要一个类型为 XmlElement 的参数,该参数将从模板代码中以 classElement (如第 12 行)的名称访问。

模板的其余部分是文字内容,混合了内联表达式(在 <%=%> 之间),以及两个脚本块,从第 14 行到 17 行,以及第 26 行。

在第 19 行,我们访问了一个本地方法 ToCamel()。此方法默认不存在。但是,在 CodeTemplate 指令中,我们提供了一个 CodeFileCodeFile 是一个部分类,将用作编译模板的一部分。每当使用 CodeFile 指令属性时,还必须在 CodeTemplate 指令(第 1 行)上指定 ClassName 属性,因为生成引擎需要为生成的类赋予与 codefile 中的类完全相同的名称。

codefile *Class.cst.cs* 为名为 *Template.Class* 的部分类提供了一个 ToCamel() 方法。因此,我们定义 *Class.cst.cs* 文件如下

Class.cst.cs file

代码(隐藏)文件有助于使模板代码更易于阅读,因为您可以将复杂的逻辑放在代码隐藏文件中。

运行示例

示例几乎准备好运行了。我们唯一需要提供的是设置。代码生成器命令行工具需要一个作为设置文件的参数,以及/或在命令行上提供的设置。

运行示例的最简单方法是

CGEN /template "BuildClasses.cst"

我们为 CGEN 命令行工具提供一个 template 设置的值。这是唯一真正必需的设置。

但是,由于我们在模板中使用设置,因此我们还需要为这些设置提供值。我们需要为 source 设置(在第一个模板的第 8 行中使用)和 namespace 设置(在第二个模板的第 10 行中使用)提供值。

此外,我们还可以为 targetdirlogfile 设置提供值。有关这些设置和其他设置的信息,请键入 CGEN /?。设置文件现在是

BuildClasses.settings file

现在我们可以运行代码生成器,只传递设置文件

CGEN BuildClasses.settings

结果是,在 *Result\Domain* 目录中生成了两个文件,其中一个看起来像

Result\Domain\Person.generated.cs file

如您所见,生成的类被声明为 `partial`。这允许我们创建一个独立的、未生成的文件,来定制生成类的行为。

请亲自尝试示例,它可以在本文下载。下载 *CGenSample.zip*,将其解压到某处,然后运行 *RunSample.cmd* 批处理文件来执行示例。然后,您可以随意修改模板或其他文件来查看效果,添加语法或运行时错误来查看错误如何报告,或者按照下一段的说明调试模板的执行。

调试模板

通过添加对 System.Diagnostics.Debugger.Launch() 的调用,如下一屏幕(第 7 行)所示,您可以在模板执行时启动调试会话。

Screenshot - debug_fig1.gif

模板语法

以下提供了有关模板语法的信息,包括指令列表及其属性。

指令

指令包含有关模板的信息,以及模板成功编译所需的信息。指令提供有关模板语言、所需的程序集引用、要导入的命名空间等信息。

模板必须以一个唯一的 CodeTemplate 指令开头,并且可以包含其他指令。虽然不是强制性的,但强烈建议将所有指令声明放在模板文件的前面。

指令看起来像 HTML 标签(它们有名称,通常有属性),但它们写在 <%@%> 标记之间。

CodeTemplate 指令

描述

提供模板声明和元数据。每个模板在文件开头都应该有且只有一个 CodeTemplate 指令。

属性
语言 模板代码语言。“C#”或“VB”。如果未指定,则默认假定为“C#”。
TargetLanguage 可选。目标语言,输出的语言。可以是任何值。
AssemblyFile 可选。要生成的程序集文件的名称。可以用于在其他模板中引用此程序集。
ClassName 可选。为此模板创建的类的完整名称。如果使用 CodeFile 文件,则此设置是必需的。
CodeFile 可选。代码隐藏文件名,包含一个部分类,需要由模板完成。
继承自 可选。模板类的基类。必须是 Arebis.CodeGeneration.CodeTemplate 类型。
LinePragmas 可选。是否输出行号指令,从而以模板的术语提供调试信息。默认值为 `True`。
Explicit (显式) 可选。仅限 VB。值可以是“On”或“Off”,指示是否启用或禁用 Explicit 选项。默认值为 Off。
Strict 可选。仅限 VB。值可以是“On”或“Off”,指示是否启用或禁用 Strict 选项。默认值为 Off。
描述 可选。对模板的自由描述。

ReferenceAssembly 指令

描述

引用一个程序集文件,并提供一个绝对或相对文件名路径。程序集应位于模板的同一目录、*bin* 子目录、传递给 referencepath 设置的任何目录或 GAC 中。

属性
Path 必需。程序集文件的路径(绝对或相对)。
注释

请注意,程序集 *mscorlib.dll*、*System.dll* 和 *Arebis.CodeGeneration.dll* 会被自动引用。

Import 指令

描述

将一个命名空间导入到模板代码源中。

属性
命名空间 必需。要导入的命名空间。
别名 可选。导入的命名空间的别名。

Parameter 指令

描述

声明模板的参数。

属性
名称 必需。参数的名称(必须是有效的 .NET 标识符)
类型 参数的类型(默认假定为 System.Object

CompileFile 指令

描述

提供要包含在模板编译中的其他文件。默认情况下,会编译翻译后的模板及其可能存在的代码隐藏文件。此指令允许指定其他文件。

属性
Path 必需。要包含在模板程序集编译中的文件的绝对或相对路径。

表达式

表达式以模板的语言(C# 或 VB)编写,并在原地进行评估。它们的结果被转换为 string 并写入模板的输出。

表达式写在 <%=%> 标记之间。

代码块

代码块包含原地执行的代码。它们可以调用方法,包含条件(if)语句、循环定义等。实际上,它们可以包含任何在编译后的模板方法中合法的代码。

代码块写在 <%%> 标记之间。

函数块

在模板中,您可以定义一个或多个函数块,其中可以放置方法和其他类级元素。这些不会原地执行,而是定义在编译后的模板类中,并且可以包含在代码块中调用的方法。

函数块写在 <%%%%> 标记之间。

注释模板

模板中的注释可以写在 <%----%> 标记之间。

包含文件

文件可以作为模板的一部分包含在内。Include 文件可以包含模板中有效的任何内容部分。

Include 文件使用以下表示法指定

<!--#include file="filename"-->

路径是绝对路径或相对于模板文件的路径。

历史

  • 2007/11/04:更新了下载 - 增强了调试体验
  • 2007/11/16:更新了文章和下载 - 修复了解析器问题,支持自定义模板语法,生成器返回错误级别。
© . All rights reserved.