DLinqEntityGenerator – 使用 XSL 模板的类 SqlMetal 的 DLinq 实体生成器
本文介绍如何使用 VS.NET 上的插件应用程序,生成映射到 SQL Server 数据库对象的 DLinq 实体。
引言
SqlMetal 工具可用于生成映射到 SQL Server 数据库的 C# 或 VB.NET 对象。它提供具有映射到表和视图列的成员和属性的类。它反映了表之间的关系。它还提供了一个派生自基类 DataContext 的类,该类可以按需映射函数和存储过程,区分表函数和标量函数,以及区分返回行集的存储过程和仅执行插入、更新、删除等操作的存储过程。另一项功能是创建用于单独存储数据库对象定义的 XML 映射。
DLinqEntityGenerator 应用程序生成对象的方式与 SqlMetal 工具类似,但它构建为 VS.NET 的插件,并提供更多选项,例如为每个数据库对象生成输出文件,以及创建附加到当前解决方案的 C# 或 VB.NET 项目。它不是取代 SqlMetal 的工具,而是一个用于生成 DLinq 类的向导式选项。

这个想法源于之前的文章 DACBuilder – 基于 XML 和 XSL 模板转换的数据访问对象生成工具,该工具使用 XSL 转换来生成映射到不同数据源的简单对象。使用 XML 和 XSL 转换要容易得多,因为在修改和后续功能时,您只需要修改模板而无需更改代码并重新编译应用程序。另一个原因是您可以使用 XSL 参数(使用 xsl:param
节点)来自定义模板。
元数据
数据库对象元数据定义使用 SQL Server 中提供的 INFORMATION_SCHEMA 视图和 FOR XML 功能。调用的对象是:
EXCLUDE_DB_OBJECTS
– 一个包含不应映射的数据库实体的表;vwGetTables
– 用于表的视图;vwGetViews
– 用于视图的视图;vwGetProcedures
– 用于存储过程的视图;vwGetFunctions
– 用于函数的视图;GetTableColumnsXML
– 一个存储过程,用于获取有关表(基表或视图)中列的元数据信息;GetRoutineParametersXML
– 一个存储过程,用于获取有关例程(存储过程或函数)参数的元数据信息;GetFunctionColumnsXML
– 一个存储过程,用于获取有关函数返回的列的元数据信息;fnGetExtendedProperty
– 一个函数,用于获取表中指定列的扩展属性;fnCreateFriendlyName
– 一个函数,用于从字符串创建友好名称(有助于提供有效的类名和标识符);
vwGetTables
和 vwGetViews
使用 INFORMATION_SCHEMA.TABLES 视图,通过对 TABLE_TYPE
列进行特定过滤,并排除 EXCLUDE_DB_OBJECTS
表中的相应对象来获取表或视图。
vwGetProcedures
和 vwGetFunctions
视图使用 INFORMATION_SCHEMA.ROUTINES
,通过对 ROUTINE_TYPE
列进行特定过滤,并排除 EXCLUDE_DB_OBJECTS
表中的相应对象来获取存储过程或函数。
GetTableColumnsXML
存储过程使用 FOR XML EXPLICIT
子句来分层存储有关指定表、其列以及与其他表的关联信息。FOR XML EXPLICIT
子句在存储过程中的逻辑可能看起来很复杂,但它是创建嵌套层次结构的标准技术。嵌套是通过使用 TAG
和 PARENT
别名以及应用于具有 IDREF
和 HIDE
指令的属性的排序来确保的。结构与 DACBuilder 应用程序使用的 GetTableColumnsXML
存储过程相似,但略有改进,以包含更多关于参照完整性的信息。
GetRoutineParametersXML
存储过程从 INFORMATION_SCHEMA.PARAMETERS
视图读取有关指定存储过程或函数的信息。
GetFunctionColumnsXML
存储过程使用 INFORMATION_SCHEMA.ROUTINE_COLUMNS
视图读取有关指定函数的信息。
向导窗体 (frmWizard) 和 DLinqEntitiesEventArgs 类
用于收集数据库对象和将映射它们的实体的相关信息的窗体称为 frmWizard
。由于我有点懒,我使用了一个小技巧来模拟向导。我在设计时添加了一个 TabControl
对象,以便在相应的步骤上创建所有必需的控件。TabControl
控件的 Visible
属性设置为 false。每个步骤都有自己的 TabPage
对象,其中包含一个 Panel
控件,该控件包含相应的 TexBox
、Label
、PictureBox
、Combobox
、Button
控件。当用户执行下一步或上一步操作时,相应的 Panel
对象会从其 TabPage
父级分离并添加到窗体。
当用户单击下一步或上一步按钮时,将调用窗体的 NextAction
或 BackAction
方法。提供了验证子句,因此如果当前步骤设置无效,用户将无法进入向导的下一步。但是,用户可以返回以更改上一步的设置。
第一步是连接信息。必须提供服务器名称或 IP 地址、用户名和密码以及数据库名称才能进入下一步。如果服务器名称、用户名和密码正确,当您“下拉”数据库组合框时,它将填充指定服务器上的所有数据库。
ServerConnection cnn = new ServerConnection(txtServer.Text, txtUser.Text,
txtPassword.Text);
cnn.Connect();
Server srvr = new Server(cnn);
DatabaseCollection oColl = srvr.Databases;
ServerConnection
类属于 Microsoft.SqlServer.Management.Common
命名空间,而 Server
和 DatabaseCollection
类属于 Microsoft.SqlServer.Management.Smo
命名空间。

第二步是数据库对象选择步骤。这些对象存储在 TreeView
控件的四个独立分支中:表、视图、存储过程和函数。每个节点都是派生自 TreeNode
基类的类实例,因为有必要存储有关数据库对象的补充信息(对象类型、名称、友好名称,如果函数是返回类型)。此信息是 DBObject
结构的一部分。
public struct DBObject
{
public DBObjectTypes ObjectType;
public string ObjectName;
public string ObjectFriendlyName;
public string RoutineDataType; // null for tables, views, stored
// procedures; sql data type for scalar
// function; TABLE for inline and multi-
// statement table-valued functions
}
如果未选中至少一个数据库对象,则无法进入下一步。

第三步是补充选项,例如项目名称、要存储结果文件的文件夹、生成的代码语言、XML 映射文件以及在生成的类上进行映射的选项。默认情况下,应用程序会以所选语言创建项目,但这可以禁用。在创建项目的情况下,选定的文件夹必须为空。项目将添加到当前解决方案。如果没有可用的解决方案,将创建一个新的解决方案。可选地,您可以将所有类生成到一个文件中。

第三步之后的摘要回顾了收集到的信息,并为每个设置提供了简短的描述。

frmWizard
窗体有一个公共事件 WizardFinished
,遵循委托的定义:
public delegate void DLinqEntitiesEventHandler(object sender,
DLinqEntitiesEventArgs e);
...
public event DLinqEntitiesEventHandler WizardFinished;
当向导完成其工作后,所有收集到的信息都存储在 DLinqEntitiesEventArgs
类型的对象中。该事件可以在 OnWizardFinished
方法中消耗此创建的对象。
private void OnWizardFinished()
{
ArrayList dbObjects = new ArrayList();
foreach(TreeNode root in tvDBObjects.Nodes)
{
foreach(DBObjectTreeNode tn in root.Nodes)
{
if(tn.Checked)
{
dbObjects.Add(tn.DBObj);
}
}
}
string dbName = cboDatabases.SelectedIndex >= 0 ?
DBHelper.CreateFriendlyName(
cboDatabases.SelectedItem.ToString()) : "";
LanguageTypes lt = rbCSharp.Checked ? LanguageTypes.CSharp :
LanguageTypes.VisualBasic;
string projectName = chkDontCreateProject.Checked ? null :
DBHelper.CreateFriendlyName(txtProjectName.Text);
string path = txtFolder.Text;
DLinqEntitiesEventArgs e = new DLinqEntitiesEventArgs(cnnString, dbName,
dbObjects, chkUseOneOnlyFile.Checked, lt,
path, projectName, txtXMLFile.Text,
chkUseMapping.Enabled &&
chkUseMapping.Checked);
if(WizardFinished != null)
{
WizardFinished(this, e);
}
}
DLinqEntitiesEventArgs
类具有以下属性:
ConnectionString
– 使用第一步中的连接信息构建;DBName
– 选定的数据库名称;DBObjects
– 一个 ArrayList,仅包含选定的数据库对象;UseOneOnlyFile
– 布尔属性,指定生成的类是输出到一个文件中,还是为每个对象生成一个文件;LanguageType
–LanguageTypes
枚举属性(两个值:CSharp
和VisualBasic
),指定生成代码的语言;Path
– 输出将保存到的完整目录名称;ProjectName
– 项目的名称;CreateProject
– 布尔属性,如果 ProjectName 不为 null 且不为空,则返回 true,否则返回 false;ProjectExtension
– 根据LanguageType,为 "csproj" 或 "vbproj";
FileExtension
– 根据LanguageType,为 "cs" 或 "vb";
GenerateXML
– 布尔属性,如果 XML 映射文件名不为 null 且不为空,则返回 true,否则返回 false;XmlMapFileName
– XML 映射文件的完整名称;UseMapping
– 布尔属性,指定是否使用映射源生成类(不使用映射的类在其定义中包含数据库对象元数据,作为自定义属性)。
DBHelper 类
DBHelper
类提供用于以下方面的函数和属性:
- 连接到 SQL Server:
OpenConnection
、CloseConnection
、IsConnectionOpen
; - 调用元数据视图和存储过程:
GetTables
、GetViews
、GetProcedures
、GetFunctions
、GetTableColumns
、GetRoutineParameters
、GetFunctionColumns
、GetProcedureType
; - 使用 XSL 样式表转换 XML 文档或读取器(可以是磁盘上的文件,也可以是内存中的 XML 文档):
Transform
(有三个重载定义); - 其他实用程序:
SetXslDocumentParameters
(允许使用 XSL 参数自定义 XSL 文档)、CreateFriendlyName
(静态方法,用于生成有效的类名或标识符)。
除了 Transform
和 GetProcedureType
之外,所有方法都易于理解。其中大部分是用于收集元数据的视图和存储过程的简单包装器。
Transform(XmlReader xmlRdr, XmlDocument xslTemplateDoc)
方法使用 .NET Framework 2.0 的新 XslCompiledTransform
类。
public string Transform(XmlReader xmlRdr, XmlDocument xslTemplateDoc)
{
string strXML = string.Empty;
try
{
XmlNamespaceManager nsmgr
= new XmlNamespaceManager(xslTemplateDoc.NameTable);
nsmgr.AddNamespace("xsl", "http://www.w3.org/1999/XSL/Transform");
XmlUrlResolver resolver = new XmlUrlResolver();
XslCompiledTransform trans = new XslCompiledTransform();
trans.Load(xslTemplateDoc, null, resolver);
TextWriter writer = new StringWriter();
XmlTextWriter xmlWriter = new XmlTextWriter(writer);
xmlWriter.Formatting = Formatting.Indented;
trans.Transform(xmlRdr, xmlWriter);
strXML = writer.ToString();
}
catch(Exception ex)
{
strXML = "<errors>";
while(ex != null)
{
strXML += "<error>";
strXML += ex.Message;
strXML += "</error>";
ex = ex.InnerException;
}
strXML += "</errors>";
}
return strXML;
}
GetProcedureType(string procedureName, XmlDocument parametersDoc, string xslExecuteProcTemplatePath, out XmlDocument rowsetMetaDataDoc, out string strErrors)
方法用于区分返回行集的存储过程和返回插入、更新、删除等操作结果的存储过程。由于我没有找到 SQL Server 提供的方法来检索存储过程的行集元数据,因此我使用 SqlCommand
对象的 ExecuteReader
方法,并将参数设置为 CommandBehavior.SchemaOnly
。一个问题是 SQL 字符串,因为我必须为参数提供默认值。这可能是一个问题,因为 NULL 值赋给参数可能会改变存储过程的行为和返回类型。此外,当我使用 SqlMetal 工具生成 DLinq 实体时,我看到对于 T-SQL 角度来说没问题的存储过程,出现了一些架构错误。如果有人有关于如何检索行集信息的有用信息并愿意分享,我将不胜感激。
首先,GetProcedureType
方法生成一个有效的 SQL 语句用于 EXECUTE
存储过程,并为参数提供默认值(parametersDoc
参数是使用 GetRoutineParametersXML
存储过程创建的 XML 文档):数字类型为 -1,位类型为 0,所有其他类型为 NULL
。ExecuteReader
方法的结果是 SqlDataReader
对象。我假设如果此对象的 FieldCount
属性大于 0,则该过程返回一个行集,否则它返回一个值或为空。
SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.SchemaOnly);
if(rdr.FieldCount > 0)
{
...
return ProcedureTypes.Rowset;
}
else
{
...
return ProcedureTypes.ReturnValue;
}
如果该过程返回一个行集,元数据信息将在通过 SqlDataReader
对象的 GetSchemaTable()
方法获得的架构表中找到。通过遍历 DataTable 对象中的列集合,创建包含与
GetFunctionsColumnsXML
收集的结构相似的 XML 文档。
如果发生任何 SQL 错误,返回类型是 UnableToHandleSQL
,如果发生其他未知错误,返回类型是 UnableToHandleOther
,正如 ProcedureTypes
枚举中定义的。
public enum ProcedureTypes
{
UnableToHandleSQL = 0
, UnableToHandleOther = 1
, Rowset = 2
, ReturnValue = 3
}
Add-In Connect 类
Connect
类是用于将插件实例附加到 VS.NET 的主类。帮助我们执行操作的方法称为 Exec
,其定义如下:
public void Exec(string commandName, vsCommandExecOption executeOption,
ref object varIn, ref object varOut, ref bool handled)
此方法实例化一个 frmWizard
类型的对象,并为 WizardFinished
事件附加一个处理程序。
frmWizard frm = new frmWizard();
frm.WizardFinished += new DLinqEntitiesEventHandler(frm_WizardFinished);
DialogResult dr = frm.ShowDialog();
处理程序 frm_WizardFinished
使用向导收集并存储在 DLinqEntitiesEventArgs e
参数中的设置来生成 DLinq 类。它遍历 DBObjects 集合,并根据 ObjectType
属性执行某些操作。要访问 VS.NET 对象,Add-In 应用程序使用类型为 DTE2
的 _applicationObject
对象。
private DTE2 _applicationObject;
如果选择了项目创建,它将使用特定方法以所选语言创建项目,并添加对 System.Data.DLinq
和 System.Query
程序集的引用。为了反映代码生成的当前状态,它使用 VS.NET 状态栏来显示文本和操作进度。
if(e.CreateProject)
{
prjPath = e.Path;
prjTemplatePath = sln.GetProjectTemplate("ClassLibrary.zip",
e.LanguageType.ToString());
sln.AddFromTemplate(prjTemplatePath, prjPath, e.ProjectName, false);
prj = sln.Projects.Item(sln.Projects.Count);
ProjectItem item = sln.FindProjectItem(prjPath + "\\Class1." +
e.FileExtension);
item.Remove();
itemTemplatePath = sln.GetProjectItemTemplate("Class.zip",
e.LanguageType.ToString());
VSProject thisPrj = (VSProject)prj.Object;
thisPrj.References.Add("System.Data.DLinq");
thisPrj.References.Add("System.Query");
_applicationObject.StatusBar.Progress(true, "Creating project " +
e.ProjectName + "." + e.ProjectExtension + "...",
2, 100);
}
对于每个数据库对象,根据设置,应用程序获取数据库对象的结构,执行代码生成,如果需要将其保存在其文件中,则将其添加到 StreamWriter
对象,如果选择了项目创建,则将项添加到之前创建的项目中。例如,对于表和视图,代码是:
xmlDoc = dbH.GetTableColumns(dbObj.ObjectName);
if(e.UseOneOnlyFile)
{
sCode += dbH.Transform(xmlDoc, xslDoc);
}
else
{
sCode = dbH.Transform(xmlDoc, xslDoc);
sw = new StreamWriter(e.Path + @"\" + dbObj.ObjectFriendlyName + "." +
e.FileExtension);
sw.Write(sCode);
sw.Close();
if(e.CreateProject)
{
prj = sln.Projects.Item(sln.Projects.Count);
prj.ProjectItems.AddFromFile(e.Path + @"\" + dbObj.ObjectFriendlyName
+ "." + e.FileExtension);
}
}
在转换完所有选定的数据库对象后,应用程序生成派生自 DataContext
的主类,其中包含表(包括视图)、存储过程和函数的定义。
如果用户请求创建 XML 映射,将在指定位置创建一个包含数据库对象定义信息的 XML 文件。此外,如果用户请求生成的类使用映射,则包含数据库对象定义的自定义属性将不会添加到类中。
XSL 样式表和 DLinq 类
表和视图类、表函数和行集存储过程类使用 table_CSharp.xsl 和 table_VisualBasic.xsl 样式表进行映射。它们包含特定的语言语法和模板,用于生成类定义、构造函数、映射到列的私有成员和公共属性、用于关系的私有成员和公共属性(EntityRef
和 EntitySet
对象),以及反映列值更改的事件。
派生自 DataContext
的主类使用 data_context_CSharp.xsl 和 data_context_VisualBasic.xsl 生成。它是映射到所选数据库的类,具有映射到 Table
对象的构造函数和公共属性,用于返回值的存储过程的 StoredProcedureResult
,用于返回值的存储过程的 int
数据类型,用于标量函数的返回类型,以及用于表函数的 IQueryable
对象。
所有样式表都包含名为 type.xsl 的样式表,用于将 SQL 类型映射到 C# 或 VB.NET 的基本类型,并包含这些语言的默认值。
execute_procedure.xsl 样式表用于生成有效的 T-SQL 语句,用于执行带有参数默认值的存储过程,这有助于区分返回行集的存储过程和返回值的存储过程。
map.xsl 用于获取包含数据库对象元数据定义的 XML 映射文件。
输出与 SqlMetal 工具创建的非常相似,除了 EntityRef
和 EntitySet
属性。类定义如下:
[Table(Name="Customers")]
public partial class Customer
{
private int xCustomerID;
[Column(Storage="xCustomerID", DBType="int NOT NULL IDENTITY",
Id=true, AutoGen=true)]
public CustomerID
{
get
{
return xCustomerID;
}
set
{
if(this.xCustomerID != value)
{
this.OnPropertyChanging("CustomerID");
this.xCustomerID = value;
this.OnPropertyChanged("CusotmerID");
}
}
}
}
EntitySet
类用于父类中,以拥有子实体的“集合”。
[Table(Name="Customers")]
public class Customer
{
[Column(Id=true)]
public string CustomerID;
...
private EntitySet<Order> _Orders;
[Association(Storage="_Orders", OtherKey="CustomerID")]
public EntitySet<Order> Orders
{
get { return this._Orders; }
set { this._Orders.Assign(value); }
}
}
子实体必须具有指向父级的引用,使用 EntityRef
类。
[Table(Name="Orders")]
public class Order
{
[Column]
public string CustomerID;
private EntityRef<Customer> _Customer;
[Association(Storage="_Customer", ThisKey="CustomerID")]
public Customer Customer
{
get { return this._Customer.Entity; }
set { this._Customer.Entity = value; }
}
}
派生自 DataContext
的类(主数据库类)如下所示:
public partial class Northwind : DataContext
{
public Table<Customer> Customers;
public Table<Order> Orders;
public Northwind(string connection): base(connection) {}
[StoredProcedure(Name="CustOrderHist")]
public StoredProcedureResult<CustOrderHistResult>
CustOrderHist([Parameter(Name="CustomerID")] string customerID)
{
return this.ExecuteStoredProcedure<CustOrderHistResult>
(((MethodInfo)(MethodInfo.GetCurrentMethod())), customerID);
}
[StoredProcedure(Name="InsertRegion")]
public int InsertRegion([Parameter(Name="RegionID", DBType="int")]
System.Nullable RegionID,
[Parameter(Name="RegionDescription",
DBType="nchar(50)")] string RegionDescription)
{
StoredProcedureResult result =
this.ExecuteStoredProcedure(((MethodInfo)
(MethodInfo.GetCurrentMethod())), RegionID,
RegionDescription);
return result.ReturnValue.Value;
}
[Function(Name="[dbo].[ProductsCostingMoreThan]")]
public IQueryable<ProductsCostingMoreThanResult>
ProductsCostingMoreThan(System.Nullable<decimal> cost)
{
MethodCallExpression mc = Expression.Call(
((MethodInfo)(MethodInfo.GetCurrentMethod())),
Expression.Constant(this),
new Expression[] {
Expression.Constant(cost, typeof(System.Nullable<decimal>))
}
);
return this.CreateQuery<ProductsCostingMoreThanResult>(mc);
}
[Function(Name="[dbo].[fnGetCustomers]")]
public IQueryable<FnGetCustomersRowset> FnGetCustomers()
{
MethodCallExpression mc = Expression.Call(((MethodInfo)
(MethodInfo.GetCurrentMethod())),
Expression.Constant(this), null);
return this.CreateQuery<FnGetCustomersRowsetRowset>(mc);
}
}
生成的类的简单用法,如 LINQ 文档所示:
NorthwindDataContext db
= new NorthwindDataContext("c:\\northwind\\northwnd.mdf");
var q =
from c in db.Customers
where c.City == "London"
select c;
foreach (var cust in q)
Console.WriteLine("id = {0}, City = {1}",cust.CustomerID, cust.City);
使用应用程序
首先,您必须运行用于选择元信息的表、视图、函数和存储过程的脚本。要映射到 DLinq 实体的数据库必须具有所有这些对象(在 scripts 文件夹中可用)。
为了使 DLinqEntityGenerator 插件应用程序可用于 VS.NET,必须将程序集和 .AddIn 文件复制到“Add-in File Paths”选项指定的目录之一(工具 - > 选项 -> Add-In/宏安全性)。此外,必须选中“Allow Add-in components to load”选项。包含用于生成类的 XSL 样式表的 Templates 文件夹必须复制到选定的目录。如果您在运行 DLinqEntityGenerator Add-in 时遇到困难,请将随 DlinqEntityGenerator.dll 文件提供的其他程序集复制到同一目录。
文件复制完成后,当您启动 VS.NET 时,将在“工具”菜单的开头找到一个名为“DLinqEntityGenerator”的新命令。DLinqEntityGenerator 应用程序现在应该在 VS.NET 环境中可用,作为一个新工具。