GenOmatic






3.80/5 (4投票s)
一个从数据库查询生成枚举的控制台应用程序
引言
本文描述了一个简单的控制台应用程序,可用于根据数据库查询生成枚举的代码。我一直想写类似的东西。在编写过程中,我意识到我可以使其足够灵活,以便用于其他用途,但主要目标是生成小的代码文件。我无意将本文作为枚举的入门介绍,我只会涵盖我认为解释此实用程序动机所必需的内容。
另外,我不使用VB,所以请提醒我VB语句中的任何错误。
背景
此时,您应该知道什么是枚举;本质上,它是一种类型安全的方法,用于将名称与数值关联起来。
C#
enum TransactionType
{
Credit = 0
,
Debit = 1
}
VB
Enum TransactionType
Credit = 0
Debit = 1
End Enum
然后可以在其他语句中使用这些名称
C#
if ( trans.Type == TransactionType.Credit ) ...
switch ( trans.Type )
{
case TransactionType.Credit : ...
case TransactionType.Debit : ...
}
VB
If trans.Type = TransactionType.Credit ...
Select trans.Type
Case TransactionType.Credit ...
Case TransactionType.Debit ...
End Select
与仅使用数值相比,使用枚举可以使代码的含义更清晰(且更易于维护)。
这同样适用于数据库。许多数据库都设置为单字符字段,可能使用值“C”表示贷记,“D”表示借记;其他则使用数字。这些代码可能令人费解。通常,数据库会有一个表来提供参照完整性。这样的表也可以提供这些代码的翻译。
我通常遵循此模式
Table: TransactionType
Code Meaning
---- -------
0 Credit
1 Debit
拥有一个与此类数据库表匹配的枚举是很常见的;但是,这两组不同的信息需要同步。另一方面,这些数据更改的频率不高,并且应用程序可能需要更新才能处理任何新增的值,因此同步只需要在应用程序编译时进行。
我长期以来一直在思考,特别是当有人提出需要对此情况进行自动同步解决方案的问题时,一个小型控制台应用程序可以查询数据库并生成枚举的代码。在此,我将展示我所构思的内容……
设计要求
我决定该实用程序需要以下功能:
- 该实用程序必须从用户创建的文件(而不是命令行参数)获取指令。
- 使用SQL获取数据;允许用户提供查询。
- 不与任何特定数据库系统绑定;允许用户指定。
- 根据要求生成C#或VB.NET代码;如果可能,支持其他语言。
- 支持枚举及其成员的属性和XML文档注释。
- 最好允许用户指定枚举的所有内容。
- 名称
- 命名空间
- 访问修饰符
- 封闭类
- 底层类型
- 数字格式
在进行此工作时,我决定需要一个用于数据库访问的插件系统;这篇文章是结果。我还发现需要一种方法来允许用户指定更复杂的格式值;这篇文章是结果。这两篇文章的代码都包含在此处的zip文件中。
我选择使用XML作为系统的基础。本文也不会解释XML;这里是一个很好的资源。
我考虑过使用插件系统来提供代码生成,但很快意识到XSLT将提供所有这些以及更多。而且,如果您认为我要解释XSLT,那您就疯了;请这里代替。
定义文件
要生成的文件(除了数据本身)的所有内容都包含在一个XML文档中。(XSLT*可能*包含在定义文件中,但通常会在单独的文件中,就像示例中一样。)
zip文件包含以下EnumSample.cs.xml文件(行号是为了方便讨论而添加的):
1 <?xml-stylesheet type="text/xsl" href="GenOmatic.enum.xsl#cs"?>
2 <GenOmatic>
3 <Datasource>
4 <Connector>PIEBALD.Data.OleDbDatabaseConnector.dll</Connector>
5 <ConnectionString>Provider=Microsoft.Jet.OLEDB.4.0;Data Source=Demo.mdb
</ConnectionString>
6 <Query>SELECT Meaning AS Name, Code AS [Value], Summary,
Description FROM EnumSample ORDER BY Code</Query>
7 <Format Column="Value" Format="'0x'X4" />
8 </Datasource>
9 <Result>
10 <Namespace>PIEBALD</Namespace>
11 <Class>EnumSampleTest</Class>
12 <Summary>An example of a C# enum produced by GenOmatic</Summary>
13 <Attribute>System.FlagsAttribute()</Attribute>
14 <AccessModifier>public</AccessModifier>
15 <Name>EnumSample</Name>
16 <Type>int</Type>
17 </Result>
18 <File>EnumSample.cs</File>
19 </GenOmatic>
第1行:文件应指定用于生成输出的样式表(XSLT)。*GenOmatic.enum.xsl*文件包含在zip文件中,它包含用于生成C#和VB枚举的样式表。如果您不指定样式表,或者找不到它,则不会执行任何转换。
第2至19行:根(文档)元素必须命名为GenOmatic
。
第3至8行:文件必须包含一个Datasource
元素,其中包含以下子元素(顺序不限):
第4行:Connector
元素指定要使用的数据库连接器插件。
第5行:ConnectionString
元素指定要使用的连接字符串。如果您担心纯文本密码,建议您指定一个具有只读访问权限的用户,以仅读取所需内容。此外,这仅应在开发系统上使用,不应部署到客户站点。
第6行:Query
元素显然包含要执行的查询,以获取枚举的数据。如果您阅读过我的数据库连接器文章,您就会知道实际上可以指定一个以分号分隔的语句列表。但是,GenOmatic只会使用第一个语句的结果。
第7行:您可以指定用于检索数据的格式;这通常是针对枚举值。如果您愿意这样做,请添加一个Format
元素,该元素指定列名和要使用的格式;列中的数据必须是IFormattable
且非空。有关更多信息,请参阅我的ApplyFormat文章。
更新:由于对ApplyFormat
方法的增强,Format
元素现在可以通过以下任何方式指定格式:
-
<Format Column="Value" Format="'0x'X4" />
(我现在认为这已弃用。)
-
<Format Column="Value">'0x'X4</Format>
(我一开始就应该这样做。)
-
<Format Column="Value"> <Text>0x</Text> <Formatter>X4</Formatter> </Format>
第9至17行:可以存在一个Result
元素;如果不存在,GenOmatic
会创建一个;GenOmatic
不会直接使用其内容。GenOmatic
将创建一个Columns
元素和一个Rows
元素在Result
元素中,并用查询结果填充它们。(任何预先存在的Columns
和Rows
元素都将被替换。任何其他内容都将保持不变。)当GenOmatic
完成时,您可以查看结果以确保您获得了所需的数据。Result
元素的内容旨在由XSLT处理;我将在稍后讨论。
第18行:您可以指定一个文件接收生成代码的名称。如果您不指定文件,则生成的代码将发送到控制台;您可以将其重定向或管道传输到其他地方。如果您*确实*指定了一个文件,并且该文件存在,那么文件内容和新生成的代码将被比较,并且只有在新代码与原始代码不同时才会写入文件;这是为了在数据未更改时减少不必要的重新生成。如果文件不存在,新生成的代码将被写入该文件。
注意:比较只是一个简单的string
比较;它似乎不怎么优雅,但我认为使用哈希或CRC会更低效。如果您考虑生成非常大的文件,请记住这一点。
zip文件还包含EnumSample.vb.xml,它生成相同的枚举,但语法为VB.NET。它仅在以下几行中有所不同:
1 <?xml-stylesheet type="text/xsl" href="GenOmatic.enum.xsl#vb"?>
7 <Format Column="Value" Format="'&H'X4" />
16 <Type>Integer</Type>
18 <File>EnumSample.vb</File>
样式表
GenOmatic.enum.xsl
GenOmatic.enum.xsl中提供的样式表会将Result
元素的内容转换为C#或VB.NET枚举。
第10行:您必须为枚举指定一个命名空间。
第11行:您可以指定一个类来容纳枚举。如果这样做,它将被标记为partial
,以便它可以成为存在于其他文件中的类的一部分。
第12行:您可以指定一个摘要,它将成为枚举的XML文档注释。
第13行:您可以指定任意数量的属性以附加到枚举。
第14行:您可以为枚举指定一个访问修饰符;通常,除非您希望它对类是私有的,否则您不会这样做。
第15行:您必须为枚举提供一个名称。
第16行:您可以为枚举提供一个底层类型。
枚举成员的信息来自查询结果;Columns
和Rows
。提供的样式表需要Name
和Value
字段,并且还支持Summary
和Description
字段(如果提供);查询需要考虑这些名称。
我建议您构建GenOmatic
并在提供的XML文件上运行它,以亲自查看结果。
您可以修改XSLT以支持您可能需要的其他枚举内容;如果您认为其他人可能想知道,请发帖。
GenOmatic.csv.xsl和CsvSample.xml
我已将这些文件包含在zip文件中,作为GenOmatic
其他用途的示例。如果您只需要执行一个简单的查询并生成一个小的CSV文件,那么这就可以做到。但是,如果查询返回*大量*数据,您可能需要寻求其他技术。
实现
GenOmatic
是一个相当简单的控制台应用程序,它读取上面描述的定义文件,连接到指定的数据库,执行查询,使用指定的样式表转换结果,并更新定义文件。
Main
GenOmatic.cs中Main
方法的核心是:
foreach ( string filename in args )
{
System.Xml.XmlDocument doc = PIEBALD.Lib.LibXml.LoadXmlDocument ( filename ) ;
Process ( doc ) ;
PIEBALD.Lib.LibXml.WriteXmlDocument ( doc , filename ) ;
Transform ( doc ) ;
}
进程
Process
方法完成了连接数据库、执行查询和更新文档结果的所有工作。我不会展示整个方法,但这是用于用数据填充Rows
元素的foreach
循环。
top
、mid
和bot
是用于保存XmlElement
的变量;att
是用于保存XmlAttribute
的变量;也许这张图有助于您可视化变量与XML结构的关系。
top --> <Rows>
mid ----> <Row>
bot ------> <Value Null="false"> ...
att ---------------^
此代码的作用是追加一个新的Row
元素来保存当前数据行,然后遍历DataTable
的列,为每个字段的值追加一个元素。一个名为Null
的属性用于指示值是否为null
。一个Dictionary
将被填充有任何Format
元素的内容,因此如果字段的值是IFormattable
且其列具有Format
,则该格式将应用于该值。
foreach ( System.Data.DataRow dr in dt [ 0 ].Rows )
{
top.AppendChild ( mid = Doc.CreateElement ( "Row" ) ) ;
for ( int col = 0 ; col < dt [ 0 ].Columns.Count ; col++ )
{
mid.AppendChild ( bot = Doc.CreateElement (
dt [ 0 ].Columns [ col ].ColumnName ) ) ;
bot.Attributes.Append ( att = Doc.CreateAttribute ( "Null" ) ) ;
if ( dr [ col ] == System.DBNull.Value )
{
att.Value = "true" ;
}
else
{
att.Value = "false" ;
if
(
( dr [ col ] is System.IFormattable )
&&
formats.ContainsKey ( dt [ 0 ].Columns [ col ].ColumnName )
)
{
bot.InnerText = ((System.IFormattable) dr [ col ]).ApplyFormat
( formats [ dt [ 0 ].Columns [ col ].ColumnName ] ) ;
}
else
{
bot.InnerText = dr [ col ].ToString() ;
}
}
}
}
变换
Transform
方法检索样式表(如果存在)并执行转换。转换结果存储在string
中,而不是直接写入文件。之后,将确定是否将结果保存到文件。
System.Xml.XmlElement stylesheet = PIEBALD.Lib.LibXsl.GetStylesheet ( Doc ) ;
if ( stylesheet == null )
{
System.Console.WriteLine ( "No stylesheet specified." ) ;
}
else
{
string newver =
PIEBALD.Lib.LibXsl.Transform ( Doc , stylesheet ).ToString() ;
System.Xml.XmlElement temp =
(System.Xml.XmlElement) Doc.SelectSingleNode ( "GenOmatic/File" ) ;
if ( ( temp == null ) || ( temp.InnerText.Length == 0 ) )
{
System.Console.Write ( newver ) ;
}
else
{
System.IO.FileInfo fi = new System.IO.FileInfo ( temp.InnerText ) ;
string oldver = null ;
if ( fi.Exists )
{
using
(
System.IO.StreamReader sr
=
new System.IO.StreamReader ( fi.FullName )
)
{
oldver = sr.ReadToEnd() ;
sr.Close() ;
}
if ( ( oldver.Length == newver.Length ) && ( oldver == newver ) )
{
System.Console.WriteLine ( "The file is unchanged." ) ;
}
else
{
oldver = null ;
}
}
if ( oldver == null )
{
using
(
System.IO.TextWriter sw
=
new System.IO.StreamWriter ( fi.FullName )
)
{
sw.Write ( newver ) ;
sw.Close() ;
}
}
}
}
奖励功能
上述代码使用了一些库例程;在此,我将记录更重要的例程。
LoadXmlDocument
此方法实际上非常简单;它创建一个具有我偏好设置的XmlReader
,实例化一个XmlDocument
,然后加载文档。有其他重载可以接受其他源。
public static System.Xml.XmlDocument
LoadXmlDocument
(
string Source
)
{
using
(
System.Xml.XmlReader reader =
System.Xml.XmlReader.Create
(
System.Environment.ExpandEnvironmentVariables ( Source )
,
XmlReaderSettings.Settings
)
)
{
System.Xml.XmlDocument result = new System.Xml.XmlDocument() ;
result.Load ( reader ) ;
reader.Close() ;
return ( result ) ;
}
}
private static class XmlReaderSettings
{
public static readonly System.Xml.XmlReaderSettings Settings ;
static XmlReaderSettings
(
)
{
Settings = new System.Xml.XmlReaderSettings() ;
System.Xml.XmlUrlResolver resolver = new System.Xml.XmlUrlResolver() ;
resolver.Credentials =
System.Net.CredentialCache.DefaultNetworkCredentials ;
Settings.XmlResolver = resolver ;
Settings.ValidationType = System.Xml.ValidationType.Schema ;
Settings.ProhibitDtd = false ;
return ;
}
}
GetStylesheet
此方法稍微复杂一些;它首先尝试在提供的XmlDocument
中查找样式表ProcessingInstruction
;一旦找到,它会尝试使用正则表达式解析其值。如果样式表在另一个文件中,则会从该文件中加载一个XmlDocument
;否则,假定样式表在当前XmlDocument
中。然后,如果请求了具有特定ID的样式表,则会在XmlDocument
中搜索具有该ID的样式表。如果未请求样式表或找不到请求的样式表,则此方法将返回null
。
private static readonly System.Text.RegularExpressions.Regex HrefReg =
new System.Text.RegularExpressions.Regex
(
"href\\s*=\\s*(('(?'uri'[^'#]*)?(#(?'id'[^']*))?')" +
"|(\"(?'uri'[^\"#]*)?(#(?'id'[^\"]*))?\"))"
) ;
public static System.Xml.XmlElement GetStylesheet
(
System.Xml.XmlDocument Doc
)
{
System.Xml.XmlElement result = null ;
if ( Doc == null )
{
throw ( new System.ArgumentNullException
(
"Doc"
,
"You must provide a document"
) ) ;
}
if ( Doc.DocumentElement == null )
{
throw ( new System.ArgumentNullException
(
"Doc.DocumentElement"
,
"You must provide a document"
) ) ;
}
int nod = 0 ;
while ( nod < Doc.ChildNodes.Count )
{
if
(
( Doc.ChildNodes [ nod ] is System.Xml.XmlProcessingInstruction )
&&
( Doc.ChildNodes [ nod ].Name == "xml-stylesheet" )
)
{
System.Text.RegularExpressions.MatchCollection mat ;
if ( ( mat = HrefReg.Matches ( Doc.ChildNodes [ nod ].Value ) ).Count == 1 )
{
System.Xml.XmlDocument sheet ;
if ( mat [ 0 ].Groups [ "uri" ].Value == "" )
{
sheet = Doc ;
}
else
{
sheet = PIEBALD.Lib.LibXml.LoadXmlDocument
(
mat [ 0 ].Groups [ "uri" ].Value
) ;
}
if ( mat [ 0 ].Groups [ "id" ].Value != "" )
{
System.Xml.XmlNodeList sheets =
sheet.GetElementsByTagName ( "xsl:stylesheet" ) ;
int ele = 0 ;
while ( ele < sheets.Count )
{
if
(
( sheets [ ele ].Attributes [ "id" ] != null )
&&
( sheets [ ele ].Attributes [ "id" ].Value ==
mat [ 0 ].Groups [ "id" ].Value )
)
{
result = (System.Xml.XmlElement) sheets [ ele ] ;
break ;
}
ele++ ;
}
}
else
{
if ( sheet.DocumentElement.Name == "xsl:stylesheet" )
{
result = sheet.DocumentElement ;
}
}
}
break ;
}
nod++ ;
}
return ( result ) ;
}
变换
此方法使用提供的样式表对提供的XmlDocument
执行XSLT转换。样式表必须作为XmlElement
传递,因为它可能只是一个较大的XmlDocument
中的一个元素,甚至可能是要被转换的XmlDocument
本身。有其他重载可以接受其他源。
public static System.Text.StringBuilder
Transform
(
System.Xml.XmlDocument Doc
,
System.Xml.XmlElement Stylesheet
)
{
if ( Doc == null )
{
throw ( new System.ArgumentNullException ( "Doc" ,
"You must provide a document" ) ) ;
}
if ( Stylesheet == null )
{
throw ( new System.ArgumentNullException ( "Stylesheet" ,
"You must provide a stylesheet" ) ) ;
}
System.Xml.Xsl.XslCompiledTransform xslt =
new System.Xml.Xsl.XslCompiledTransform() ;
xslt.Load ( Stylesheet ) ;
System.Text.StringBuilder result = new System.Text.StringBuilder() ;
xslt.Transform
(
Doc
,
null
,
new System.IO.StringWriter ( result )
) ;
return ( result ) ;
}
Using the Code
与我提到的其他文章一样,我使用build.bat来构建和测试此代码。我添加了以下行:
@rem Build GenOmatic
csc GenOmatic.cs Lib*.cs /r:DatabaseConnector.dll
@rem Test the GenOmatic
del EnumSample.cs
del EnumSample.vb
del CsvSample.csv
GenOmatic EnumSample.cs.xml EnumSample.vb.xml CsvSample.xml
type EnumSample.cs
type EnumSample.vb
type CsvSample.csv
在Visual Studio中使用该实用程序
我设计此实用程序的初衷是将其用作Visual Studio项目的预生成事件。打开项目的属性页,选择“生成事件”选项卡,有一个用于预生成命令行的框。其中一个缺点是(默认情况下)预生成事件在项目*release\bin*或*debug\bin*子目录中作为工作目录运行;您需要考虑到这一点。以下命令是一个示例,说明如何在预生成事件中引用项目目录中的*GenOmatic*文件:
GenOmatic ..\..\MyEnum.xml
这假设GenOmatic可以通过Path(环境变量)访问,但可能我是Windows上最后一个使用它的人。如果您不将GenOmatic放在Path列出的目录中,则可以完全指定其位置:
"C:\Program files\Utilities\GenOmatic" ..\..\MyEnum.xml
(或您放置它的任何位置。)
一种更智能、更面向Windows的方法是注册文件类型。您需要选择一个扩展名(可能是“GenOmatic”,它只需要是唯一的)用于XML文件。在Windows资源管理器 | 工具 | 文件夹选项 | 文件类型:单击新建,输入您选择的扩展名,单击确定,单击更改…,单击“从列表中选择程序”,单击确定,单击浏览…,找到并选择GenOmatic.exe,单击确定,单击关闭。
现在,预生成事件可以简化为:
..\..\MyEnum.GenOmatic
但是等等!还有更多!这只会告诉系统GenOmatic和XML文件在哪里。上面提供的示例假设所有文件都在一个目录中,这在实际使用中可能不会如此。您需要告诉GenOmatic在哪里可以找到数据库访问器插件和样式表,以及在哪里放置输出文件。所有这些都在XML文件中处理。
<?xml-stylesheet type="text/xsl"
href="C:\Program files\Utilities\GenOmatic.enum.xsl#cs"?>
<Connector>C:\Program files\Utilities\PIEBALD.Data.OleDbDatabaseConnector.dll
</Connector>
<File>..\..\EnumSample.cs</File>
(或您想要的任何位置。)
如果您觉得更方便,也可以使用环境变量。
<?xml-stylesheet type="text/xsl" href="%Programfiles%\Utilities\GenOmatic.enum.xsl#cs"?>
<Connector>%Programfiles%\Utilities\PIEBALD.Data.OleDbDatabaseConnector.dll
</Connector>
历史
- 2009-02-15 - 首次提交
- 2009-03-06 - 添加了
ApplyFormat
的新功能