自动从 SQL Server 数据表中生成类和枚举






4.60/5 (9投票s)
在 ASP.NET 中使用 BuildProvider 为消息、默认值等 SQL Server 存储库启用智能感知。
引言
对于许多应用程序,SQL Server 数据库不仅仅存储业务数据。很有可能 sys.messages 表存储了应用程序的自定义消息,而另一个表可能用于应用程序范围内的默认值。这些消息、默认值等的存储库有助于开发人员维护一致性这一重要属性。在开发 ASP.NET 应用程序时,我们需要定期引用这些表来确定数据库中特定消息的消息 ID,或者默认值的确切名称或值。我的经验是,这可能导致捷径和不一致。需要的是一种无忧无虑的方法来为我的消息生成类和枚举。
我需要的是我的应用程序的所有消息都存储在 sys.messages 中,而我的所有应用程序默认值都存储在我的 PortDefaults 表中。在 SQL Server 中,其他想要直接查询数据库的开发人员,以及存储过程和用户定义函数(ASP.NET 应用程序访问数据库的唯一方式)都可以访问它们。在编写应用程序的 VB.NET 代码时,我需要智能感知为我提供可用消息和默认值的列表供我选择,这样我就不需要不断地回查我的 SQL 表。最重要的是,我希望所有这些都毫无麻烦且无需维护。如果另一位开发人员添加了一组新消息,我希望它们对我可用,而无需更改其他表或代码。如果我向数据库添加了一个新默认值,我希望它出现在我的智能感知提示中。
出色的 BuildProvider
类结合 CodeDom 使得这些目标可以轻松实现,并得到了两篇优秀文章的大力帮助
开发工具
本文中的代码是在 Visual Studio 2005 中使用 VB.NET 2005 开发的。
背景
我正在处理的应用程序有数百条相关消息,我意识到每次在智能感知下拉列表中显示整个列表会太多,因此我决定我将使用的表将包含三列
- ID 列:消息的唯一整数,这将是我的枚举字段值的整数值。
- 文本列:消息文本,它将是枚举字段名。当然,为了使名称有效,我必须去除任何标点符号。
- 组列:消息所属的组名,这将是枚举的名称。幸运的是,像许多人一样,我在 sys.messages 中有现成的消息组,不同的数字范围代表不同类型的消息,例如,50001-59999 表示信息,70000-79999 表示错误等。
sys.messages 没有“group”列,所以我创建了一个视图来提供一个
SELECT message_id,
CASE
WHEN message_id < 60000 THEN 'Information'
WHEN message_id < 70000 THEN 'Warning'
WHEN message_id < 80000 THEN 'Error'
END AS [group],
text FROM sys.messages
WHERE (message_id > 50000)
Using the Code
我们的任务有三个截然不同的部分。第一个是确定我们的数据表在哪里以及我们对哪些列感兴趣。第二个是根据我们数据表的内容生成代码。第三个是让 Visual Studio 在我们开发代码时自动创建代码。我们要创建的代码将类似于
Namespace repository
Class SqlMessage
Enum Information
The_task_has_completed_successfully = 50001
Your_password_was_changed = 50002
End Enum
Enum Warning
Stock_of_this_item_is_now_low = 60001
This_supplier_will_not_deliver_at_weekends = 60002
Your_password_will_expire_in_PARM_days
End Enum
Enum Error
No_items_were_found = 70001
This_account_has_not_been_authorised = 70002
Your_password_has_expired = 70003
End Enum
End Class
End Namespace
我们首先创建一个 XML 文件来保存有关我们的 SQL 连接、数据表和列的信息,以及一些关于我们想要创建的内容的细节。我们将该文件扩展名为 .repos。任何**未使用的**扩展名都可以,但该扩展名稍后将很重要。该文件的名称并不重要。我们的 XML 文件将类似于以下内容
<?xml version="1.0" encoding="utf-8" ?>
<repositorys namespace="repository">
<repository
connectionString= "SERVER=.\SQLEXPRESS;
DATABASE=portsys;Integrated Security=SSPI"
tableName="PortMessagesView"
numberColumnName="message_id"
groupColumnName="group"
textColumnName="text"
className="SqlMessage" />
<repository
connectionString= "SERVER=.\SQLEXPRESS;
DATABASE=portsys;Integrated Security=SSPI"
tableName="PortDefaultsView"
numberColumnName="uid"
groupColumnName="group"
textColumnName="name"
className="PortDefaults" />
</repositorys>
<repositorys>
具有 namespace
属性,该属性指定我们创建的代码将所在的命名空间。
我在这里显示了两个 <repository>
来演示可以在同一文件中进行多个存储库条目。<repository>
的属性是
connectionString
- 用于连接到数据库。tableName
- 指定我们要获取数据的表(或者在本例中是视图)。numberColumnName
- 指定表中包含唯一整数标识符的列。groupColumnName
- 指定表中包含消息所属组的列。textColumnName
- 指定包含消息文本的列。className
- 指定我们创建的代码中类的名称。
您必须为每个存储库指定**所有**属性。
现在我们知道了足够多的信息,可以进行第二项任务:生成代码。如果您不熟悉 CodeDom,这篇文章并不是学习它的好地方,但希望足以启发您进一步研究。我们将遍历 XML 文件,创建 CodeCompileUnit
并在此过程中添加我们的命名空间。
'get the xml input file
Try
Dim filename As String = MyBase.VirtualPath
Dim xmlStream As Stream = VirtualPathProvider.OpenFile(MyBase.VirtualPath)
xmlFile.Load(xmlStream)
Catch ex As XPath.XPathException
System.Console.WriteLine("XML Exception:" & ex.Message)
Catch ex As Exception
System.Console.WriteLine("Exception:" & ex.Message)
End Try
'and create our navigator
navigator = xmlFile.CreateNavigator
'now on to the business of creating the code
'somewhere to put our code
Dim createdCode As New CodeCompileUnit
'create the namespace
Dim createdNamespace As New CodeNamespace
'and find its name and name it
Dim ns As String = ""
iterator = navigator.Select("/repositorys")
iterator.MoveNext()
ns = iterator.Current.GetAttribute("namespace", "")
If ns = "" Then
ns = "DefaultRepository"
System.Console.WriteLine("No namespace found - using default")
End If
createdNamespace.Name = ns
createdCode.Namespaces.Add(createdNamespace)
'add commentary
Dim comment As New CodeCommentStatement("This code has " & _
"been generated by the message repository tool")
createdNamespace.Comments.Add(comment)
'now we iterate through the individual repository(s) pulling
'of the attributes we need to access the data
'so that we can enumerate the datarows
iterator = navigator.Select("/repositorys/repository")
Do While iterator.MoveNext
Dim cs As String = iterator.Current.GetAttribute("connectionString", "")
If cs = "" Then
System.Console.WriteLine("connectionString not specified " & _
"for repository " & iterator.Current.Name)
Exit Sub
End If
'... and so on for our other attributes (tn(tablename),
'nc(numberColumn), gc(groupColumn) tc(textColumn) and cn(className) ...
现在我们知道了所有属性,因此我们可以继续使用 CodeTypeDeclaration
来填充命名空间并创建一个类。然后,使用 CodeTypeDeclaration
(将 isEnum
设置为 True
)填充一个或多个枚举(取决于有多少组)。每个枚举都使用 CodeMemberField
来创建字段,并使用 CodePrimitiveExpression
来设置其值。字段名必须仅包含字母和下划线,因此一个快速的 filterName
函数将清理文本以供使用。
Private Function filterName(ByVal source As String) As String
Dim filtered As String = ""
For Each letter As Char In source.ToCharArray
If Not Char.IsLetter(letter) Then
If letter = "%"c Then
filtered &= "PARM"
Else
letter = "_"c
filtered &= letter
End If
Else
filtered &= letter
End If
Next
Return filtered
End Function
filtered
函数在百分号的位置返回 PARM,只是为了强调消息需要一个参数。不完美,因为它不处理转义的百分号,但足够我们使用。
'create our top level class with the classname
Dim messageClass As CodeTypeDeclaration = New CodeTypeDeclaration(cn)
messageClass.Name = cn
createdNamespace.Types.Add(messageClass) 'class is the default type
'now access the data
'get the data we need
Dim allDa As SqlDataAdapter = New SqlDataAdapter("select * from " & tn, cs)
Dim allDs As DataSet = New DataSet
allDa.Fill(allDs)
'and and a list of the distinct groups in the table which will become enums
Dim groupsDa As SqlDataAdapter = _
New SqlDataAdapter("select distinct [" & _
gc & "] from " & tn, cs)
Dim groupsDs As DataSet = New DataSet
groupsDa.Fill(groupsDs)
For Each group As DataRow In groupsDs.Tables(0).Rows 'zero is the only table
Dim currentGroup As String = group.Item(0) ' there is only column zero
'now create an enum for this group
Dim createEnum As CodeTypeDeclaration = New CodeTypeDeclaration(currentGroup)
createEnum.IsEnum = True 'need to specify enum for this type
'and add it to our message class
messageClass.Members.Add(createEnum)
'now fill it with declarations
For Each datarow As DataRow In allDs.Tables(0).Select(_
"[" & gc & "]='" & _
currentGroup & "'")
'our field name is derived from the text,
'replacing punctuation with underscores using filterName function
Dim fieldName As String = filterName(datarow.Item(tc).ToString)
'and our value is the value form the numbercolumn
Dim fieldValue As Integer = CInt(datarow.Item(nc))
'create the field
Dim field As CodeMemberField = New CodeMemberField
field.Name = fieldName
field.InitExpression = New CodePrimitiveExpression(fieldValue)
'add to the current group enumeration
createEnum.Members.Add(field)
Next
Next
现在我们在 CodeCompileUnit
中拥有了一切。当然,我们还没有对它做任何事情。我们的下一个任务是让 CodeCompileUnit
中的代码能够被我们的应用程序使用。为此,我们使用 ASP.NET 可用的 BuildProvider 功能。如果您以前从未遇到过 BuildProvider,请注意 - 这真的**和看起来一样简单**!
首先,我们需要在 web.config 文件中告知 ASP.NET 关于我们的提供程序。我在我的 App_Code 文件夹中创建了一个名为 CustomBuilders 的文件夹,我将把生成器放在这里。我们在 <codeSubDirectories>
中指定这一点。我的 BuildProvider 的命名空间和类将是 CustomBuilders.ReposBuilder
。我们在 <buildProviders>
中 <add>
的 type
属性中指定这一点。您会记得,之前我们创建了带有 .repos 扩展名的输入 XML 文件。这在 <add>
的 extension
属性中指定。web.config 文件中的条目将类似于下面的示例
<system.web>
<!-- Set compilation debug="true" to insert debugging symbols
into the compiled page.
Because this affects performance, set this value
to true only during development. -->
<compilation debug="true">
<codeSubDirectories>
<add directoryName="CustomBuilders"/>
</codeSubDirectories>
<assemblies>
<add assembly="System.Design, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A"/>
<add assembly="System.Windows.Forms, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<add assembly="VSLangProj, Version=7.0.3300.0,
Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A"/>
</assemblies>
<buildProviders>
<add extension=".repos" type="CustomBuilders.ReposBuilder"/>
</buildProviders>
</compilation>
...
</system.web>
.repos 的 extension
属性(或您之前为 XML 输入文件选择的任何扩展名)是 BuildProvider 的绝妙之处。现在,每次您将带有 .repos(或其他)扩展名的文件放入 App_Code 文件夹时,BuildProvider 都会被触发以生成您指定的代码。您将看不到代码(就像在 ASP.NET 2.0 中看不到很多代码一样),但它就在那里,而且就像魔法一样,您新生成的命名空间和类将可供您使用。
所以(终于!),是时候将这些内容整合起来,创建我们的自定义生成器命名空间(CustomBuilders
),其中包含我们的生成器(ReposBuilder
)。我们继承 BuildProvider 类,并为 GenerateCode
方法提供一个重写,其中将包含我们的代码生成代码以及几行用于输出代码的代码。
Imports Microsoft.VisualBasic
Imports System
Imports System.IO
Imports System.Text
Imports System.Web
Imports System.Web.UI
Imports System.Web.Hosting
Imports System.Web.Compilation
Imports System.CodeDom
Imports System.Xml
Imports System.Data
Imports System.Data.SqlClient
Namespace CustomBuilders
<BuildProviderAppliesTo(BuildProviderAppliesTo.Code)> _
Public Class ReposBuilder
Inherits BuildProvider
Private xmlFile As New XmlDocument
Private navigator As XPath.XPathNavigator
Private iterator As XPath.XPathNodeIterator
Public Overrides Sub GenerateCode(ByVal assemblyBuilder _
As System.Web.Compilation.AssemblyBuilder)
MyBase.GenerateCode(assemblyBuilder)
'...
'in here, our code for reading our attributes and creating our CodeComplieUnit
'...
If Not (createdCode Is Nothing) Then
assemblyBuilder.AddCodeCompileUnit(Me, createdCode)
End If
End Sub
End Class
End Namespace
此 VB 文件需要放在我们之前创建的 App_Code/CustomBuilders 文件夹中。无需编译 - 除了此代码,其他任何东西都不需要,只需要 web.config 条目和 App_Code 文件夹中带有 .repos 扩展名的 XML 输入文件。
那么我们得到什么?
当您将 .repos 文件添加到 App_Code 文件夹时,ASP.NET 将为您处理代码创建。如果您查看页面的 VB 代码并添加一个导入,您将看到(在本例中)he **{}repository** 出现在列表中。导入它之后,您可以使用一个简单的语句,如
dim t as integer = message.error.No_items_were_found
当您在 message 之后输入点时,智能感知将提供 Error|Information|Warning,当您在 Error 之后输入点时,智能感知下拉列表将为您提供所有错误消息。变量 t
将被分配您消息表中的消息编号。设计器甚至很贴心地将它们全部按字母顺序列出!
关注点
如果您像我一样,倾向于回避 ASP.NET 中一些不太明显的特性,因为您没有时间掌握这些技能,或者觉得付出的努力不值得,那么请重新考虑 BuildProvider。它确实非常简单易用,即使是这样一个简单的应用程序也能在很短的时间内获得收益,更不用说提高一致性和减少维护工作了。