此分步主题演示了用于创建 SQL 代码分析规则的各个步骤。 此演练中创建的规则用于避免在存储过程、触发器和函数中出现 WAITFOR DELAY 语句。
在此演练中,将使用以下过程创建 Transact-SQL 静态代码分析的自定义规则:
- 创建类库、对项目启用签名和添加必要的引用。 
- 创建两个帮助器 C# 类。 
- 创建 C# 自定义规则类。 
- 创建用于注册程序集的 XML 文件。 
- 将创建所得的 DLL 和 XML 文件复制到 Extensions 目录中以将其注册。 
- 确认新的代码分析规则已就绪。 
系统必备
必须安装 Visual Studio 高级专业版 或 Visual Studio 旗舰版 才能完成此演练。
创建 SQL 的自定义代码分析规则
首先,将创建一个类库。
创建类库
- 在**“文件”菜单上,单击“新建”,然后单击“项目”**。 
- 在**“添加新项目”对话框的“已安装的模板”列表中,单击“Visual C#”**。 
- 在细节窗格中,选择**“类库”**。 
- 在**“名称”文本框中,键入 SampleRules,然后单击“确定”**。 
接下来,将为项目签名。
对项目启用签名
- 在**“解决方案资源管理器”选中 SampleRules 项目节点的情况下,从“项目”菜单中,单击“属性”(或在“解决方案资源管理器”中右击该项目节点,然后单击“属性”**)。 
- 单击**“签名”**选项卡。 
- 选中**“为程序集签名”**复选框。 
- 指定一个新密钥文件。 在**“选择强名称密钥文件”下拉列表中,选择“<新建...>”**。 - 将显示**“创建强名称密钥”**对话框。 有关更多信息,请参见“创建强名称密钥”对话框。 
- 在**“创建强名称密钥”对话框的“名称”**文本框中,键入 SampleRulesKey 作为新密钥文件的名称。 在此演练中无须提供密码。 有关更多信息,请参见管理程序集签名和清单签名。 
接下来,要向项目添加必要的引用。
向项目添加适当的引用
- 在**“解决方案资源管理器”**中,选择 SampleRules 项目。 
- 在**“项目”菜单上,单击“添加引用”**。 - 将打开**“添加引用”**对话框。 有关更多信息,请参见如何:在 Visual Studio 中添加或移除引用。 
- 选择**“.NET”**选项卡。 
- 在**“组件名称”**列中,找到以下组件: - 提示 - 若要选择多个组件,请在按住 Ctrl 键的同时单击各个组件。 
- 选择了所有所需的组件后,单击**“确定”**。 - 此时将在**“解决方案资源管理器”中该项目的“引用”**节点下显示所选的引用。 
创建自定义代码分析规则支持类
创建规则自身的类之前,将向项目添加一个访问器类和一个帮助器类。
提示
这些类对于创建其他自定义规则可能很有用。
您必须定义的第一个类为派生自 TSqlConcreteFragmentVisitor 的 WaitForDelayVisitor 类。 此类提供对模型中的 WAITFOR DELAY 语句的访问。
定义 WaitForDelayVisitor 类
- 在**“解决方案资源管理器”**中,选择 SampleRules 项目。 
- 在**“项目”菜单上选择“添加类”**。 - 将显示**“添加新项”**对话框。 
- 在**“名称”文本框中,键入 WaitForDelayVisitor.cs,然后单击“添加”**按钮。 - 此时 WaitForDelayVisitor.cs 文件将添加到**“解决方案资源管理器”**的项目中。 
- 打开 WaitForDelayVisitor.cs 文件,然后更新内容以匹配以下代码: - using System.Collections.Generic; using Microsoft.Data.Schema.ScriptDom.Sql; namespace SampleRules { class WaitForDelayVistor { } }
- 在类声明中,将访问修饰符更改为内部并从 TSqlConcreteFragmentVisitor 派生该类: - internal class WaitForDelayVisitor : TSqlConcreteFragmentVisitor { }
- 添加以下代码以定义 List 成员变量: - private List<WaitForStatement> _waitForDelayStatments;
- 通过添加以下代码来定义类构造函数: - #region ctor public WaitForDelayVisitor() { _waitForDelayStatments = new List<WaitForStatement>(); } #endregion
- 通过添加以下代码来定义只读 WaitForDelayStatements 属性: - #region properties public List<WaitForStatement> WaitForDelayStatements { get { return _waitForDelayStatments; } } #endregion
- 通过添加以下代码来重写 ExplicitVisit 方法: - #region overrides public override void ExplicitVisit(WaitForStatement node) { // We are only interested in WAITFOR DELAY occurrences if (node.WaitForOption == WaitForOption.Delay) { _waitForDelayStatments.Add(node); } } #endregion- 此方法将访问模型中的 WAITFOR 语句,并将指定了 DELAY 选项的 WAITFOR 语句添加到 WAITFOR DELAY 语句的列表中。 此处引用的关键类为 WaitForStatement。 
- 在**“文件”菜单上,单击“保存”**。 
第二个类是 SqlRuleUtils.cs,它包含自定义代码分析规则类将使用的某些实用工具方法,稍后将在此演练中的创建自定义代码分析规则类部分创建该规则类。 这些方法包括:
- GetElementName 用于获取模型元素的已转义的完全限定名称。 
- UpdateProblemPosition:用于计算行和列信息。 
- ReadFileContent:用于从文件读取内容。 
- GetElementSourceFile:用于获取源文件。 
- ComputeLineColumn:用于将偏移量从 ScriptDom 转换为脚本文件中的行和列。 
向项目添加 SqlRuleUtils.cs 文件
- 在**“解决方案资源管理器”**中,选择 SampleRules 项目。 
- 在**“项目”菜单上选择“添加类”**。 - 将显示**“添加新项”**对话框。 
- 在**“名称”文本框中,键入 SqlRuleUtils.cs,然后单击“添加”**按钮。 - 此时 SqlRuleUtils.cs 文件将添加到**“解决方案资源管理器”**的项目中。 
- 打开 SqlRuleUtils.cs 文件,并将以下 using 语句添加到文件中: - using System; using System.Diagnostics; using System.IO; using Microsoft.Data.Schema.SchemaModel; using Microsoft.Data.Schema.Sql.SchemaModel; using Microsoft.Data.Schema.StaticCodeAnalysis; using Microsoft.Data.Schema; namespace SampleRules { }
- 在 SqlRuleUtils 类声明中,将访问修饰符更改为公共静态: - public static class SqlRuleUtils { }
- 添加以下代码以创建 GetElementName 方法,该方法使用 SqlSchemaModel 和 ISqlModelElement 作为输入参数: - /// <summary> /// Get escaped fully qualified name of a model element /// </summary> /// <param name="sm">schema model</param> /// <param name="element">model element</param> /// <returns>name of the element</returns> public static string GetElementName(SqlSchemaModel sm, ISqlModelElement element) { return sm.DatabaseSchemaProvider.UserInteractionServices.GetElementName(element, ElementNameStyle.EscapedFullyQualifiedName); }
- 添加以下代码以创建 ReadFileContent 方法: - /// <summary> /// Read file content from a file. /// </summary> /// <param name="filePath"> file path </param> /// <returns> file content in a string </returns> public static string ReadFileContent(string filePath) { // Verify that the file exists first. if (!File.Exists(filePath)) { Debug.WriteLine(string.Format("Cannot find the file: '{0}'", filePath)); return string.Empty; } string content; using (StreamReader reader = new StreamReader(filePath)) { content = reader.ReadToEnd(); reader.Close(); } return content; }
- 添加以下代码以创建 GetElementSourceFile 方法,该方法使用 IModelElement 作为输入参数,并使用 String 检索文件名。 此方法将 IModelElement 强制转换为 IScriptSourcedModelElement,然后在确定模型元素中的脚本文件路径时使用 ISourceInformation。 - /// <summary> /// Get the corresponding script file path from a model element. /// </summary> /// <param name="element">model element</param> /// <param name="fileName">file path of the scripts corresponding to the model element</param> /// <returns></returns> private static Boolean GetElementSourceFile(IModelElement element, out String fileName) { fileName = null; IScriptSourcedModelElement scriptSourcedElement = element as IScriptSourcedModelElement; if (scriptSourcedElement != null) { ISourceInformation elementSource = scriptSourcedElement.PrimarySource; if (elementSource != null) { fileName = elementSource.SourceName; } } return String.IsNullOrEmpty(fileName) == false; }
- 添加以下代码以创建 ComputeLineColumn 方法: - /// This method converts offset from ScriptDom to line\column in script files. /// A line is defined as a sequence of characters followed by a carriage return ("\r"), /// a line feed ("\n"), or a carriage return immediately followed by a line feed. public static bool ComputeLineColumn(string text, Int32 offset, Int32 length, out Int32 startLine, out Int32 startColumn, out Int32 endLine, out Int32 endColumn) { const char LF = '\n'; const char CR = '\r'; // Setting the initial value of line and column to 0 since VS auto-increments by 1. startLine = 0; startColumn = 0; endLine = 0; endColumn = 0; int textLength = text.Length; if (offset < 0 || length < 0 || offset + length > textLength) { return false; } for (int charIndex = 0; charIndex < length + offset; ++charIndex) { char currentChar = text[charIndex]; Boolean afterOffset = charIndex >= offset; if (currentChar == LF) { ++endLine; endColumn = 0; if (afterOffset == false) { ++startLine; startColumn = 0; } } else if (currentChar == CR) { // CR/LF combination, consuming LF. if ((charIndex + 1 < textLength) && (text[charIndex + 1] == LF)) { ++charIndex; } ++endLine; endColumn = 0; if (afterOffset == false) { ++startLine; startColumn = 0; } } else { ++endColumn; if (afterOffset == false) { ++startColumn; } } } return true; }
- 添加以下代码以创建 UpdateProblemPosition 方法,该方法使用 DataRuleProblem 作为输入参数: - /// <summary> /// Compute the start Line/Col and the end Line/Col to update problem info /// </summary> /// <param name="problem">problem found</param> /// <param name="offset">offset of the fragment having problem</param> /// <param name="length">length of the fragment having problem</param> public static void UpdateProblemPosition(DataRuleProblem problem, int offset, int length) { if (problem.ModelElement != null) { String fileName = null; int startLine = 0; int startColumn = 0; int endLine = 0; int endColumn = 0; bool ret = GetElementSourceFile(problem.ModelElement, out fileName); if (ret) { string fullScript = ReadFileContent(fileName); if (fullScript != null) { if (ComputeLineColumn(fullScript, offset, length, out startLine, out startColumn, out endLine, out endColumn)) { problem.FileName = fileName; problem.StartLine = startLine + 1; problem.StartColumn = startColumn + 1; problem.EndLine = endLine + 1; problem.EndColumn = endColumn + 1; } else { Debug.WriteLine("Could not compute line and column"); } } } } }
- 在**“文件”菜单上,单击“保存”**。 
接下来,您添加一个资源文件,此文件将定义规则名称、规则说明以及规则在规则配置界面中所属的类别。
添加一个资源文件和三个资源字符串
- 在**“解决方案资源管理器”**中,选择 SampleRules 项目。 
- 在**“项目”菜单上选择“添加新项”**。 - 将显示**“添加新项”**对话框。 
- 在**“已安装的模板”列表中,单击“常规”**。 
- 在细节窗格中,单击**“资源文件”**。 
- 在**“名称”**中,键入 SampleRuleResource.resx。 - 这将显示资源编辑器,其中尚未定义任何资源。 
- 定义三个资源字符串,如下所示: - 名称 - 值 - AvoidWaitForDelay_ProblemDescription - 在 {0} 中找到了 WAITFOR DELAY 语句。 - AvoidWaitForDelay_RuleName - 避免在存储过程、函数和触发器中使用 WaitFor Delay 语句。 - CategorySamples - SamplesCategory 
- 在**“文件”菜单上,单击“保存 SampleRuleResource.resx”**。 
接下来,定义一个引用资源文件中的资源的类,这些资源供 Visual Studio 用来显示有关用户界面中的规则的信息。
定义 SampleConstants 类
- 在**“解决方案资源管理器”**中,选择 SampleRules 项目。 
- 在**“项目”菜单上选择“添加类”**。 - 将显示**“添加新项”**对话框。 
- 在**“名称”文本框中,键入 SampleRuleConstants.cs,然后单击“添加”**按钮。 - 此时 SampleRuleConstants.cs 文件将添加到**“解决方案资源管理器”**的项目中。 
- 打开 SampleRuleConstants.cs 文件,并将以下 using 语句添加到文件中: - namespace SampleRules { internal class SampleConstants { public const string NameSpace = "SamplesRules"; public const string ResourceBaseName = "SampleRules.SampleRuleResource"; public const string CategorySamples = "CategorySamples"; public const string AvoidWaitForDelayRuleId = "SR1004"; public const string AvoidWaitForDelay_RuleName = "AvoidWaitForDelay_RuleName"; public const string AvoidWaitForDelay_ProblemDescription = "AvoidWaitForDelay_ProblemDescription"; } }
- 在**“文件”菜单上,单击“保存”**。 
创建自定义代码分析规则类
现在已添加了自定义代码分析规则将使用的帮助器类,接下来将创建自定义规则类,并将其命名为 AvoidWaitForDelayRule。 AvoidWaitForDelayRule 自定义规则将用于帮助数据库开发人员避免在存储过程、触发器和函数中出现 WAITFOR DELAY 语句。
创建 AvoidWaitForDelayRule 类
- 在**“解决方案资源管理器”**中,选择 SampleRules 项目。 
- 在**“项目”菜单上,选择“新建文件夹”**。 
- 此时将在**“解决方案资源管理器”**中显示一个新文件夹。 将文件夹命名为 AvoidWaitForDelayRule。 
- 在**“解决方案资源管理器”**中,确认选择了 AvoidWaitForDelayRule 文件夹。 
- 在**“项目”菜单上选择“添加类”**。 - 将显示**“添加新项”**对话框。 
- 在**“名称”文本框中,键入 AvoidWaitForDelayRule.cs,然后单击“添加”**按钮。 - 此时 AvoidWaitForDelayRule.cs 文件将添加到**“解决方案资源管理器”**中项目的 AvoidWaitForDelayRule 文件夹。 
- 打开 AvoidWaitForDelayRule.cs 文件,并将以下 using 语句添加到文件中: - using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using Microsoft.Data.Schema.Extensibility; using Microsoft.Data.Schema.SchemaModel; using Microsoft.Data.Schema.ScriptDom.Sql; using Microsoft.Data.Schema.Sql.SchemaModel; using Microsoft.Data.Schema.Sql; using Microsoft.Data.Schema.StaticCodeAnalysis; namespace SampleRules { public class AvoidWaitForDelayRule { } }- 提示 - 您必须将命名空间名称从 SampleRules.AvoidWaitForDelayRule 更改为 SampleRules。 
- 在 AvoidWaitForDelayRule 类声明中,将访问修饰符更改为公共: - /// <summary> /// This is a SQL rule which returns a warning message /// whenever there is a WAITFOR DELAY statement appears inside a subroutine body. /// This rule only applies to SQL stored procedures, functions and triggers. /// </summary> public class AvoidWaitForDelayRule
- 从 StaticCodeAnalysisRule 基类派生 AvoidWaitForDelayRule 类: - public class AvoidWaitForDelayRule : StaticCodeAnalysisRule
- 将 DatabaseSchemaProviderCompatibilityAttribute、DataRuleAttribute 和 SupportedElementTypeAttribute 添加到类中。 有关功能扩展兼容性的更多信息,请参见扩展 Visual Studio 的数据库功能。 - [DatabaseSchemaProviderCompatibility(typeof(SqlDatabaseSchemaProvider))] [DataRuleAttribute( SampleConstants.NameSpace, SampleConstants.AvoidWaitForDelayRuleId, SampleConstants.ResourceBaseName, SampleConstants.AvoidWaitForDelay_RuleName, SampleConstants.CategorySamples, DescriptionResourceId = SampleConstants.AvoidWaitForDelay_ProblemDescription)] [SupportedElementType(typeof(ISqlProcedure))] [SupportedElementType(typeof(ISqlTrigger))] [SupportedElementType(typeof(ISqlFunction))] public class AvoidWaitForDelayRule : StaticCodeAnalysisRule- DataRuleAttribute 指定当配置数据库代码分析规则时显示在 Visual Studio 中的信息。 SupportedElementTypeAttribute 定义要将此规则应用于的元素的类型。 在此示例中,规则将应用于存储过程、触发器和函数。 
- 添加 Analyze 方法的重写,它使用 DataRuleSetting 和 DataRuleExecutionContext 作为输入参数。 此方法返回一个潜在问题列表。 - 此方法从上下文参数获取 IModelElement 和 TSqlFragment。 SqlSchemaModel 和 ISqlModelElement 是从模型元素获取的。 然后,使用 WaitForDelayVisitor 类获取一个包含模型中所有 WAITFOR DELAY 语句的列表。 - 为该列表中的每个 WaitForStatement 创建了一个 DataRuleProblem。 - #region Overrides /// <summary> /// Analyze the model element /// </summary> public override IList<DataRuleProblem> Analyze(DataRuleSetting ruleSetting, DataRuleExecutionContext context) { List<DataRuleProblem> problems = new List<DataRuleProblem>(); IModelElement modelElement = context.ModelElement; // this rule does not apply to inline table-valued function // we simply do not return any problem if (modelElement is ISqlInlineTableValuedFunction) { return problems; } // casting to SQL specific SqlSchemaModel sqlSchemaModel = modelElement.Model as SqlSchemaModel; Debug.Assert(sqlSchemaModel!=null, "SqlSchemaModel is expected"); ISqlModelElement sqlElement = modelElement as ISqlModelElement; Debug.Assert(sqlElement != null, "ISqlModelElement is expected"); // Get ScriptDom for this model element TSqlFragment sqlFragment = context.ScriptFragment as TSqlFragment; Debug.Assert(sqlFragment != null, "TSqlFragment is expected"); // visitor to get the ocurrences of WAITFOR DELAY statements WaitForDelayVisitor visitor = new WaitForDelayVisitor(); sqlFragment.Accept(visitor); List<WaitForStatement> waitforDelayStatements = visitor.WaitForDelayStatements; // Create problems for each WAITFOR DELAY statement found foreach (WaitForStatement waitForStatement in waitforDelayStatements) { DataRuleProblem problem = new DataRuleProblem(this, String.Format(CultureInfo.CurrentCulture, this.RuleProperties.Description, SqlRuleUtils.GetElementName(sqlSchemaModel, sqlElement)), sqlElement); SqlRuleUtils.UpdateProblemPosition(problem, waitForStatement.StartOffset, waitForStatement.FragmentLength); problems.Add(problem); } return problems; } #endregion
- 在**“文件”菜单上,单击“保存”**。 
接下来,将生成项目。
生成项目
- 从**“生成”菜单中,单击“生成解决方案”**。
接下来,将收集项目中生成的程序集信息,其中包括版本、区域性和 PublicKeyToken。
收集程序集信息
- 在**“视图”菜单上,单击“其他窗口”,然后单击“命令窗口”打开“命令”**窗口。 
- 在**“命令”**窗口中,键入以下代码。 将 FilePath 替换为已编译的 .dll 文件的路径和文件名。 在路径和文件名的两侧加双引号。 - 提示 - 默认情况下,FilePath 为 Projects\SampleRules\SampleRules\bin\Debug\YourDLL 或 Projects\SampleRules\SampleRules\bin\Release\YourDLL。 - ? System.Reflection.Assembly.LoadFrom(@"FilePath")
- 按 Enter。 此时该行应类似于以下内容,其中含有您的特定 PublicKeyToken: - "SampleRules, Version=1.0.0.0, Culture=neutral, PublicKeyToken=nnnnnnnnnnnnnnnn"- 记下或复制此程序集信息;下一过程中将使用这些信息。 
接下来,将使用上一过程中收集的程序集信息创建 XML 文件。
创建 XML 文件
- 在**“解决方案资源管理器”**中,选择 SampleRules 项目。 
- 在**“项目”菜单上选择“添加新项”**。 
- 在**“模板”窗格中,找到并选择“XML 文件”**项。 
- 在**“名称”文本框中,键入 SampleRules.Extensions.xml,然后单击“添加”**按钮。 - 此时 SampleRules.Extensions.xml 文件将添加到**“解决方案资源管理器”**的项目中。 
- 打开 SampleRules.Extensions.xml 文件,将其更新以匹配以下 XML。 将版本、区域性和 PublicKeyToken 值替换为上一过程中检索的这些值。 - <?xml version="1.0" encoding="utf-8"?> <extensions assembly="" version="1" xmlns="urn:Microsoft.Data.Schema.Extensions" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:Microsoft.Data.Schema.Extensions Microsoft.Data.Schema.Extensions.xsd"> <extension type="SampleRules.AvoidWaitForDelayRule" assembly="SampleRules, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b4deb9b383d021b0" enabled="true"/> </extensions>
- 在**“文件”菜单上,单击“保存”**。 
接下来,要将程序集信息和 XML 文件复制到 Extensions 目录。 Visual Studio 启动时,将识别 Microsoft Visual Studio 10.0\VSTSDB\Extensions 目录和子目录中的任何扩展,并注册这些扩展以用于会话中。
将程序集信息和 XML 文件复制到 Extensions 目录
- 在 Microsoft Visual Studio 10.0\VSTSDB\Extensions\ 目录中创建一个新文件夹,名为 CustomRules。 
- 将 SampleRules.dll 程序集文件从 Projects\SampleRules\SampleRules\bin\Debug\ 目录复制到已创建的 Microsoft Visual Studio 10.0\VSTSDB\Extensions\CustomRules 目录。 
- 将 SampleRules.Extensions.xml 文件从 Projects\SampleRules\SampleRules\ 目录复制到已创建的 Microsoft Visual Studio 10.0\VSTSDB\Extensions\CustomRules 目录。 - 提示 - 最佳做法是将扩展程序集放在 Microsoft Visual Studio 10.0\VSTSDB\Extensions 目录下的文件夹中。 这样将帮助您识别随产品包括了哪些扩展,以及哪些扩展是您自定义创建的。 建议使用文件夹将扩展按特定类别进行组织。 
接下来,将启动 Visual Studio 的新会话并创建数据库项目。
启动 Visual Studio 的新会话并创建数据库项目
- 再启动 Visual Studio 的一个会话。 
- 在**“文件”菜单上,单击“新建”,然后单击“项目”**。 
- 在**“新建项目”对话框的“已安装的模板”列表中,展开“数据库项目”节点,然后单击“SQL Server”**。 
- 在细节窗格中,选择**“SQL Server 2008 数据库项目”**。 
- 在**“名称”文本框中,键入 SampleRulesDB,然后单击“确定”**。 
最后,将看到新规则显示在 SQL Server 项目中。
查看新的 AvoidWaitForRule 代码分析规则
- 在**“解决方案资源管理器”**中,选择 SampleRulesDB 项目。 
- 在**“项目”菜单上,单击“属性”**。 - 此时将显示 SampleRulesDB 属性页。 
- 单击**“代码分析”**。 - 应该看到一个名为 CategorySamples 的新类别。 
- 展开 CategorySamples。 - 应该看到**“SR1004: 避免在存储过程、触发器和函数中出现 WAITFOR DELAY 语句”**。