使用自定义 ASP.NET 生成提供程序和编译器技术创建 DAL 组件






4.86/5 (23投票s)
本文介绍如何使用 ASP.NET 生成提供程序和一种自定义的描述语言在 C# 中创建数据访问层组件 (DALC),包括一个简单的扫描器、解析器和 CodeDOM 生成器。
引言
互联网上有很多关于创建和使用数据访问层 (DAL) 及其组成组件 (也称为 DALC (DAL Components)) 的文章。创建 DAL 的过程并没有什么新东西。您可以使用强类型数据集 (Typed DataSets)、微软的企业库 (DAAB),或者使用许多第三方工具之一来实现一个全面的 DAL 系统。
本构成的主要目标是展示如何创建和使用 ASP.NET 生成提供程序,以及如何轻松地分析一种简单的自定义描述语言来声明 DALC 或其他任何内容。实现一个可以用于您整个编程生涯的完整的 DAL 杀手级应用程序并不是本文的目标。通常情况下,您不会定义自己的描述语言来声明 DALC,而是会使用一种基于 XML 的组件描述,并使用 .NET Framework 提供的功能丰富的 XML 类来分析它们。
我只是想实现一个词法分析器 (也称为扫描器或标记器)、一些解析技术,以及使用 .NET CodeDOM 进行动态代码生成。在日常工作中,有很多情况可以方便地开发一种解析器 (即使非常小且简单),从而提出一个可接受且优雅的解决方案。事实上,为了一个客户,我定义了一种描述语言来自动化 Web 应用程序的扩展。
为了展示通过 DALComp 应用程序动态生成的 DAL 的最终结果示例,我们假设有一个名为 Articles
的数据库表。对于这个表,DAL (请参阅下文“DALC 描述语言”部分) 或生成提供程序将自动创建一个名为 Article
的类,其中包含与表列名对应的 private
成员字段和 public
属性。对于值类型,会创建可空类型。
此外,系统会生成 static
方法 (也在 .dal 文件中定义),用于选择请求的数据。数据以 Article
类型的泛型列表 (在 C# 中是 List
) 的形式返回,并可如下使用:
示例 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" />
那么,让我们开始吧!
生成提供程序
生成提供程序是 ASP.NET 和 .NET Framework 2.0 版本的新特性。它们基本上允许您集成到 ASP.NET 的编译过程和构建环境中。这意味着您可以定义一种新的文件类型,您可以基于任意文件内容为该文件类型生成源代码。然后,源代码 (例如,以 CodeCompileUnit
的形式提供) 将被构建到最终编译的网站程序集中。在我们的例子中,我们将定义一种新的文件类型 .dal,其文件内容将是 Text
类型,其中包含我们自己的小型描述语言来定义 DALC。
实际上,ASP.NET Framework 对 .aspx 和 .ascx 等文件类型以及更多文件类型所做的也是同样的事情。相应的生成提供程序定义在全局 web.config 配置文件中。例如,文件类型 .apsx 由一个名为 PageBuildProvider
的框架类处理。如果您有兴趣了解 ASP.NET 团队是如何实现此提供程序的,您可以使用 ILDASM 或 Lutz Roeder 的“ .NET Reflector ”来反编译代码。
要在您的 Web 应用程序中使用生成提供程序,您必须在本地 web.config 中激活新的文件类型。对于 DALComp
应用程序,文件类型 .dal 在配置文件中定义如下:
<compilation>
<buildProviders>
<add extension=".dal"
type="Parago.DALComp.DALCompBuildProvider,
DALComp.BuildProvider"/>
</buildProviders>
</compilation>
从现在开始,DALCompBuildProvider
类将处理所有扩展名为 .dal 的文件。该类扩展了抽象基类 BuildProvider
并重写了 GenerateCode
方法。ASP.NET 在网站的编译和构建过程中调用此方法,并将 AssemblyBuilder
类型的实例传递给该方法。然后,可以通过调用 AssemblyBuilder
的 AddCodeCompileUnit
方法来添加代码。
CodeCompileUnit
s 是 CodeDOM 程序图的容器。基本上,它们是源代码的内部映像。支持代码提供程序模型的每种 .NET 语言都可以基于 CodeCompileUnit
以其自己的语言创建源代码。
创建干净的、与语言无关的 CodeDOM
程序图是一项令人讨厌且有些繁琐的任务。如果您想为不同的语言生成源代码,就必须创建它。DALComp
的 CodeGen
类目前为 C# 和 VB.NET 生成 DAL 源代码。
BuildProvider
类还提供了一个名为 OpenReader
的方法来读取源代码 (扩展名为 .dal 的文件)。接下来的步骤是标记化、解析,并生成一个 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 (存储在特殊文件夹 App_Code 中) 中的 DAL 定义示例:
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()
}
该语言的语法使用扩展的巴科斯-瑙尔范式 (EBNF) 定义,它是基本巴科斯-瑙尔范式 (BNF) 元语法表示法的一种扩展,用于正式描述语言。以下语法规则说明了 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
应用程序使用非常基础的编译器技术。实现一个真实世界的编译器可能是一项非常复杂的任务。它涉及语法错误恢复、变量作用域或字节码 (IL) 生成等技术,以及更多。
实现的第一步是创建一个标记器。标记器逐个字符地分析输入,并尝试将其分解为所谓的标记。标记是文本的分类块。类别可以是语言关键字,如 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)。这个名称被误用,因为该结构根本不是树。在此上下文中使用的 AST 只是 DALC 类对象和设置 (Config 部分) 的泛型列表。
Parser
类的实现也简单明了。我没有时间详细解释解析及其背后的概念是如何工作的。这是一个巨大的计算领域。Parser
类的源代码是自解释的,易于理解。
解析器基本上只是实现了上面通过扩展巴科斯-瑙尔范式定义的语法规则,请参阅“DALC 描述语言”部分。
/// <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
类的 mencionado 方法 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
图,该图可被 ASP.NET 用于编译成 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 accessible 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 definitions 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
// accessible 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 accessible 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 规范文件,该方法读取 DALC 表的每个模式表,构建一个新的类类型,并将所有列添加为其 private
成员字段和 public
属性。如果列数据类型是值类型,它将创建一个该值类型的可空版本,如下所示:
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<int>
。这里是一个自动生成的 C# 代码示例:
public class Article {
private System.Nullable<int> _Id;
private string _Title;
private string _Text;
private string _Text2;
private string _Language;
private System.Nullable<int> _Category;
private string _CreatedBy;
private System.Nullable<System.DateTime> _CreatedOn;
public Article() {
}
public virtual System.Nullable<int> Id {
get {
return this._Id;
}
set {
this._Id = value;
}
}
// Helper method to query data
public static List<Article> SelectData(string sql) {
List<Article> result;
result = new List<Article>();
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<int>)(reader["ArticleID"]));
}
result.Add(o);
}
reader.Close();
connection.Close();
return result;
}
// DALC function
public static List<Article> SelectAll() {
string internalSql;
internalSql = "SELECT * FROM Articles";
return SelectData(internalSql);
}
}
有关更详细的信息,请参阅源代码。
摘要
DAL 本身是一个基本的实现,展示了动态代码的创建概念。准确地说,DALComp
编译器实际上更像是一个源到源的翻译器而不是编译器。当前版本只生成选择数据的代码,没有更新或插入。如您所见,通过增强描述语言和生成更多动态代码来使 DAL 具有生产力,还有很大的扩展空间。
有关构建编译器和虚拟机的信息,我推荐 Pat Terry 的《Compiling with C# and Java》一书。另一种学习编译器技术实践的方法是查看 .NET 版 Python IronPython 的源代码。源代码可在 CodePlex 网站上下载。
对于真实世界的编译器开发,有许多可用的实用工具,例如 Coco/R,一个扫描器和解析器生成器,或者 ANTLR 编译器工具 (由 Boo 编译器使用)。您还可以在 Microsoft Research 的网站上找到大量信息,例如 F# 编译器。另一个有趣的主题是 Phalanger 项目 (“The PHP Language Compiler for the .NET Framework”) 在 CodePlex.com 上。
历史
- 2006年10月25日:初始版本