演练:创作 SQL 的自定义静态代码分析规则程序集
此分步主题演示了用于创建 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 语句”**。