这是 Dflying Chen 在博客中推荐的文章,我看过之后感觉作者的想法真的很好,即使不用他介绍的方法也算可以开阔一下思路,所以就翻译了。因为这是我初次翻译,难免一些bug,请大家多提宝贵意见!
原文见:http://www.codeproject.com/useritems/DALComp.asp
使用自定义的ASP.NET Build Provider以及编译器技术创建DAL组件
原文:Juergen Baeurle
翻译:Jack Zhai
源代码下载
说明
在因特网上有很多的文章介绍了如何创建和使用一个DAL(数据访问层)以及包含这个层的组件(也被称为DALC,即数据访问层组件)。在创建一个DAL的过程中,本文没有提到什么新的方法,实际上,你可以使用类型化的DataSet,微软的Enterprise Library(DAAB)或者你可能会用到第三方工具来实现一个复杂的DAL系统。
这篇文章的主要目的是为了展示怎样创建并使用一个ASP.NET build provider,同时,本文也说明了分析一个简单的、自我定义的描述语言是多么容易,这个描述语言可以用来声明DALC或者其他的什么东西。本文的目的并不是要试图实现一个成熟的DAL超强的应用程序,使得你可以在你以后的编程生涯中尽情使用。通常情况下,你无需自己定义一种描述语言来声明DAL,只需使用一个基于XML的组件描述,并使用.NET框架提供的功能强大的XML类库来分析它就可以了。
在这里,我要完成一个语句分析器(scanner或tokenizer 注:scanner和tokenizer均为编译原理中的术语,分别表示扫描器和词法分析器)、一些语法分析技术和使用了.NET的CodeDOM的动态代码生成。在日常工作中有很多情况下,我们可以方便的开发出一些语法分析器(即使是非常小而且简单)来帮助实现一个可以接受的、优雅的解决方案。事实上,我曾经为我的一个客户定义了一种描述语言,实现了一个web应用程序的自动化扩展。
为了使用一个名为DALComp的应用程序给大家展示动态生成DAL的最终结果,我们假定有一个名为Articles的数据库表。对于这张表(详见下面的“DALC描述语言”一节)build provider会创建一个名为Article的类,这个类包括私有字段和与数据表中列名相对应的公共属性。我为一些值类型创建了相应的可空类型(Nullable Type)。
另外,这个系统生成了一些静态方法(也定义在.dal文件中)来查询被请求的数据。查询得到数据以Article类的范型列表(List<Article>)的形式返回,并可以像下面这样被使用:
例1:
foreach(Article article in Article.SelectAll())
Console.WriteLine(article.Title);
例2:
ArticlesGridView.DataSource=Article.SelectAll();
ArticlesGridView.DataBind();
例3:
<asp:ObjectDataSource ID="ArticlesDS" TypeName="Parago.DAL.Article" SelectMethod="SelectAll" runat="server" />
所以,让我们开始吧!
Build Provider
Build provider是ASP.NET和.NET框架2.0的新特性。它从底层上允许将你自己集成到ASP.NET的编译过程和生成环境中。这意味着你可以定义一个新的文件类型,并使用任意自定义格式的文件内容来生成源代码。这些源代码(将被提供给CodeCompileUnit类的实例)将被生成到ASP.NET编译网站最终得到的程序集中。在我们的例子中,将会定义一个新的文件类型.dal,并且该文本文件的内容包含了我们所定义DALC的描述语言。
事实上,ASP.NET框架在底层为.aspx、.ascx和其他文件类型做了完全相同的事,其相应的build provider被定义在全局web.config配置文件中,以.aspx文件为例,它被一个名为PageBuildProvider的.NET框架类所处理。如果你对ASP.NET团队如何实现的这个provider感兴趣,你可以使用ILDASM或者Lutz Roeder的“.NET Reflector”来反汇编它的程序集。
要在你的网站使用build provider,你必须在web.config中激活新的文件类型。DALComp应用程序中的.dal文件类型被如下这样定义:
<compilation>
<buildProviders>
<add extension=".dal" type="Parago.DALComp.DALCompBuildProvider, DALComp.BuildProvider"/>
</buildProviders>
</compilation>
从此开始,CompBuildProvider类将会处理所有后缀为.dal的文件。扩展了抽象类BuildProvider的类需要重写GenenrateCode方法,ASP.NET将会在编译和生成网站的时候调用这个方法,并为其传递一个类型为AssemblyBuilder的参数,可以通过调用其AddCodeCompileUnit方法来插入源代码。
CodeCompileUnit代表了CodeDOM程序图中的容器,从根本上说,它是源代码的一个内部映像。每一个支持代码提供器模型的.NET语言可以通过CodeCompileUnit创建该语言的源代码。
创建一个完全语言无关的CodeDOM程序图是一件令人讨厌并且笨重的工作,但是,如果你需要为多种语言要生成源代码,你则必须这样做。DALComp应用程序中的CodeGen类负责生成当前C#和VB.NET版本的DAL源代码。
BuildProvider类提供了一个名为OpenReader的方法来读取源代码(这里指后缀为.dal的文件),下一步就是词法分析(tokenize)、语法分析然后生成一个CodeDOM程序图,我们可以将其转变为ASP.NET的生成过程:
Tokenizer tokenizer=new Tokenizer(source);
Parser parser=new Parser(tokenizer);
CodeGen codeGen=new CodeGen(parser);
builder.AddCodeCompileUnit(this, codeGen.Generate());
下一节,我们现来看一个源代码,来看看我们将要分析和用来生成代码的描述语言是什么样的。
DALC 描述语言
这种在形式描述DAL组件的描述“语言”使用一种非常简单的语法。下面的代码展示了一个包含在文件Sample.dal的DAL定义(保存在特殊文件夹App_Code中):
Config {
Namespace = "Parago.DAL",
DatabaseType = "MSSQL",
ConnectionString = "Data Source=.\SQLEXPRESS;
"
}
//
// DAL component for table Articles
//
DALC Article ( = Articles ) {
Mapping { // Map just the following fields, leave others
ArticleID => Id,
Text1 => Text
}
SelectAll()
SelectByAuthor(string name[CreatedBy])
SelectByCategory(int category[Category])
}
DALC Category( = "Categories" ) {
SelectAll()
}
语言语法的定义使用了扩展的“巴科斯-诺尔范式”(扩展了基本的巴科斯-诺尔范式的元语法记号,一种正式的描述语言的方法),下面的语法规则定义了DALC描述语言:
digit
= "0-9"
letter
= "A-Za-z"
identifier
= letter { letter | digit }
string
= '"' string-character { string-character } '"'
string-character
= ANY-CHARACTER-EXCEPT-QUOTE | '""'
dal
= config dalc { dalc }
config
= "Config" "{" config-setting { "," config-setting } "}"
config-setting
= ( "Namespace" | "DatabaseType" | "Connectionstring" ) "=" string
dalc
= "DALC" identifier [ dalc-table ] "{" [ dalc-mapping ] dalc-function { dalc-function } "}"
dalc-table
= "(" "=" ( identifier | string ) ")"
dalc-mapping
= "Mapping" "{" dalc-mapping-field { "," dalc-mapping-field } "}"
dalc-mapping-field
= ( identifier | string ) "=>" identifier
dalc-function
= identifier "(" [ dalc-function-parameter-list ] ")"
dalc-function-parameter-list
= dalc-function-parameter { "," dalc-function-parameter }
dalc-function-parameter
= ( "string" | "int" ) identifier "[" identifier | string "]"
下一节解释如何扫描和分析上面的语法。
编译技术
DALComp以一种非常简单的方式使用了编译技术,而实现一个真实世界的编译器是非常复杂的,它包含了语法错误纠正、变量作用域侦测和字节码生成等等。
实现的第一步是创建一个词法分析器,词法分析器逐字符的分析传入数据并尝试将其分割为一个个单词(token)。单词为已分类的文本块,其类别可能为语言的关键字,如C#循环语句“for”,也可能为操作符,如“=”,或空白。DALComp中定义了一个代表了单个单词的Token类和一个定义了单词类别的枚举类型TokenKind:
public enum TokenKind {
KeywordConfig,
KeywordDALC,
KeywordMapping,
Type,
Identifier,
String,
Assign, // =>
Equal, // =
Comma, // ,
BracketOpen, // [
BracketClose, // ]
CurlyBracketOpen, // {
CurlyBracketClose, // }
ParenthesisOpen, // (
ParenthesisClose, // )
EOT // End Of Text
}
public class Token {
public TokenKind Type;
public string Value;
public Token(TokenKind type) {
Type=type;
Value=null;
}
public Token(TokenKind type, string value) {
Type=type;
Value=value;
}
}
Tokenizer类通过逐字符的分析输入流进行词法分析。Tokenizer的构造器通过传入要扫描的文本初始化实例,并创建一个Token类的范型队列,然后调用Start方法进行工作:
public Tokenizer(string text) {
// To avoid index overflow append new line character to text
this.text=(text==null?String.Empty:text)+"\n";
// Create token queue (first-in, first-out)
tokens=new Queue<Token>();
// Tokenize the text!
Start();
}
构造器通过在输入文本末尾添加一个多余的字符(“\n”)来避免索引溢出。Start方法的实现看起来像如下这样:
void Start() {
int i=0;
// Iterate through input text
while(i<text.Length) {
// Analyze next character and may be the following series of characters
switch(text[i]) {
// Ignore whitespaces
case '\n':
case '\r':
case '\t':
case ' ':
break;
// Comment (until end of line)
case '/':
if(text[i+1]=='/')
while(text[++i]!='\n') ;
continue;

case '{':
tokens.Enqueue(new Token(TokenKind.CurlyBracketOpen));
break;
case '}':
tokens.Enqueue(new Token(TokenKind.CurlyBracketClose));
break;
// '=' or '=>'
case '=':
if(text[i+1]=='>') {
i++;
tokens.Enqueue(new Token(TokenKind.Assign));
}
else
tokens.Enqueue(new Token(TokenKind.Equal));
break;

就像你看到的,词法分析器的实现过程简单而直接。Tokenizer类还有另外两个方法:PeekTokenType用来查看队列中下一个单词的类型;GetNextToken则真正从队列中返回下一个单词(同时从队列中将其移除):
public TokenKind PeekTokenType() {
// Always return at least TokenKind.EOT
return (tokens.Count>0)?tokens.Peek().Type:TokenKind.EOT;
}
public Token GetNextToken() {
// Always return at least Token of type TokenKind.EOT
return (tokens.Count>0)?tokens.Dequeue():new Token(TokenKind.EOT);
}
在编译源代码的下一步中,这两个方法被语法分析器调用。语法分析的过程是对单词序列进行分析,目的是根据一个给定的语法规则来确定单词序列的语法结构。一个Tokenizer类的实例将被传递给Parser类,Parser类能够产生一个叫做抽象语法树(AST)的结构来代表一个语义上正确的源代码。因为这个结构并不是任何形式的树结构,抽象语法树这个名字其实是被误用了,在上下文中使用的抽象语法树只不过是一个DALC类对象和配置(.daf文件中的Config部分)的范型列表。
Parser类的实现同样的简单和直接,我没有太多的时间详细解释语法分析和后台的一些概念是怎样工作的,这是一个非常大的计算领域。Parser类的代码是自注释的,并且非常易懂。
根本上说,语法分析器就是实现了上述用扩展的“巴科斯-诺尔范式”定义的语法规则:
/// <summary>
/// dal = config dalc { dalc }
/// </summary>
void ParseDAL() {
ParseConfig();
do {
ParseDALC();
} while(Taste(TokenKind.KeywordDALC));
Eat(TokenKind.EOT);
}
/// <summary>
/// config = "Config" "{" config-setting { "," config-setting } "}"
/// </summary>
void ParseConfig() {
Eat(TokenKind.KeywordConfig);
Eat(TokenKind.CurlyBracketOpen);
ParseConfigSetting();
while(true) {
if(!Taste(TokenKind.Comma))
break;
Eat();
ParseConfigSetting();
}
Eat(TokenKind.CurlyBracketClose);
}

进行语法分析的方法使用了辅助函数来“吃掉”队列中的单词,具体来讲是通过调用前面提到的Tokenizer类的GetNextToken方法或者简单终止语法分析过程。这有一个例子:
/// <summary>
/// Looks ahead the token line and returns the next token type.
/// </summary>
bool Taste(TokenKind type) {
return tokenizer.PeekTokenType()==type;
}
/// <summary>
/// Returns the next token.
/// </summary>
string Eat() {
token=tokenizer.GetNextToken();
return token.Value;
}
/// <summary>
/// Returns the next token of type, otherwise aborts.
/// </summary>
string Eat(TokenKind type) {
token=tokenizer.GetNextToken();
if(token.Type!=type)
Abort();
return token.Value;
}
/// <summary>
/// Returns the next token of any of the passed array of types, otherwise aborts.
/// </summary>
string EatAny(TokenKind[] types) {
token=tokenizer.GetNextToken();
foreach(TokenKind type in types)
if(token.Type==type)
return token.Value;
Abort();
}
第三个阶段是使用Parser类生成的结构并将器转换为CodeDOM结构,这样就可被用于创建C#或VB代码。这一个阶段被称为代码生成阶段。面向.NET平台的语言编译器往往会生成中间语言(IL)代码,而DALComp应用程序不生成任何IL代码而是生成CodeDOM图以用来被编译进web程序集。
CodeGen类的Generate方法首先创建一个CodeDOM结构的容器,然后向其添加一个命名空间单元,再试图通过DAL定义文件中定义的连接字符串(详见Sample.dal)连接到数据库:
// Create container for CodeDOM program graph
CodeCompileUnit compileUnit=new CodeCompileUnit();
try {
// If applicable replace the value '|BaseDirectory|' with the current
// directory of the running assembly (within the connection string)
// to allow database access in DALComp.Test.Console
string connectionString=dal.Settings["CONNECTIONSTRING"]
.Replace("|BaseDirectory|", Directory.GetCurrentDirectory());
// Define new namespace (Config:Namespace)
CodeNamespace namespaceUnit=new CodeNamespace(dal.Settings["NAMESPACE"]);
compileUnit.Namespaces.Add(namespaceUnit);
// Define necessary imports
namespaceUnit.Imports.Add(new CodeNamespaceImport("System"));
namespaceUnit.Imports.Add(new CodeNamespaceImport("System.Collections.Generic"));
namespaceUnit.Imports.Add(new CodeNamespaceImport("System.Data"));
namespaceUnit.Imports.Add(new CodeNamespaceImport("System.Data.SqlClient"));
// Generate private member fields (to save public property values)
// by analyzing the database table which is defined for the DALC
SqlConnection connection=new SqlConnection(connectionString);
connection.Open();
// Generate a new public accessable class for each DALC definition
// with all defined methods
foreach(DALC dalc in dal.DALCs) {
// Generate new DALC class type and add to own namespace
CodeTypeDeclaration typeUnit=new CodeTypeDeclaration(dalc.Name);
namespaceUnit.Types.Add(typeUnit);
// Generate public empty constructor method
CodeConstructor constructor=new CodeConstructor();
constructor.Attributes=MemberAttributes.Public;
typeUnit.Members.Add(constructor);
// Get schema table with column defintions for the current DALC table
DataSet schema=new DataSet();
new SqlDataAdapter(String.Format("SELECT * FROM {0}", dalc.Table), connection)
.FillSchema(schema, SchemaType.Mapped, dalc.Table);
// Generate for each column a private member field and a public
// accessable property to use
foreach(DataColumn column in schema.Tables[0].Columns) {
// Define names by checking DALC mapping definition
string name=column.ColumnName;
string nameMapped=dalc.Mapping.ContainsKey(name.ToUpper())?dalc.Mapping[name.ToUpper()]:name;
// Generate private member field with underscore plus name; define
// member field type by checking if value type and create a
// nullable of that type accordingly
CodeMemberField field=new CodeMemberField();
field.Name=String.Format("_{0}", nameMapped);
field.Type=GenerateFieldTypeReference(column.DataType);
typeUnit.Members.Add(field);
// Generate public accessable property for private member field,
// to use for instance in conjunction with ObjectDataSource
CodeMemberProperty property=new CodeMemberProperty();
property.Name=nameMapped;
property.Type=GenerateFieldTypeReference(column.DataType);
property.Attributes=MemberAttributes.Public;
property.GetStatements.Add(
new CodeMethodReturnStatement(
new CodeFieldReferenceExpression(
new CodeThisReferenceExpression(),
field.Name
)
)
);
property.SetStatements.Add(
new CodeAssignStatement(
new CodeFieldReferenceExpression(
new CodeThisReferenceExpression(),
field.Name
),
new CodePropertySetValueReferenceExpression()
)
);
typeUnit.Members.Add(property);
}

}
}
基于DAL定义文件,这个方法在每一个架构表(schema table)中读取一个DALC表,建立一个新的类,添加所有的列为私有字段并为其创建相应的属性。如果某列的数据类型为值类型,它将会为其创建一个可空版本的类型,如下所示:
CodeTypeReference GenerateFieldTypeReference(Type columnType) {
// If column data type is not a value type return just return it
if(!columnType.IsValueType)
return new CodeTypeReference(columnType);
// Type is a value type, generate a nullable type and return that
Type nullableType=typeof(Nullable<>);
return new CodeTypeReference(nullableType.MakeGenericType(new Type[] { columnType }));
}
举例来说,如果某一列类型为int,这个辅助函数将会生成一个int?或者System.Nullable。下面是一个自动生成的的C#代码的例子:
public class Article {
private System.Nullable _Id;
private string _Title;
private string _Text;
private string _Text2;
private string _Language;
private System.Nullable _Category;
private string _CreatedBy;
private System.Nullable<System.DateTime> _CreatedOn;
public Article() {
}
public virtual System.Nullable Id {
get {
return this._Id;
}
set {
this._Id = value;
}
}
// Helper method to query data
public static List SelectData(string sql) {
List result;
result = new List();
System.Data.SqlClient.SqlConnection connection;
System.Data.SqlClient.SqlCommand command;
System.Data.SqlClient.SqlDataReader reader;
connection = new System.Data.SqlClient.SqlConnection("Data Source=
");
connection.Open();
command = new System.Data.SqlClient.SqlCommand(sql, connection);
reader = command.ExecuteReader();
for (; reader.Read(); ) {
Article o;
o = new Article();
if (Convert.IsDBNull(reader["ArticleID"])) {
o.Id = null;
}
else {
o.Id = ((System.Nullable)(reader["ArticleID"]));
}
result.Add(o);
}
reader.Close();
connection.Close();
return result;
}
// DALC function
public static List SelectAll() {
string internalSql;
internalSql = "SELECT * FROM Articles";
return SelectData(internalSql);
}
}
更多信息请参考源代码。
总结
DAL本身是一个基础的实现并且现实了创建动态代码的概念,精确的说,DALComp编译器与其说是编译器,不如说是一个“源代码到源代码”的翻译器更为贴切。目前的版本只能生成查询数据的方法,没有更新或者删除的操作。就像你看到的,这里有很大的空间,可以通过扩充描述语言和生成更多的动态代码来扩展DAL,以使其更有生产力。
要得到更多有关构建编译器和虚拟机的信息,我推荐Pat Terry的“Compiling with C# and Java”一书。另一个在实践中学习编译技术的方式是看一看Python的.NET版本的实现,IronPython,其源代码可以在CodePlex网站得到。
对于真实的编译器来说,有大量的工具可以使用,比如:Coco/R, 一个扫描器与语法分析器的生成器,或者ANTLR编译工具。你也可以在Microsoft Research的网站上获得许多信息,如F#编译器。另一个有趣的主题是CodePlex.com上的Phalanger项目(“.NET框架的PHP语言编译器”)。