Mo+ - 基于模板的代码生成器的演进






4.42/5 (8投票s)
比较 CodeSmith、T4 和 Mo+ 在满足代码生成要求方面的相对效率。
引言
我们坚信基于模板的代码生成方法的力量。但是,我们相信的远不止这些。我们相信您需要完全控制代码的结构和行为。我们相信您需要轻松管理异常、业务规则以及其他特殊规则和演示。我们相信您需要能够轻松定义和维护您的编码最佳实践。我们相信您需要获得全面的支持来维护,而不仅仅是创建您生成的代码。
我们相信,将基于模板的代码生成方法演变为模型驱动和面向模型的方法,是管理高质量生成代码与自定义代码相结合的最佳方式。
面向对象的程序员知道面向对象语言在创建大型复杂软件方面相对于过程语言的优势,例如封装、可重用性、模块化、清晰度等。为什么您不能从代码生成的角度获得相同的优势呢?使用 Mo+,您可以做到!Mo+ 的代码生成方法是唯一一种基于模板、模型驱动、真正面向模型,并且专注于代码维护以及初始代码生成的方法。<o:p>
在本文中,我们将比较两种基于模板的代码生成器(CodeSmith 和 T4)与 Mo+。我们将使用每种方法为 Northwind SQL Server 数据库中的每个表创建一组简单的数据访问层类。我们将比较最终结果和模板,特别是如何将模板用作更复杂任务的构建块。
背景
Mo+ 模型化编程语言和用于模型化开发的 Mo+ Solution Builder IDE 已在此Code Project 文章中介绍。
Mo+ 开源技术可在 moplus.codeplex.com 获取。该网站还提供视频教程和其他材料。Mo+ Solution Builder 还包含丰富的内置帮助。
如果您正在使用 Mo+ Solution Builder 并遵循本文,需要了解如何从数据库加载解决方案模型,请观看此关于从 SQL Server 加载模型的教程。您可以使用本文随附的模板。
待解决的问题
我们希望为 Northwind SQL Server 数据库中的每个表生成一组简单的数据访问层 C# 类。该类将只包含每个表列的 get/set 属性。以下 Category
类是与 Categories
表对应的所需输出示例。
using System;
namespace Test3.DAL
{
public class Category
{
/* this property gets/sets CategoryID */
public int CategoryID { get; set; }
/* this property gets/sets CategoryName */
public string CategoryName { get; set; }
/* this property gets/sets Description */
public string Description { get; set; }
/* this property gets/sets Picture */
public byte[] Picture { get; set; }
}
}
使用 CodeSmith 解决
CodeSmith 是一个特别强大和灵活的基于模板的代码生成器。为了解决这个问题,我们在需要 DAL 类文件的 Visual Studio 项目中创建了一个 Generator 项目。Generator 项目设置了以下 Master
模板的属性,并调用了它:
<%@ CodeTemplate Src="TemplateBase.cs" Inherits="Lib.Templates.TemplateBase" OutputType="None" Language="C#" TargetLanguage="Text" Debug="False" %>
<%@ Property Name="SourceDatabase" Type="SchemaExplorer.DatabaseSchema" Optional="False" %>
<%@ Assembly Name="SchemaExplorer" %>
<%@ Property Name="ResultsFolder" Type="System.String" Default="DAL" Optional="False" %>
<%@ Import Namespace="System.IO" %>
<%@ Register Name="DALEntity" Template="DALEntity.cst" %>
<script runat="template">
public override void Render(TextWriter writer)
{
if (!Directory.Exists(ResultsFolder)) Directory.CreateDirectory(ResultsFolder);
foreach (TableSchema table in SourceDatabase.Tables)
{
CreateDALEntity(table);
}
}
public void CreateDALEntity(TableSchema table)
{
DALEntity dalEntity = this.Create<DALEntity>();
dalEntity.SourceTable = table;
dalEntity.ClassName = GetClassName(table.Name.ToCSharpIdentifier().ToPascalCase());
OutputFile outputFile = new OutputFile(GetClassFileName(ResultsFolder, dalEntity.ClassName));
dalEntity.RenderToFile(outputFile, true);
}
</script>
对于每个数据库表,都会调用 CreateDALEntity
方法来创建一个 DAL 类文件。此方法会创建一个 <code>DALEntity
模板实例,设置该模板的属性,然后将其内容渲染到输出 DAL 类文件中。
Master
模板继承了提供可重用方法以获取类的最佳实践信息的基类。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CodeSmith.Engine;
namespace Lib.Templates
{
public partial class TemplateBase : CodeTemplate
{
public string GetClassName(string name)
{
string className = name;
if (className.EndsWith("ies") == true)
{
// replace with y
className = className.Substring(0, className.Length - 3) + "y";
}
else if (className.EndsWith("xes") == true
|| className.EndsWith("ses") == true)
{
// chop off es
className = className.Substring(0, className.Length - 2);
}
else if (className.EndsWith("as") == true
|| className.EndsWith("is") == true
|| className.EndsWith("os") == true
|| className.EndsWith("us") == true)
{
// leave as is
}
else if (className.EndsWith("s") == true)
{
// chop off s
className = className.Substring(0, className.Length - 1);
}
return className;
}
public string GetClassFileName(string directory, string className)
{
return String.Format("{0}/{1}.cs", directory, className);
}
}
}
DALEntity
模板构建 DAL 类文件的内容。
<%@ Template Language="C#" TargetLanguage="C#" Description="An example on creating a class with properties from a database table." %>
<%@ Property Name="SourceTable" Type="SchemaExplorer.TableSchema" Category="DataSource" Optional="False" %>
<%@ Property Name="Namespace" Type="System.String" Default="Test.DAL" Optional="False" %>
<%@ Property Name="ClassName" Type="System.String" Optional="False" %>
<%@ Assembly Name="SchemaExplorer" %>
<%@ Import Namespace="SchemaExplorer" %>
<%@ Import Namespace="CodeSmith.Core.Extensions" %>
using System;
namespace <%= Namespace %>
{
public class <%= ClassName %>
{
<% foreach (var column in SourceTable.Columns) { %>
/* this property gets/sets <%= column.Name.ToCSharpIdentifier().ToPascalCase() %> */
public <%= column.SystemType.FullName %> <%= column.Name.ToCSharpIdentifier().ToPascalCase() %> { get; set; }
<% } %>
}
}
该模板利用由 Master
模板设置的 Namespace
和 ClassName
属性,然后为表中的每个列生成属性内容。
这些模板生成的 Category
类如下所示
using System;
namespace Test.DAL
{
public class Category
{
/* this property gets/sets CategoryID */
public System.Int32 CategoryID { get; set; }
/* this property gets/sets CategoryName */
public System.String CategoryName { get; set; }
/* this property gets/sets Description */
public System.String Description { get; set; }
/* this property gets/sets Picture */
public System.Byte[] Picture { get; set; }
}
}
使用 T4 解决
T4 也是一个有效的基于模板的代码生成器。为了解决这个问题,我们在想要 DAL 类文件的 Visual Studio 项目中创建了一个 Master
模板。Master
模板是生成代码的高级驱动程序:
<#@ template language="C#" hostspecific="true" #>
<#@ assembly name="System.Data" #>
<#@ assembly name="Microsoft.SqlServer.ConnectionInfo" #>
<#@ assembly name="Microsoft.SqlServer.Smo" #>
<#@ assembly name="Microsoft.SqlServer.Management.Sdk.Sfc" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Data.SqlClient" #>
<#@ import namespace="Microsoft.SqlServer.Management.Common" #>
<#@ import namespace="Microsoft.SqlServer.Management.Smo" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#
string connectionString = @"Server=INCODE-1;Trusted_Connection=True;";
string databaseName = "Northwind";
string resultsFolder = @"\DAL\";
if (!Directory.Exists(resultsFolder)) Directory.CreateDirectory(resultsFolder);
Server server = new Server(new ServerConnection(new SqlConnection(connectionString)));
Database database = server.Databases[databaseName];
foreach (Table table in database.Tables)
{
string tableName = table.Name;
if (!tableName.Equals("sysdiagrams"))
{
MasterTemplateHelper.CreateDALEntity(Host, connectionString, databaseName, tableName, resultsFolder);
}
}
#>
<#+
public class MasterTemplateHelper
{
public static void CreateDALEntity(ITextTemplatingEngineHost host, string connectionString,
string databaseName, string tableName, string resultsFolder)
{
string projectNamespace = "Test2.DAL";
string className = tableName.Replace(" ", "");
string classFileName;
string relativeOutputFilePath = null;
className = GetClassName(className);
classFileName = GetClassFileName(resultsFolder, className);
string templateFile = host.ResolvePath("DALEntity.tt");
string templateContent = File.ReadAllText(templateFile);
TextTemplatingSession session = new TextTemplatingSession();
session["Namespace"] = projectNamespace;
session["ClassName"] = className;
session["ConnectionString"] = connectionString;
session["DatabaseName"] = databaseName;
session["TableName"] = tableName;
var sessionHost = (ITextTemplatingSessionHost) host;
sessionHost.Session = session;
Engine engine = new Engine();
string generatedContent = engine.ProcessTemplate(templateContent, host);
relativeOutputFilePath = resultsFolder + className + ".cs";
WriteTemplateOutputToFile(relativeOutputFilePath, host, generatedContent);
}
public static string GetClassName(string name)
{
string className = name;
if (className.EndsWith("ies") == true)
{
// replace with y
className = className.Substring(0, className.Length - 3) + "y";
}
else if (className.EndsWith("xes") == true || className.EndsWith("ses") == true)
{
// chop off es
className = className.Substring(0, className.Length - 2);
}
else if (className.EndsWith("as") == true
|| className.EndsWith("is") == true
|| className.EndsWith("os") == true
|| className.EndsWith("us") == true)
{
// leave as is
}
else if (className.EndsWith("s") == true)
{
// chop off s
className = className.Substring(0, className.Length - 1);
}
return className;
}
public static string GetClassFileName(string directory, string className)
{
return String.Format("{0}/{1}.cs", directory, className);
}
public static void WriteTemplateOutputToFile(
string relativeOutputFilePath,
Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost Host,
string templateText)
{
string outputPath = System.IO.Path.GetDirectoryName(Host.TemplateFile);
string outputFilePath = outputPath + relativeOutputFilePath;
System.IO.File.WriteAllText(outputFilePath, templateText);
}
}
#>
对于每个数据库表,都会调用 CreateDALEntity
方法来创建一个 DAL 类文件。此方法访问 <code>DALEntity
模板,设置全局 TextTemplatingSession
变量(也可以使用 CallContext
完成),然后将 <code>DALEntity
模板的内容渲染到输出 DAL 类文件中。
Master
模板还包含 MasterTemplateHelper
方法,用于获取类的最佳实践信息和生成输出文件。
DALEntity
模板构建 DAL 类文件的内容。
<#@ template language="C#" hostspecific="true" #>
<#@ parameter name="Namespace" Type="System.String" Default="MyProject.DAL" Optional="False" #>
<#@ parameter name="ClassName" Type="System.String" Optional="False" #>
<#@ parameter name="ConnectionString" Type="System.String" Optional="False" #>
<#@ parameter name="DatabaseName" Type="System.String" Optional="False" #>
<#@ parameter name="TableName" Type="System.String" Optional="False" #>
<#@ assembly name="System.Data" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="Microsoft.SqlServer.ConnectionInfo" #>
<#@ assembly name="Microsoft.SqlServer.Smo" #>
<#@ assembly name="Microsoft.SqlServer.Management.Sdk.Sfc" #>
<#@ import namespace="System.Data.SqlClient" #>
<#@ import namespace="Microsoft.SqlServer.Management.Common" #>
<#@ import namespace="Microsoft.SqlServer.Management.Smo" #>
<#
Server server = new Server(new ServerConnection(new SqlConnection(ConnectionString)));
Database database = server.Databases[DatabaseName];
Table sourceTable = database.Tables[TableName];
#>
using System;
namespace <#= Namespace #>
{
public class <#= ClassName #>
{
<#
foreach (Column column in sourceTable.Columns)
{
#>
/* this property gets/sets <#= column.Name #> */
public <#= DALTemplateHelper.GetClrType(column.DataType.ToString()) #> <#= column.Name #> { get; set; }
<#
}#>
}
}
<#+
public class DALTemplateHelper
{
public static string GetClrType(string sqlType)
{
switch (sqlType)
{
case "bigint":
return "long";
case "binary":
case "image":
case "timestamp":
case "varBinary":
return "byte[]";
case "bit":
return "bool";
case "char":
case "nchar":
case "ntext":
case "nvarchar":
case "text":
case "varchar":
case "xml":
return "string";
case "datetime":
case "smalldatetime":
case "date":
case "time":
case "datetime2":
return "DateTime";
case "decimal":
case "money":
case "smallmoney":
return "decimal";
case "float":
return "double";
case "int":
return "int";
case "real":
return "float";
case "uniqueidentifier":
return "Guid";
case "smallint":
return "short";
case "tinyint":
return "byte";
case "variant":
case "udt":
return "object";
case "structured":
return "DataTable";
case "datetimeoffset":
return "DateTimeOffset";
default:
return "object";
}
}
}
#>
该模板利用由 Master
模板设置的 Namespace
、ClassName
、ConnectionString
、DatabaseName
和 TableName
属性,然后为表中的每个列生成属性内容。还提供了一个 DALTemplateHelper
方法,用于从 SQL Server 类型获取 CLR 类型。
这些模板生成的 Category
类如下所示
using System;
namespace Test2.DAL
{
public class Category
{
/* this property gets/sets CategoryID */
public int CategoryID { get; set; }
/* this property gets/sets CategoryName */
public string CategoryName { get; set; }
/* this property gets/sets Description */
public string Description { get; set; }
/* this property gets/sets Picture */
public byte[] Picture { get; set; }
}
}
使用 Mo+ 解决
为了用 Mo+ 解决这个问题,我们在与 Visual Studio 项目相同的目录中创建了一个 Solution
模型,我们希望在该项目中生成 DAL 类文件。使用 MDLSqlModel
SQL Server 规范模板(附带),该模型加载了与 Northwind 数据库中的表和列对应的基本 Entity
和 Property
信息。与 CodeSmith 或 T4 不同,使用 Mo+,您可以随时访问模型信息,这将大大简化代码生成。您还可以通过编程或手动方式定义和增强模型,添加您需要的信息,以便更轻松地生成代码。
解决方案级别的 Master
模板是生成代码的高级驱动程序。此模板没有内容,其输出区域如下所示(图像与代码块并排显示,以显示正确的语法高亮)
<%%:
foreach (Entity)
{
<%%>DALEntity%%>
}
%%>
此模板遍历解决方案中的每个 Entity
(表),并调用 Entity
级别的 DALEntity
模板来输出其内容。在第 4 行,DALEntity
模板嵌入在输出标签中,以表示该模板将生成其输出。
通过模型信息和面向模型的模板,创建主模板变得非常简单。无需额外设置属性或调用其他模板。被调用的模板封装了生成其内容和输出所需的所有信息(如果需要,模板也可以有参数来传递额外的数据),而不是像过程式那样传递所有相关数据。
DALEntity
模板的内容区域生成 DAL 类的实际内容
<%%-using System;
namespace %%><%%=Solution.Namespace%%><%%-
{
public class %%><%%=DALClassName%%><%%-
{%%>
<%%:
foreach (Property)
{
<%%=DALProperty%%>
}
%%><%%-
}
}%%>
构建 DAL 类内容的信息直接从模型中提取,以面向模型的方式,基于此模板调用所针对的 Entity
实例。命名空间从 Solution
中检索。类名从 Entity
级别的名为 DALClassName
的模板中检索。然后,模板遍历 Entity
的每个 Property
,并从 Property
级别的 DALProperty
模板获取属性内容。
与 CodeSmith 或 T4 必须以过程式方法将数据传递给模板不同,使用 Mo+,模板的封装特性鼓励自然的构建块。事实上,Mo+ 模板可以像任何内置的面向模型的属性(如果模板有参数,则像方法)一样使用,以检索用于表达式的信息,或在属性标签中构建内容(如上所示)。
使用 CodeSmith 或 T4,您对生成文档的输出位置、时间或方式几乎没有支持。您的文档通常每次都会重新生成。然而,使用 Mo+,对输出决策的完全控制是内置的。DALEntity
模板的输出区域包含了将 DAL 类内容写入磁盘的决策
<%%=Solution.SolutionDirectory%%><%%-\DAL\%%><%%=DALClassFileName%%>
<%%:
if (File(Path) != Text)
{
update(Path)
}
%%>
第 1 行构建了 DAL 类文件将存储的 Path
。在第 3 行,如果磁盘上文件的内容与 Text
(模板内容)不同,则调用 update
语句以更新 Path
处文件的 Text
。在模板级别封装输出决策提高了可重用性,因为此决策无需由每个调用模板重复。
Entity
级别的 DALClassName
模板是一个可重用的构建块,而不是使用过程来获取类名信息。
<%%:
var className = EntityName.CapitalCase().Replace(" ", "").Replace("_", "")
if (className.EndsWith("ies") == true)
{
// replace with y
className = className.Substring(0, className.Length - 3) + "y"
}
else if (className.EndsWith("xes") == true
|| className.EndsWith("ses") == true)
{
// chop off es
className = className.Substring(0, className.Length - 2)
}
else if (className.EndsWith("as") == true
|| className.EndsWith("is") == true
|| className.EndsWith("os") == true
|| className.EndsWith("us") == true)
{
// leave as is
}
else if (className.EndsWith("s") == true)
{
// chop off s
className = className.Substring(0, className.Length - 1)
}
%%>
<%%=className%%>
为了进一步简化,上述逻辑可用于在从数据库加载模型时为 Entity
创建名称。
最后,Property
级别的 DALProperty
模板构建了 get/set 属性的内容
<%%-
/* this property gets/sets %%><%%=PropertyName%%><%%- */
public %%><%%=CSharpDataType%%><%%- %%><%%=PropertyName%%><%%- { get; set; }%%>
此示例中使用的另外两个模板 DALClassFileName
和 CSharpDataType
已附上。
这些模板生成的 Category
类如下所示
using System;
namespace Test3.DAL
{
public class Category
{
/* this property gets/sets CategoryID */
public int CategoryID { get; set; }
/* this property gets/sets CategoryName */
public string CategoryName { get; set; }
/* this property gets/sets Description */
public string Description { get; set; }
/* this property gets/sets Picture */
public byte[] Picture { get; set; }
}
}
扩展您的模板
显然,您可以使用 CodeSmith、T4 或 Mo+ 轻松解决上述代码生成问题。当您的代码生成需求复杂性和范围增加时,这些方法如何很好地扩展?
如何扩展以管理异常?例如,假设您的 DAL 类中的某些属性需要是只读的。如果您无法直接从数据库架构中获取哪些属性应该是只读的,这在 CodeSmith 或 T4 中将很难做到。但在 Mo+ 中却很容易。您可以轻松地使用任意数量的自由格式标签关键字(例如 READ_ONLY
)标记属性或其他模型元素。一个更新的 DALProperty
模板,用于管理只读属性异常,可能看起来像这样
<%%-
%%>
<%%:
if (Tags.Contains("READ_ONLY") == true)
{
<%%-
/* this property gets %%><%%=PropertyName%%><%%- */
private %%><%%=CSharpDataType%%><%%- %%><%%=PropertyName.UnderscoreCase()%%><%%-
public %%><%%=CSharpDataType%%><%%- %%><%%=PropertyName%%><%%- { get { return %%><%%=PropertyName.UnderscoreCase()%%><%%-;) }%%>
}
else
{
<%%-
/* this property gets/sets %%><%%=PropertyName%%><%%- */
public %%><%%=CSharpDataType%%><%%- %%><%%=PropertyName%%><%%- { get; set; }%%>
}
%%>
如何扩展以连接相同或不同层中的远距离元素?例如,您希望增强 DAL 类以包含其他 DAL 类的 List
属性。这在 CodeSmith 或 T4 中会很麻烦,您需要检索远距离表并调用 GetClassName
函数来检索类的名称。但再次强调,在 Mo+ 中这很容易。只需使用模型搜索中的 Entity
,或直接从模型中的相关 Entity
重用 DALClassName
模板。在更新的 DALEntity
模板中,为 Entity
的每个相关 Collection
创建属性信息。
<%%-using System;
namespace %%><%%=Solution.Namespace%%><%%-
{
public class %%><%%=DALClassName%%><%%-
{%%>
<%%:
foreach (Property)
{
<%%=DALProperty%%>
}
foreach (Collection)
{
<%%-
List<%%><%%=ReferencedEntity.DALClassName%%><%%-> %%><%%=ReferencedEntity.DALClassName%%><%%-List { get; set; }%%>
}
%%><%%-
}
}%%>
总结
希望本文能让您很好地了解 Mo+ 面向模型的方法与其他基于模板的方法相比如何,以及面向模型的方法如何理想地适用于扩展和解决更复杂的代码生成要求。如果有什么不清楚或需要展开的地方,请提出问题。
另外,如果使用 CodeSmith 或 T4 有更好或更具可扩展性的方法来解决上述问题,请告诉我。我很乐意更新文章以展示这些方法的最佳用法。
最后,我希望您能尝试 Mo+ 和 Mo+ Solution Builder。这个免费的开源产品可在 moplus.codeplex.com 获取。除了产品本身,该网站还包含用于构建更完整的模型和生成完整工作应用程序的示例包。
成为会员!
Mo+ 社区通过网站 https://modelorientedplus.com 获得额外支持,并为 Mo+ 的发展做出贡献。成为会员可为 Mo+ 用户提供额外福利,例如额外的论坛支持、会员贡献的工具以及对 Mo+ 发展方向进行投票和/或贡献的能力。此外,我们将每月为会员举办比赛,您可以通过使用 Mo+ 开发解决方案来赢取奖金。
如果您对高效的面向模型软件开发有丝毫兴趣,请注册成为会员。 它是免费的,您不会收到垃圾邮件!