65.9K
CodeProject 正在变化。 阅读更多。
Home

GenOmatic

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.80/5 (4投票s)

2009年2月17日

CPOL

11分钟阅读

viewsIcon

23782

downloadIcon

84

一个从数据库查询生成枚举的控制台应用程序

引言

本文描述了一个简单的控制台应用程序,可用于根据数据库查询生成枚举的代码。我一直想写类似的东西。在编写过程中,我意识到我可以使其足够灵活,以便用于其他用途,但主要目标是生成小的代码文件。我无意将本文作为枚举的入门介绍,我只会涵盖我认为解释此实用程序动机所必需的内容。

另外,我不使用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元素中,并用查询结果填充它们。(任何预先存在的ColumnsRows元素都将被替换。任何其他内容都将保持不变。)当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行:您可以为枚举提供一个底层类型。

枚举成员的信息来自查询结果;ColumnsRows。提供的样式表需要NameValue字段,并且还支持SummaryDescription字段(如果提供);查询需要考虑这些名称。

我建议您构建GenOmatic并在提供的XML文件上运行它,以亲自查看结果。

您可以修改XSLT以支持您可能需要的其他枚举内容;如果您认为其他人可能想知道,请发帖。

GenOmatic.csv.xsl和CsvSample.xml

我已将这些文件包含在zip文件中,作为GenOmatic其他用途的示例。如果您只需要执行一个简单的查询并生成一个小的CSV文件,那么这就可以做到。但是,如果查询返回*大量*数据,您可能需要寻求其他技术。

实现

GenOmatic是一个相当简单的控制台应用程序,它读取上面描述的定义文件,连接到指定的数据库,执行查询,使用指定的样式表转换结果,并更新定义文件。

Main

GenOmatic.csMain方法的核心是:

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循环。

topmidbot是用于保存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的新功能
© . All rights reserved.