ORM工具开发系列的代码生成工具的开发,接上回,继续来设计基于模板的代码生成器。
模板编辑器 Template Editor
编辑器的基本要求是,文件编辑(Copy,Cut,Paste,Find/Replace)功能,语法高亮显示,智能提示。
对于.NET系统的内置类型,可以预先加载,并提供智能提示功能。
如图所示,可以直接引用系统内置的类型,在编辑模板时,会自动调出智能提示窗口。
如果能做到自定义的变量可达到这种效果,给模板的编写带来极大的方面,如下图所示,自定义属性Math的提示窗口
自定义的类型的解析的一个困难之处在于,它是动态增加进来的,无法预先知道它的类型定义。比如
先输入属性IncludeDelete,是简单类型,不需要智能提示支持
<%@ Property Name="IncludeDelete" Type="System.Int32" Default="123" Category="Options" Description="If true delete statements will be generated." %>
再写一个自定义类型MathProgram,名字是Math
<%@ Property Name="Math" Type="MathProgram" Category="Text"Description="Namespace for this class" %>
敲完了这一句,解析器仍然无法工作,因为它不知道MathProgram类型来自于哪里,是哪一层命名空间下的。(.NET允许同一程序集,在不同的命名空间下,有相同的类型名称)。当敲完了下面的语句
<%@ Assembly Name="TestClassLibrary" %>
<%@ Import Namespace="EPN.Common" %>
解析器会到程序集TestClassLibrary的命名空间EPN.Common下面去找MathProgram类型,如果能找到类型定义,则把它加入到智能提示窗口中,否则不加入。
再复杂一点的情况,有两个程序集引用和两个命名空间导入,代码如下
<%@ Assembly Name="TestClassLibrary" %>
<%@ Import Namespace="EPN.Common" %>
<%@ Assembly Name="TestProviderLibrary" %>
<%@ Import Namespace="Paradox.Common" %>
有一个类型声明,代码如下所示
<%@ Property Name="Math" Type="MathProgram" Category="Text"Description="Namespace for this class" %>
这样,需要到以上两个程序集的两个命名空间中去匹配,找到指定的类型定义则可以加入智能提示成员。情况再糟糕一点,程序集TestClassLibrary的EPN.Common命名空间下和程序集TestProviderLibrary的Paradox.Common命名空间下,都含有类型MathProgram的定义。这时应当主动报编译错误。
模板代码生成的原型
通过以下的倒退流程,由生成的代码,推回到模板定义,以解释模板代码生成的基础模型。
假设目标代码是这样的,一个简单的类型定义
public class MathProgram
{
public MathProgram()
{
}
}
用简单的变量替换,可以写成这样的代码,也就是把类型名字换成字符串值
<% private string classname = “MathProgram”; %>
public class <%=classname%>
{
public <%=classname%> ()
{
}
}
如何做到变量可以替换?把上面的代码,放到一个Stream中,在执行时替换变量定义即可
string classname = “MathProgram”;
MemoryStream mStream = new MemoryStream();
StreamWriter writer = new
StreamWriter(mStream,System.Text.Encoding.UTF8);
writer.Write(@"public class );
writer.Write(classname);
writer.Write(@"{
public );
writer.Write(classname);
writer.Write(@"()
{
}
});
StreamReader sr = new StreamReader(mStream);
writer.Flush();
mStream.Position = 0;
string code = sr.ReadToEnd();
最终的变量code就是我们需要的结果。
再推进一下,字符串值可以由用户来输入或是通过Properties窗体来设置,那就是像这样
<%@ Property Name="classname" Type="String" Category="Name" Default="MathProgram" %>
public class <%=classname%>
{
public <%=classname%> ()
{
}
}
这样,可以通过Properties窗体来改变类型的名字,而不用每次都改模板里面的Default的值"MathProgram"。
执行时的替换是什么意思?再体会这句话的含义:模板会被解析引擎转化为一个类型定义,然后会被动态编译成类型,调用它的方法输入类型的结果。在以前的文章《工作多年后才明白的.NET底层开发技术》中提到过的工资公式编译器,也是这个原理:通过构造一个类型,执行它的方法,最后取出执行结果,就是我们需要的生成后的代码。
代码看起来是这样的,生成程序集,调用指定的类型的方法,取方法的执行结果即可
Assembly assembly=CreateAssembly(sourceTemplate,parameters)
Type type=assembly.GetType(“Builder”);
InvokeMethod(type,”Render”, new object []{ “MathProgram” });
自定义的程序集,可以通过下面的方式统一放到指定的目录中
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="bin;AddIns;Providers"/>
</assemblyBinding>
</runtime>
这个设置允许bin,AddIns,Providers目录都可以存放程序集文件,而不会发生Load时FileNotFoundedException。
多种语法形式的模板
.NET如今正统的语言是VB.NE和C#,曾经红火的J#,Delphi.NET都已经退出市场。Template Studio也要支持这两种语言的模板,语法仍然兼容于Code Smith的语法声明
<%@ CodeTemplate Language="C#" TargetLanguage="C#" Description="Generates a very simple business object." %>
VB.NET的写法如下
<%@ CodeTemplate Language="VB" TargetLanguage="C#" Description="Generates a very simple business object." %>
如果是用Language是VB,代码中需要定义变量或是包含代码段的地方,都应该用VB的语法方式
属性定义
<% Private Const RUN_MULTIPLIER As Integer = 10 %>
代码片段
<% For i = 0 To RUN_MULTIPLIER – 1 %>
<%=Math.ApplictionName%> <%=i%>
<% Next i %>
此外,在设计生成的类型时,还需要注意,要全部用VB的语法,而不是C#的语法代码。
在编译时,需要把编译器由CSharpCodeProvider换成VBCodeProvider,以正确编译生成的源代码类型。
Flexible and Plug-in-based using Provider Pattern 应用Provider模式实现灵活的插件式编程
ASP.NET发行之初,通过著名的论坛程序Community Sever和ASP.NET Forum,首先提出了Provider模式。
先抄写一段赞美Provider模式的段落语句
ASP.net 2.0 的 Provider 模型为开发者提供了将他们自己的实现作为一种特性加入到运行时的可扩展方法。Membership Provider 与 Role Provider 在 ASP.net 2.0 中都通过细化一个接口或者协议来遵循 Provider 的模型。如果你创建你的组件来实现 Provider 模型定义的协议,你可以插入你的代码到 ASP.net 运行时并且替换或者扩展已经存在的 Provider。在 ASP.net 2.0 的 Provider 模型包括一个 Provider 配置与初始化的基础结构。
再以下面的代码中的例子,帮助理解Provider模式
public abstract class ProviderBase
{
protected string name;
protected string description;
public string Name { get; }
public string Description { get; }
public abstract void Initialize();
public abstract void GenerateSchema();
}
public abstract class SQLServerProvider : ProviderBase
{
public override void GenerateSchema() { }
}
public class MySqlProvider : ProviderBase
{
public override void GenerateSchema() { }
}
public static class SchemaProvider
{
static SchemaProvider()
{
}
static ProviderBase provider;
public static ProviderBase Provider { get { return provider; } }
public static ProviderBase Instance
{
if(provider=null)
{
swith(databaseType)
{
case DatabaseType.SQLServer:
provider=new SQLServerProvider();
break;
case DatabaseType.MySql:
provider=new MySqlProvider ();
break;
}
}
return provider
}
}
正式的Provider的例子,会用到配置文件,我这里作了简化,直接用代码判断。
Provider模式在Template Studio中的应用举例
1)连接到数据库服务器,获取元数据的方法
2)模板代码的生成,VB和C#两种代码生成器,分别相同的类型的不同代码实现(VB.NET和C#)
代码生成器的高级主题
1)要支持批量代码生成。比如对一个数据表GBITEM,有实体ItemEntity对应,需要为它生成四个类型:ItemEntity.cs,ItemValidation.cs,ItemInterface.cs,ItemManager.cs
要支持,在一步的情况下,同时生成这四个类型,通常是四个UTF8格式的代码文件。
像LLBL Gen 3.x这样,把多个模板绑定到一个templatebinding文件中,传入一个llblgenproj项目文件
2) 模板生成的代码,要可以被第三方的工具调用。在我开源工具Smith Builder中,它的最有价值的代码,就是获取参数值,传放到模板中,批次生成代码。
如下面的代码所示,可以以代码方式调用,脱离IDE Template Studio的限制,灵活运用
用Code Smith SDK中的代码示例子,如下所示
Dim compiler As CodeTemplateCompiler
compiler = New CodeTemplateCompiler("StoredProcedures.cst")
compiler.Compile()
If compiler.Errors.Count = 0 Then
Dim template As CodeTemplate
template = compiler.CreateInstance()
Dim database As DatabaseSchema
database = New DatabaseSchema(New SqlSchemaProvider(), "Data Source=.\SQLEXPRESS;AttachDbFilename=PetShop.mdf;Integrated Security=True;Connect Timeout=30;User Instance=True")
Dim table As TableSchema
table = database.Tables("Inventory")
template.SetProperty("SourceTable", table)
template.SetProperty("IncludeDrop", False)
template.SetProperty("InsertPrefix", "Insert")
template.Render(Console.Out)
Else
Dim i As Integer
For i = 0 To compiler.Errors.Count
Console.Error.WriteLine(compiler.Errors(i).ToString())
Console.Read()
Next
End If
这种能力的威力是巨大的,这意味着模板可以脱离IDE的存在,被第三方的工具集成。Code Smith 本身是不收费的,它只收费Code Smith Studio,集成化的编辑,调试模板的IDE。
也只有这个原因,它才会被ORM工具以API的方式所调用,如下图所示
ORM.NET是.NET 1.x时代流行ORM工具,可惜后来停止了更新。似乎可以应验开源死的说法,如果不开源,或是转向商业化,或许发展会好很多。许多项目一放到互联网上,就慢慢的停止更新了。
3)支持主流的数据库。SQL Server,Oracle,MySQL这些常见的数据库,都需要写程序集来获取元数据,用于代码生成。这不是难点,有现成的开源的Code Smith的Prover作例子。这里也可以参考《LLBL Gen 3.x 源代码追踪与解析 查询命令的追踪》中提到的Dynamic Query Enginee,DQE,动态查询引擎,对不同的数据库,引用不同的程序集。
在Template Studio这边,这不算是难点,但是放到ORM框架-工具-产品开发的全局来看(big picture),这一部分是ORM框架重要的特性部分:多数据库平台支持。ORM天生是数据库独立的,不依赖于数据库特性。
在讲解ORM框架-工具-产品开发的ORM框架时,这一部分再做重点讲解。
4).NET程序集中的多重定向
先回忆一个场景,VS2005发布之后,微软又陆续发布了WCF,WPF等.NET 3.0/3.5的技术。如果要编译的原代码的Target=.NET 2.0,而要引用的程序集的版本可以是.NET 3.0/3.5;当时可以用VS2005做到这一点。但是后来VS2008发布,这种好处消失,即使是现在的VS2010也做不到,无法用低版本Target的程序集引用高版本Target的程序集。
这会影响到什么? 假设TestClassLibrary编译是的Target是.NET 4.0,而运行模板的程序集,Template Studio的Target是.NET 20,如下所示的类型引用:
<%@ Assembly Name="TestClassLibrary" %>
<%@ Import Namespace="EPN.Common" %>
这会影响到执行结果。这个困扰,可以用下面的办法解决,将应用程序设置为在版本 3.5 上运行
<runtime>
<compatibilityversion major="3" minor="0"/>
</runtime> <startup> <supportedRuntime version="v3.5.7000"/>
</startup>