编写适用于 SQL Server 的自定义静态代码分析规则程序集

此演练演示创建 SQL Server 代码分析规则的步骤。 在此演练中创建的规则用于避免存储过程、触发器和函数中的 WAITFOR DELAY 语句。

此演练使用以下过程创建 Transact-SQL 静态代码分析的自定义规则:

  1. 创建类库、启用该项目的签名并添加必要的引用。

  2. 创建 C# 自定义规则类。

  3. 创建两个帮助程序 C# 类。

  4. 为了安装你创建的已生成 DLL,请将其复制到 Extensions 目录中。

  5. 请验证新 Code Analysis 规则是否已到位。

先决条件

你需要满足以下条件才能完成本演练:

  • 已安装包含 SQL Server Data Tools 且支持 C# 或 Visual Basic .NET Framework 开发的 Visual Studio 版本。

  • 包含 SQL Server 对象的 SQL Server 项目。

  • 可以向其部署数据库项目的 SQL Server 实例。

本演练面向已熟悉 SQL Server Data Tools 的 SQL Server 功能的用户。 你应熟悉相关 Visual Studio 概念,例如,如何创建类库、添加 NuGet 包以及如何使用代码编辑器向类添加代码。

创建 SQL Server 的自定义 Code Analysis 规则

首先创建类库。 若要创建类库项目:

  1. 创建名为 SampleRules 的 C# (.NET Framework) 或 Visual Basic (.NET Framework) 类库项目。

  2. 将文件 Class1.cs 重命名为 AvoidWaitForDelayRule.cs

  3. 在解决方案资源管理器中,右键单击项目节点,然后依次选择“添加”、“引用”

  4. 在“程序集\框架”选项卡上选择 System.ComponentModel.Composition

  5. 在解决方案资源管理器中,右键单击项目节点,然后选择“管理 NuGet 包”。 找到并安装 Microsoft.SqlServer.DacFx NuGet 包。 所选版本必须为具有 Visual Studio 2022 的 162.x.x(例如 162.2.111)。

然后添加将由该规则使用的支持类。

创建自定义 Code Analysis 规则支持类

在创建规则本身的类之前,首先需要为项目添加访问者类和特性类。 这些类对于创建其他自定义规则可能非常实用。

定义 WaitForDelayVisitor 类

第一种必须定义的类是从 TSqlConcreteFragmentVisitor 派生的 WaitForDelayVisitor 类。 此类提供了对模型中 WAITFOR DELAY 语句的访问权限。 访问者类使用 SQL Server 提供的 ScriptDom API。 在此 API 中,Transact-SQL 代码表示为抽象语法树 (AST),如果希望查找特定语法对象(例如 WAITFOR DELAY 语句),访问者类可能非常实用。 使用对象模型可能难以找到这些语句,因为它们没有关联特定的对象属性或关系,而使用访问者模式和 ScriptDom API 就能够找到它们。

  1. 在“解决方案资源管理器”中,选择 SampleRules 项目。

  2. 在“项目”菜单上,选择“添加类”。 此时会显示“添加新项”对话框。

  3. 在“名称”文本框中键入 WaitForDelayVisitor.cs,然后选择“添加”按钮。 WaitForDelayVisitor.cs 文件将被添加到“解决方案资源管理器”中的项目。

  4. 打开 WaitForDelayVisitor.cs 文件并更新内容,以与以下代码相匹配:

    using System.Collections.Generic;
    using Microsoft.SqlServer.TransactSql.ScriptDom;
    namespace SampleRules {
        class WaitForDelayVistor {}
    }
    
  5. 在类声明中,将访问修饰符更改为内部并从 TSqlConcreteFragmentVisitor 派生类:

    internal class WaitForDelayVisitor : TSqlConcreteFragmentVisitor {}
    
  6. 添加以下代码以定义列表成员变量:

    public IList<WaitForStatement> WaitForDelayStatements { get; private set; }
    
  7. 通过添加以下代码,定义类构造函数:

    public WaitForDelayVisitor() {
       WaitForDelayStatements = new List<WaitForStatement>();
    }
    
  8. 通过添加以下代码,重写 ExplicitVisit 方法:

    public override void ExplicitVisit(WaitForStatement node) {
       // We are only interested in WAITFOR DELAY occurrences
       if (node.WaitForOption == WaitForOption.Delay)
          WaitForDelayStatements.Add(node);
    }
    

    此方法会访问模型中的 WAITFOR 语句,并且将指定了 DELAY 选项的语句添加到 WAITFOR DELAY 语句列表中。 引用的关键类为 WaitForStatement

  9. 在“文件”菜单上,选择“保存”。

定义 LocalizedExportCodeAnalysisRuleAttribute 类

第二个类为 LocalizedExportCodeAnalysisRuleAttribute.cs。 这是框架提供的内置 Microsoft.SqlServer.Dac.CodeAnalysis.ExportCodeAnalysisRuleAttribute 的扩展,支持从资源文件中读取规则使用的 DisplayNameDescription。 如果之前计划在多种语言中使用规则,此类会很有用。

  1. 在“解决方案资源管理器”中,选择 SampleRules 项目。

  2. 在“项目”菜单上,选择“添加类”。 此时会显示“添加新项”对话框。

  3. 在“名称”文本框中键入 LocalizedExportCodeAnalysisRuleAttribute.cs,然后选择“添加”按钮。 该文件将添加到“解决方案资源管理器”中的项目。

  4. 打开 文件并更新内容,以与以下代码相匹配:

    using Microsoft.SqlServer.Dac.CodeAnalysis;
    using System;
    using System.Globalization;
    using System.Reflection;
    using System.Resources;
    
    namespace SampleRules
    {
    
        internal class LocalizedExportCodeAnalysisRuleAttribute : ExportCodeAnalysisRuleAttribute
        {
            private readonly string _resourceBaseName;
            private readonly string _displayNameResourceId;
            private readonly string _descriptionResourceId;
    
            private ResourceManager _resourceManager;
            private string _displayName;
            private string _descriptionValue;
    
            /// <summary>
            /// Creates the attribute, with the specified rule ID, the fully qualified
            /// name of the resource file that will be used for looking up display name
            /// and description, and the Ids of those resources inside the resource file.
            /// </summary>
            public LocalizedExportCodeAnalysisRuleAttribute(
                string id,
                string resourceBaseName,
                string displayNameResourceId,
                string descriptionResourceId)
                : base(id, null)
            {
                _resourceBaseName = resourceBaseName;
                _displayNameResourceId = displayNameResourceId;
                _descriptionResourceId = descriptionResourceId;
            }
    
            /// <summary>
            /// Rules in a different assembly would need to overwrite this
            /// </summary>
            /// <returns></returns>
            protected virtual Assembly GetAssembly()
            {
                return GetType().Assembly;
            }
    
            private void EnsureResourceManagerInitialized()
            {
                var resourceAssembly = GetAssembly();
    
                try
                {
                    _resourceManager = new ResourceManager(_resourceBaseName, resourceAssembly);
                }
                catch (Exception ex)
                {
                    var msg = String.Format(CultureInfo.CurrentCulture, RuleResources.CannotCreateResourceManager, _resourceBaseName, resourceAssembly);
                    throw new RuleException(msg, ex);
                }
            }
    
            private string GetResourceString(string resourceId)
            {
                EnsureResourceManagerInitialized();
                return _resourceManager.GetString(resourceId, CultureInfo.CurrentUICulture);
            }
    
            /// <summary>
            /// Overrides the standard DisplayName and looks up its value inside a resources file
            /// </summary>
            public override string DisplayName
            {
                get
                {
                    if (_displayName == null)
                    {
                        _displayName = GetResourceString(_displayNameResourceId);
                    }
                    return _displayName;
                }
            }
    
            /// <summary>
            /// Overrides the standard Description and looks up its value inside a resources file
            /// </summary>
            public override string Description
            {
                get
                {
                    if (_descriptionValue == null)
                    {
                        _descriptionValue = GetResourceString(_descriptionResourceId);
                    }
                    return _descriptionValue;
                }
            }
        }
    }
    

添加一个资源文件和三个资源字符串

接下来,添加一个资源文件,该文件定义规则名称、规则描述以及类别(规则将在规则配置界面的该类别中显示)。

  1. 在“解决方案资源管理器”中,选择 SampleRules 项目。

  2. 在“项目”菜单上,选择“添加”,然后选择“新项”。 “添加新项”对话框随即出现。

  3. 在“已安装的模板”列表中,选择“常规”

  4. 在详细内容窗格中,选择“资源文件”

  5. “名称”中,键入 RuleResources.resx。 资源编辑器随即出现,其中未定义任何资源。

  6. 定义 4 个资源字符串,如下所示:

    名称
    AvoidWaitForDelay_ProblemDescription WAITFOR DELAY statement was found in {0}.
    AvoidWaitForDelay_RuleName Avoid using WaitFor Delay statements in stored procedures, functions and triggers.
    CategorySamples SamplesCategory
    CannotCreateResourceManager Can't create ResourceManager for {0} from {1}.
  7. 在“文件”菜单上,选择“保存RuleResources.resx”

定义 SampleConstants 类

接下来,定义某个类(该类引用资源文件中由 Visual Studio 使用的资源),以在用户界面中显示有关规则的信息。

  1. 在“解决方案资源管理器”中,选择 SampleRules 项目。

  2. 在“项目”菜单上,选择“添加”,然后选择“类”。 “添加新项”对话框随即出现。

  3. 在“名称”文本框中,键入“SampleRuleConstants.cs”,然后选择“添加”按钮。 SampleRuleConstants.cs 文件将被添加到“解决方案资源管理器”中的项目。

  4. 打开 SampleRuleConstants.cs 文件并将以下 using 语句添加到该文件:

    namespace SampleRules
    {
        internal static class RuleConstants
        {
            /// <summary>
            /// The name of the resources file to use when looking up rule resources
            /// </summary>
            public const string ResourceBaseName = "Public.Dac.Samples.Rules.RuleResources";
    
            /// <summary>
            /// Lookup name inside the resources file for the select asterisk rule name
            /// </summary>
            public const string AvoidWaitForDelay_RuleName = "AvoidWaitForDelay_RuleName";
            /// <summary>
            /// Lookup ID inside the resources file for the select asterisk description
            /// </summary>
            public const string AvoidWaitForDelay_ProblemDescription = "AvoidWaitForDelay_ProblemDescription";
    
            /// <summary>
            /// The design category (should not be localized)
            /// </summary>
            public const string CategoryDesign = "Design";
    
            /// <summary>
            /// The performance category (should not be localized)
            /// </summary>
            public const string CategoryPerformance = "Design";
        }
    }
    
  5. 选择“文件”>“保存”。

创建自定义 Code Analysis 规则类

添加自定义 Code Analysis 规则将使用的帮助程序类后,创建一个自定义规则类并将其命名为 AvoidWaitForDelayRuleAvoidWaitForDelayRule 自定义规则将用于帮助数据库开发人员避免在存储过程、触发器和函数中使用 WAITFOR DELAY 语句。

创建 AvoidWaitForDelayRule 类

  1. 在“解决方案资源管理器”中,选择 SampleRules 项目。

  2. 在“项目”菜单上,选择“添加”,然后选择“类”。 “添加新项”对话框随即出现。

  3. 在“名称”文本框中,键入 AvoidWaitForDelayRule.cs,然后选择“添加”AvoidWaitForDelayRule.cs 文件将被添加到“解决方案资源管理器”中的项目。

  4. 打开 AvoidWaitForDelayRule.cs 文件并将以下 using 语句添加到该文件:

    using Microsoft.SqlServer.Dac.CodeAnalysis;
    using Microsoft.SqlServer.Dac.Model;
    using Microsoft.SqlServer.TransactSql.ScriptDom;
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    namespace SampleRules {
        class AvoidWaitForDelayRule {}
    }
    
  5. AvoidWaitForDelayRule 类声明中,将访问修饰符更改为公共:

    /// <summary>
    /// This is a rule that returns a warning message
    /// whenever there is a WAITFOR DELAY statement appears inside a subroutine body.
    /// This rule only applies to stored procedures, functions and triggers.
    /// </summary>
    public sealed class AvoidWaitForDelayRule
    
  6. AvoidWaitForDelayRule 类派生自 Microsoft.SqlServer.Dac.CodeAnalysis.SqlCodeAnalysisRule 基类:

    public sealed class AvoidWaitForDelayRule : SqlCodeAnalysisRule
    
  7. LocalizedExportCodeAnalysisRuleAttribute 添加到类中。

    LocalizedExportCodeAnalysisRuleAttribute 允许 Code Analysis 服务发现自定义 Code Analysis 规则。 只有使用 ExportCodeAnalysisRuleAttribute 标记的类(或派生自此类的属性)可以用于代码分析。

    LocalizedExportCodeAnalysisRuleAttribute 提供服务使用的一些必需元数据。 包括此规则的唯一 ID、将在 Visual Studio 用户界面显示的显示名称以及标识问题时可由规则使用的 Description

    [LocalizedExportCodeAnalysisRule(AvoidWaitForDelayRule.RuleId,
        RuleConstants.ResourceBaseName,
        RuleConstants.AvoidWaitForDelay_RuleName,
        RuleConstants.AvoidWaitForDelay_ProblemDescription
        Category = RuleConstants.CategoryPerformance,
        RuleScope = SqlRuleScope.Element)]
    public sealed class AvoidWaitForDelayRule : SqlCodeAnalysisRule
    {
       /// <summary>
       /// The Rule ID should resemble a fully-qualified class name. In the Visual Studio UI
       /// rules are grouped by "Namespace + Category", and each rule is shown using "Short ID: DisplayName".
       /// For this rule, that means the grouping will be "Public.Dac.Samples.Performance", with the rule
       /// shown as "SR1004: Avoid using WaitFor Delay statements in stored procedures, functions and triggers."
       /// </summary>
       public const string RuleId = "RuleSamples.SR1004";
    }
    

    当此规则分析特定元素时,RuleScope 属性应为 Microsoft.SqlServer.Dac.CodeAnalysis.SqlRuleScope.Element。 该规则为模型中的每个匹配元素调用一次。 如果想要分析整个模型,则可以改用 Microsoft.SqlServer.Dac.CodeAnalysis.SqlRuleScope.Model

  8. 添加设置 Microsoft.SqlServer.Dac.CodeAnalysis.SqlAnalysisRule.SupportedElementTypes 的构造函数。 这是元素范围内的规则所必需的。 它定义了应用此规则的元素的类型。 在这种情况下,此规则会应用于存储过程、触发器和函数。 Microsoft.SqlServer.Dac.Model.ModelSchema 类列出了可以分析的所有可用元素类型。

    public AvoidWaitForDelayRule()
    {
       // This rule supports Procedures, Functions and Triggers. Only those objects will be passed to the Analyze method
       SupportedElementTypes = new[]
       {
          // Note: can use the ModelSchema definitions, or access the TypeClass for any of these types
          ModelSchema.ExtendedProcedure,
          ModelSchema.Procedure,
          ModelSchema.TableValuedFunction,
          ModelSchema.ScalarFunction,
    
          ModelSchema.DatabaseDdlTrigger,
          ModelSchema.DmlTrigger,
          ModelSchema.ServerDdlTrigger
       };
    }
    
  9. 添加 Microsoft.SqlServer.Dac.CodeAnalysis.SqlAnalysisRule.Analyze (Microsoft.SqlServer.Dac.CodeAnalysis.SqlRuleExecutionContext) 方法的替代,该方法将 Microsoft.SqlServer.Dac.CodeAnalysis.SqlRuleExecutionContext 用作输入参数。 此方法返回潜在问题列表。

    该方法从上下文参数中获取 Microsoft.SqlServer.Dac.Model.TSqlModelMicrosoft.SqlServer.Dac.Model.TSqlObjectTSqlFragment。 然后使用 WaitForDelayVisitor 类获取包含模型中所有 WAITFOR DELAY 语句的列表。

    对于该列表中的每个 WaitForStatement,将创建 Microsoft.SqlServer.Dac.CodeAnalysis.SqlRuleProblem

    /// <summary>
    /// For element-scoped rules the Analyze method is executed once for every matching
    /// object in the model.
    /// </summary>
    /// <param name="ruleExecutionContext">The context object contains the TSqlObject being
    /// analyzed, a TSqlFragment
    /// that's the AST representation of the object, the current rule's descriptor, and a
    /// reference to the model being
    /// analyzed.
    /// </param>
    /// <returns>A list of problems should be returned. These will be displayed in the Visual
    /// Studio error list</returns>
    public override IList<SqlRuleProblem> Analyze(
        SqlRuleExecutionContext ruleExecutionContext)
    {
         IList<SqlRuleProblem> problems = new List<SqlRuleProblem>();
    
         TSqlObject modelElement = ruleExecutionContext.ModelElement;
    
         // this rule does not apply to inline table-valued function
         // we simply do not return any problem in that case.
         if (IsInlineTableValuedFunction(modelElement))
         {
             return problems;
         }
    
         string elementName = GetElementName(ruleExecutionContext, modelElement);
    
         // The rule execution context has all the objects we'll need, including the
         // fragment representing the object,
         // and a descriptor that lets us access rule metadata
         TSqlFragment fragment = ruleExecutionContext.ScriptFragment;
         RuleDescriptor ruleDescriptor = ruleExecutionContext.RuleDescriptor;
    
         // To process the fragment and identify WAITFOR DELAY statements we will use a
         // visitor
         WaitForDelayVisitor visitor = new WaitForDelayVisitor();
         fragment.Accept(visitor);
         IList<WaitForStatement> waitforDelayStatements = visitor.WaitForDelayStatements;
    
         // Create problems for each WAITFOR DELAY statement found
         // When creating a rule problem, always include the TSqlObject being analyzed. This
         // is used to determine
         // the name of the source this problem was found in and a best guess as to the
         // line/column the problem was found at.
         //
         // In addition if you have a specific TSqlFragment that is related to the problem
         //also include this
         // since the most accurate source position information (start line and column) will
         // be read from the fragment
         foreach (WaitForStatement waitForStatement in waitforDelayStatements)
         {
            SqlRuleProblem problem = new SqlRuleProblem(
                String.Format(CultureInfo.CurrentCulture,
                    ruleDescriptor.DisplayDescription, elementName),
                modelElement,
                waitForStatement);
            problems.Add(problem);
        }
        return problems;
    }
    
    private static string GetElementName(
        SqlRuleExecutionContext ruleExecutionContext,
        TSqlObject modelElement)
    {
        // Get the element name using the built in DisplayServices. This provides a number of
        // useful formatting options to
        // make a name user-readable
        var displayServices = ruleExecutionContext.SchemaModel.DisplayServices;
        string elementName = displayServices.GetElementName(
            modelElement, ElementNameStyle.EscapedFullyQualifiedName);
        return elementName;
    }
    
    private static bool IsInlineTableValuedFunction(TSqlObject modelElement)
    {
        return TableValuedFunction.TypeClass.Equals(modelElement.ObjectType)
                       && FunctionType.InlineTableValuedFunction ==
            modelElement.GetMetadata<FunctionType>(TableValuedFunction.FunctionType);
    }
    
  10. 选择“文件”>“保存”。

生成类库

  1. 在“项目”菜单上,选择“SampleRules 属性”

  2. 选择“签名”选项卡。

  3. 选择“为程序集签名”

  4. 在“选择强名称密钥文件”中,选择“新建”<>

  5. 在“创建强名称密钥”对话框的“密钥文件名称”中,键入“MyRefKey”。

  6. (可选)可以为强名称密钥文件指定密码。

  7. 选择“确定”

  8. 在“文件”菜单上,单击“全部保存”。

  9. 在“生成”菜单上,选择“生成解决方案” 。

安装静态 Code Analysis 规则

然后必须安装程序集,以便在生成和部署 SQL Server 项目时加载该程序集。

要安装规则,必须将程序集和关联的 .pdb 文件复制到 Extensions 文件夹。

安装 SampleRules 程序集

然后将程序集信息复制到 Extensions 目录中。 当 Visual Studio 启动后,它会标识 <Visual Studio Install Dir>\Common7\IDE\Extensions\Microsoft\SQLDB\DAC\Extensions 目录和子目录中的任何扩展,并使其可供使用。

对于 Visual Studio 2022,<Visual Studio Install Dir> 通常为 C:\Program Files\Microsoft Visual Studio\2022\Enterprise。 将 Enterprise 替换为 ProfessionalCommunity,具体取决于安装的 Visual Studio 版本。

将 SampleRules.dll 程序集文件从输出目录复制到 <Visual Studio Install Dir>\Common7\IDE\Extensions\Microsoft\SQLDB\DAC\Extensions 该目录。 默认情况下,已编译的 .dll 文件的路径为 YourSolutionPath\YourProjectPath\bin\DebugYourSolutionPath\YourProjectPath\bin\Release

注意

可能需要创建 Extensions 目录。

现在规则安装完成,重新启动 Visual Studio 即可显示。 然后启动 Visual Studio 的一个新会话并且创建一个数据库项目。

启动新 Visual Studio 会话并且创建数据库项目

  1. 启动 Visual Studio 的第二个会话。

  2. 选择“文件”>“新建”>“项目”。

  3. 在“新建项目”对话框中,找到并选择“SQL Server 数据库项目”

  4. 在“名称”文本框中,键入 SampleRulesDB,然后选择“确定”

最后会看到在 SQL Server 项目中显示的新规则。 要查看新的 AvoidWaitForRule Code Analysis 规则,请执行以下操作:

  1. 在“解决方案资源管理器”中,选择 SampleRulesDB 项目。

  2. 在“项目”菜单上选择“属性”。 此时将显示 SampleRulesDB 属性页。

  3. 选择“Code Analysis”。 应会看到名为 RuleSamples.CategorySamples 的新类别。

  4. 展开 RuleSamples.CategorySamples。 应会看到 SR1004: Avoid WAITFOR DELAY statement in stored procedures, triggers, and functions