Share via


Exemplarische Vorgehensweise: Erweitern des Datenbankprojektbuilds zum Generieren von Modellstatistiken

Sie können einen Buildcontributor erstellen, damit beim Erstellen eines Datenbankprojekts benutzerdefinierte Aktionen ausgeführt werden. In dieser exemplarischen Vorgehensweise wird ein Buildcontributor mit dem Namen "ModelStatistics" erstellt, durch den beim Erstellen eines Datenbankprojekts Statistikdaten aus dem Datenbankmodell ausgegeben werden. Da von diesem Buildcontributor bei der Erstellung Parameter akzeptiert werden, sind einige zusätzliche Schritte erforderlich.

Im Verlauf dieser exemplarischen Vorgehensweise werden die folgenden Hauptaufgaben ausgeführt:

  • Erstellen eines Buildcontributors

  • Installieren des Buildcontributors

  • Testen des Buildcontributors

Vorbereitungsmaßnahmen

Zum Durchführen dieser exemplarischen Vorgehensweise benötigen Sie die folgenden Komponenten:

  • Visual Studio 2010 Premium oder Visual Studio 2010 Ultimate muss installiert sein.

  • Sie müssen über ein Datenbankprojekt mit Datenbankobjekten verfügen.

Tipp

Diese exemplarische Vorgehensweise richtet sich an Benutzer, die bereits mit den Datenbankfunktionen von Visual Studio Premium vertraut sind. Sie sollten außerdem mit grundlegenden Konzepten von Visual Studio wie dem Erstellen einer Klassenbibliothek und dem Verwenden des Code-Editors zum Hinzufügen von Code zu einer Klasse vertraut sein.

Erstellen eines Buildcontributors

Führen Sie zum Erstellen eines Buildcontributors die folgenden Aufgaben aus:

  • Erstellen eines Klassenbibliothekprojekts und Hinzufügen erforderlicher Verweise

  • Definieren einer Klasse mit dem Namen "ModelStatistics" und Vererbung von BuildContributor

  • Überschreiben der OnPopulateArguments-Methode und der OnExecute-Methode

  • Hinzufügen einiger privater Hilfsmethoden

  • Erstellen der resultierenden Assembly

Tipp

Dieser Contributor gibt nur dann eine Ausgabe aus, wenn ein Datenbankprojekt mit MSBuild erstellt wird. Der Bericht ist standardmäßig deaktiviert, aber dies können Sie überschreiben, indem Sie eine Eigenschaft in der MSBuild-Befehlszeile bereitstellen. Ein Beispiel zum Aktivieren der Ausgabe im Ausgabefenster finden Sie unter Exemplarische Vorgehensweise: Erweitern der Bereitstellung von Datenbankprojekten zum Analysieren des Bereitstellungsplans.

So erstellen Sie ein Klassenbibliotheksprojekt

  1. Erstellen Sie ein Visual Basic- oder Visual C#-Klassenbibliothekprojekt mit dem Namen "MyBuildContributor".

  2. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf den Projektknoten, und klicken Sie anschließend auf Verweis hinzufügen.

  3. Klicken Sie auf die Registerkarte .NET.

  4. Markieren Sie die Einträge Microsoft.Data.Schema und Microsoft.Data.Schema.Sql, und klicken Sie auf OK.

    Im nächsten Schritt wird der Klasse Code hinzugefügt.

So definieren Sie die ModelStatistics-Klasse

  1. Aktualisieren Sie im Code-Editor die Datei "class1.cs" mit den folgenden using- oder Imports-Anweisungen:

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Xml.Linq;
    using Microsoft.Data.Schema;
    using Microsoft.Data.Schema.Build;
    using Microsoft.Data.Schema.Extensibility;
    using Microsoft.Data.Schema.SchemaModel;
    using Microsoft.Data.Schema.Sql;
    
    Imports System
    Imports System.Collections.Generic
    Imports System.IO
    Imports System.Linq
    Imports System.Xml.Linq
    Imports Microsoft.Data.Schema
    Imports Microsoft.Data.Schema.Build
    Imports Microsoft.Data.Schema.Extensibility
    Imports Microsoft.Data.Schema.SchemaModel
    Imports Microsoft.Data.Schema.Sql
    
  2. Aktualisieren Sie die Klassendefinition wie folgt:

    /// <summary>
    /// The ModelStatistics class demonstrates
    /// how you can create a class that inherits BuildContributor
    /// to perform actions when you build a database project.
    /// </summary>
    [DatabaseSchemaProviderCompatibility(typeof(SqlDatabaseSchemaProvider))]
        public sealed class ModelStatistics : BuildContributor
        {
        }
    
    ''' <summary>
    ''' The ModelStatistics class demonstrates
    ''' how you can create a class that inherits BuildContributor
    ''' to perform actions when you build a database project.
    ''' </summary>
    <DatabaseSchemaProviderCompatibility(GetType(SqlDatabaseSchemaProvider))> _
    Public NotInheritable Class ModelStatistics
        Inherits BuildContributor
    End Class
    

    Sie haben nun den Buildcontributor definiert und mit dem Attribut angegeben, dass dieser Contributor mit jedem beliebigen Datenbankschema-Anbieter kompatibel ist, der von SqlDatabaseSchemaProvider erbt.

  3. Fügen Sie als Nächstes die folgenden Member hinzu. Mithilfe der Member ermöglichen Sie dem Anbieter das Akzeptieren von Befehlszeilen-Buildparametern:

            private const string GenerateModelStatistics = "GenerateModelStatistics";
            private const string SortModelStatisticsBy = "SortModelStatisticsBy";
            private const string GenerateModelStatisticsVariable = "$(" + GenerateModelStatistics + ")";
            private const string SortModelStatisticsByVariable = "$(" + SortModelStatisticsBy + ")";
    
            private enum SortBy { None, Name, Value };
            private static Dictionary<string, SortBy> SortByMap = new Dictionary<string, SortBy>(StringComparer.OrdinalIgnoreCase)
            {
                { "none", SortBy.None },
                { "name", SortBy.Name },
                { "value", SortBy.Value },
            };
    
            private SortBy _sortBy = SortBy.None;
    
        Private Const GenerateModelStatistics As String = "GenerateModelStatistics"
        Private Const SortModelStatisticsBy As String = "SortModelStatisticsBy"
        Private Const GenerateModelStatisticsVariable As String = "$(" & GenerateModelStatistics & ")"
        Private Const SortModelStatisticsByVariable As String = "$(" & SortModelStatisticsBy & ")"
    
        Private Enum SortBy
            None
            Name
            Value
        End Enum
        '' SortByMap maps the command-line parameter string values to the enumeration values
        Private Shared SortByMap As New Dictionary(Of String, SortBy)(StringComparer.OrdinalIgnoreCase)
    
        Private _sortBy As SortBy = SortBy.None
    

    Dank dieser Member kann der Benutzer mithilfe der GenerateModelStatistics-Option angeben, ob die Statistik generiert werden soll. Zudem kann der Benutzer mithilfe der SortModelStatisticsBy-Option die gewünschte Sortierung der Statistik angeben.

    Im nächsten Schritt wird die OnPopulateArguments-Methode überschrieben, um die Liste mit den Argumenten zu erstellen, die an den Buildcontributor übergeben werden sollen.

So überschreiben Sie OnPopulateArguments

  • Fügen Sie der ModelStatistics-Klasse die folgende Überschreibungsmethode hinzu:

        /// <summary>
        /// Override the OnPopulateArgument method to build a list of arguments from the input
        /// configuration information.
        /// </summary>
            protected override IList<ContributorArgumentConfiguration> OnPopulateArguments()
            {
                List<ContributorArgumentConfiguration> args = new List<ContributorArgumentConfiguration>();
    
                args.Add(new ContributorArgumentConfiguration(
                    name: GenerateModelStatistics,
                    value: GenerateModelStatisticsVariable,
                    condition: null));
    
                args.Add(new ContributorArgumentConfiguration(
                    name: SortModelStatisticsBy,
                    value: SortModelStatisticsByVariable,
                    condition: null));
    
                return args;
            }
    
        ''' <summary>
        ''' Override the OnPopulateArgument method to build a list of arguments from the input
        ''' configuration information.
        ''' </summary>
        Protected Overrides Function OnPopulateArguments() As System.Collections.Generic.IList(Of Microsoft.Data.Schema.Build.ContributorArgumentConfiguration)
            Dim args As List(Of ContributorArgumentConfiguration) = New List(Of ContributorArgumentConfiguration)
    
            args.Add(New ContributorArgumentConfiguration(name:=GenerateModelStatistics, _
                                                           value:=GenerateModelStatisticsVariable, _
                                                           condition:=Nothing))
            args.Add(New ContributorArgumentConfiguration(name:=SortModelStatisticsBy, _
                                                           value:=SortModelStatisticsByVariable, _
                                                           condition:=Nothing))
            Return MyBase.OnPopulateArguments()
        End Function
    

    Zwei ContributorArgumentConfiguration-Objekte werden erstellt und der Argumentliste hinzugefügt.

    Anschließend wird die OnExecute-Methode überschrieben, um den Code hinzuzufügen, der beim Erstellen eines Datenbankprojekts ausgeführt werden soll.

So überschreiben Sie OnExecute

  • Fügen Sie der ModelStatistics-Klasse die folgende Methode hinzu:

        /// <summary>
        /// Override the OnExecute method to perform actions when you build a database project.
        /// </summary>
            protected override void OnExecute(BuildContributorContext context, ErrorManager errorsContainer)
            {
                // handle related arguments, passed in as part of
                // the context information.
                bool generateModelStatistics;
                ParseArguments(context.Arguments, errorsContainer, out generateModelStatistics);
    
                // Only generate statistics if requested to do so
                if (generateModelStatistics)
                {
                    // First, output model-wide information, such
                    // as the type of database schema provider (DSP)
                    // and the collation.
                    List<DataSchemaError> args = new List<DataSchemaError>();
                    args.Add(new DataSchemaError(" ", ErrorSeverity.Message));
                    args.Add(new DataSchemaError("Model Statistics:", ErrorSeverity.Message));
                    args.Add(new DataSchemaError("=================", ErrorSeverity.Message));
                    args.Add(new DataSchemaError(" ", ErrorSeverity.Message));
                    errorsContainer.Add(args, ErrorManager.DefaultCategory);
    
                    var model = context.Model;
    
                    // Start building up the XML that will later
                    // be serialized.
                    var xRoot = new XElement("ModelStatistics");
    
                    SummarizeModelInfo(model, xRoot, errorsContainer);
    
                    // First, count the elements that are contained 
                    // in this model.
                    var elements = model.GetElements(typeof(IModelElement), ModelElementQueryFilter.Internal);
                    Summarize(elements, element => element.ElementClass.ClassName, "Internal Elements", xRoot, errorsContainer);
    
                    // Now, count the elements that are defined in
                    // another model. Examples include built-in types,
                    // roles, filegroups, assemblies, and any 
                    // referenced objects from another database.
                    elements = model.GetElements(typeof(IModelElement), ModelElementQueryFilter.External);
                    Summarize(elements, element => element.ElementClass.ClassName, "External Elements", xRoot, errorsContainer);
    
                    // Now, count the number of each type
                    // of relationship in the model.
                    SurveyRelationships(model, xRoot, errorsContainer);
    
                    // Count the various types of annotations
                    // in the model.
                    var annotations = model.GetAllAnnotations();
                    Summarize(annotations, anno => anno.AnnotationClass.ClassName, "Annotations", xRoot, errorsContainer);
    
                    // finally, count any types of custom data
                    // defined for the model.
                    var customData = model.GetCustomData();
                    Summarize(customData, custom => custom.Category, "Custom Data", xRoot, errorsContainer);
    
                    // Determine where the user wants to save
                    // the serialized XML file.
                    string outDir;
                    if (context.Arguments.TryGetValue("OutDir", out outDir) == false)
                    {
                        outDir = ".";
                    }
                    var filePath = Path.Combine(outDir, "ModelStatistics.xml");
                    // Save the XML file and tell the user
                    // where it was saved.
                    xRoot.Save(filePath);
                    DataSchemaError resultArg = new DataSchemaError("Result was saved to " + filePath, ErrorSeverity.Message);
                    errorsContainer.Add(resultArg, ErrorManager.BuildCategory);
                }
            }
    
    ''' <summary>
    ''' Override the OnExecute method to perform actions when you build a database project.
    ''' </summary>
    Protected Overloads Overrides Sub OnExecute(ByVal context As BuildContributorContext, ByVal errorsContainer As ErrorManager)
        ' handle related arguments, passed in as part of
        ' the context information.
        Dim generateModelStatistics As Boolean
        ParseArguments(context.Arguments, errorsContainer, generateModelStatistics)
    
        ' Only generate statistics if requested to do so
        If generateModelStatistics Then
            ' First, output model-wide information, such
            ' as the type of database schema provider (DSP)
            ' and the collation.
            Dim args As New List(Of DataSchemaError)()
            args.Add(New DataSchemaError(" ", ErrorSeverity.Message))
            args.Add(New DataSchemaError("Model Statistics:", ErrorSeverity.Message))
            args.Add(New DataSchemaError("=================", ErrorSeverity.Message))
            args.Add(New DataSchemaError(" ", ErrorSeverity.Message))
            errorsContainer.Add(args, ErrorManager.DefaultCategory)
    
            Dim model = context.Model
    
            ' Start building up the XML that will later
            ' be serialized.
            Dim xRoot = New XElement("ModelStatistics")
    
            SummarizeModelInfo(model, xRoot, errorsContainer)
    
            ' First, count the elements that are contained 
            ' in this model.
            Dim elements = model.GetElements(GetType(IModelElement), ModelElementQueryFilter.Internal)
            Summarize(elements, Function(element) element.ElementClass.ClassName, "Internal Elements", xRoot, errorsContainer)
    
            ' Now, count the elements that are defined in
            ' another model. Examples include built-in types,
            ' roles, filegroups, assemblies, and any 
            ' referenced objects from another database.
            elements = model.GetElements(GetType(IModelElement), ModelElementQueryFilter.External)
            Summarize(elements, Function(element) element.ElementClass.ClassName, "External Elements", xRoot, errorsContainer)
    
            ' Now, count the number of each type
            ' of relationship in the model.
            SurveyRelationships(model, xRoot, errorsContainer)
    
            ' Count the various types of annotations
            ' in the model.
            Dim annotations = model.GetAllAnnotations()
            Summarize(annotations, Function(anno) anno.AnnotationClass.ClassName, "Annotations", xRoot, errorsContainer)
    
            ' finally, count any types of custom data
            ' defined for the model.
            Dim customData = model.GetCustomData()
            Summarize(customData, Function([custom]) [custom].Category, "Custom Data", xRoot, errorsContainer)
    
            ' Determine where the user wants to save
            ' the serialized XML file.
            Dim outDir As String
            If context.Arguments.TryGetValue("OutDir", outDir) = False Then
                outDir = "."
            End If
            Dim filePath = Path.Combine(outDir, "ModelStatistics.xml")
            ' Save the XML file and tell the user
            ' where it was saved.
            xRoot.Save(filePath)
            Dim resultArg As New DataSchemaError("Result was saved to " & filePath, ErrorSeverity.Message)
            errorsContainer.Add(resultArg, ErrorManager.BuildCategory)
        End If
    End Sub
    

    An die OnExecute-Methode wird ein BuildContributorContext-Objekt übergeben, das den Zugriff auf beliebige angegebene Argumente, auf das Datenbankmodell, auf Buildeigenschaften sowie auf Erweiterungsdateien ermöglicht. In diesem Beispiel wird zunächst das Modell abgerufen. Anschließend werden Hilfsfunktionen aufgerufen, um Informationen zum Modell auszugeben. An die Methode wird auch ein ErrorManager übergeben, um die aufgetretenen Fehler zu melden.

    Weitere relevante Typen und Methoden: DataSchemaModel, ModelStore, GetElements, GetAllAnnotations, GetCustomData und ModelElement.

    Im nächsten Schritt werden die Hilfsmethoden zum Untersuchen der Modelldetails definiert.

So fügen Sie Hilfsmethoden zum Generieren der Statistik hinzu

  • Fügen Sie zunächst die Skelette der vier Hilfsmethoden hinzu, indem Sie den folgenden Code hinzufügen:

            /// <summary>
            /// Examine the arguments provided by the user
            /// to determine if model statistics should be generated
            /// and, if so, how the results should be sorted.
            /// </summary>
            private void ParseArguments(IDictionary<string, string> arguments, ErrorManager errorsContainer, out bool generateModelStatistics)
            {
            }
    
            /// <summary>
            /// Retrieve the database schema provider for the
            /// model and the collation of that model.
            /// Results are output to the console and added to the XML
            /// being constructed.
            /// </summary>
            private static void SummarizeModelInfo(DataSchemaModel model, XElement xContainer, ErrorManager errorsContainer)
            {
            }
    
            /// <summary>
            /// For a provided list of model elements, count the number
            /// of elements for each class name, sorted as specified
            /// by the user.
            /// Results are output to the console and added to the XML
            /// being constructed.
            /// </summary>
            private void Summarize<T>(IList<T> set, Func<T, string> groupValue, string category, XElement xContainer, ErrorManager errorsContainer)
            {
            }
    
            /// <summary>
            /// Iterate over all model elements, counting the
            /// styles and types for relationships that reference each 
            /// element
            /// Results are output to the console and added to the XML
            /// being constructed.
            /// </summary>
            private static void SurveyRelationships(DataSchemaModel model, XElement xContainer, ErrorManager errorsContainer)
            {
            }
    
            /// <summary>
            /// Performs the actual output for this contributor,
            /// writing the specified set of statistics, and adding any 
            /// output information to the XML being constructed.
            /// </summary>
            private static void OutputResult<T>(string category, Dictionary<string, T> statistics, XElement xContainer, ErrorManager errorsContainer)
            {
            }
    
        ''' <summary> 
        ''' This method goes through the provided arguments to the contributor and determines what 
        ''' parameters and parameter values were provided by the user.
        ''' </summary> 
        Private Sub ParseArguments(ByVal arguments As IDictionary(Of String, String), ByVal errorsContainer As ErrorManager, ByRef generateModelStatistics__1 As Boolean)
        End Sub
    
        ''' <summary> 
        ''' This method collects and outputs information about the model itself. 
        ''' </summary> 
    Private Shared Sub SummarizeModelInfo(ByVal model As DataSchemaModel, ByVal xContainer As XElement, ByVal errorsContainer As ErrorManager)
        End Sub
    
        ''' <summary> 
        ''' This method goes counts the number of elements in specific categories from the provided element list.
        ''' </summary> 
    Private Sub Summarize(Of T)(ByVal [set] As IList(Of T), ByVal groupValue As Func(Of T, String), ByVal category As String, ByVal xContainer As XElement, ByVal errorsContainer As ErrorManager)
        End Sub
    
        ''' <summary> 
        ''' This method counts the number of relationships of each type that reference elements in the model 
        ''' </summary> 
    Private Shared Sub SurveyRelationships(ByVal model As DataSchemaModel, ByVal xContainer As XElement, ByVal errorsContainer As ErrorManager)
        End Sub
    
        ''' <summary> 
        ''' This method processes the provided data, outputting it to the console and adding the information  
        ''' to the provided XML.
        ''' </summary> 
    Private Shared Sub OutputResult(Of T)(ByVal category As String, ByVal statistics As Dictionary(Of String, T), ByVal xContainer As XElement, ByVal errorsContainer As ErrorManager)
        End Sub
    

So definieren Sie den Text der ParseArguments-Methode

  • Fügen Sie dem Text der ParseArguments-Methode den folgenden Code hinzu:

                // By default, we don't generate model statistics
                generateModelStatistics = false;
    
                // see if the user provided the GenerateModelStatistics 
                // option and if so, what value was it given.
                string valueString;
                arguments.TryGetValue(GenerateModelStatistics, out valueString);
                if (string.IsNullOrWhiteSpace(valueString) == false)
                {
                    if (bool.TryParse(valueString, out generateModelStatistics) == false)
                    {
                        generateModelStatistics = false;
    
                        // The value was not valid from the end user
                        DataSchemaError invalidArg = new DataSchemaError(
                            GenerateModelStatistics + "=" + valueString + " was not valid.  It can be true or false", ErrorSeverity.Error);
                        errorsContainer.Add(invalidArg, ErrorManager.BuildCategory);
                        return;
                    }
                }
    
                // Only worry about sort order if the user requested
                // that we generate model statistics.
                if (generateModelStatistics)
                {
                    // see if the user provided the sort option and
                    // if so, what value was provided.
                    arguments.TryGetValue(SortModelStatisticsBy, out valueString);
                    if (string.IsNullOrWhiteSpace(valueString) == false)
                    {
                        SortBy sortBy;
                        if (SortByMap.TryGetValue(valueString, out sortBy))
                        {
                            _sortBy = sortBy;
                        }
                        else
                        {
                            // The value was not valid from the end user
                            DataSchemaError invalidArg = new DataSchemaError(
                                SortModelStatisticsBy + "=" + valueString + " was not valid.  It can be none, name, or value", ErrorSeverity.Error);
                            errorsContainer.Add(invalidArg, ErrorManager.BuildCategory);
                        }
                    }
                }
    
            ' By default, we don't generate model statistics 
            generateModelStatistics__1 = False
    
            ' see if the user provided the GenerateModelStatistics 
            ' option and if so, what value was it given. 
            Dim valueString As String = ""
            arguments.TryGetValue(GenerateModelStatistics, valueString)
            If String.IsNullOrWhiteSpace(valueString) = False Then
                If Boolean.TryParse(valueString, generateModelStatistics__1) = False Then
                    generateModelStatistics__1 = False
    
                    ' The value was not valid from the end user 
                    Dim invalidArg As New DataSchemaError((GenerateModelStatistics & "=") + valueString & " was not valid. It can be true or false", ErrorSeverity.[Error])
                    errorsContainer.Add(invalidArg, ErrorManager.BuildCategory)
                    Exit Sub
                End If
            End If
    
            If SortByMap.Count = 0 Then
                '' haven't populated the map yet
                SortByMap.Add("none", SortBy.None)
                SortByMap.Add("name", SortBy.Name)
                SortByMap.Add("value", SortBy.Value)
            End If
    
            ' Only worry about sort order if the user requested 
            ' that we generate model statistics. 
            If generateModelStatistics__1 Then
                ' see if the user provided the sort option and 
                ' if so, what value was provided. 
                arguments.TryGetValue(SortModelStatisticsBy, valueString)
                If String.IsNullOrWhiteSpace(valueString) = False Then
                    Dim localSortBy As SortBy
                    If SortByMap.TryGetValue(valueString, localSortBy) Then
                        _sortBy = localSortBy
                    Else
                        ' The value was not valid from the end user 
                        Dim invalidArg As New DataSchemaError((SortModelStatisticsBy & "=") + valueString & " was not valid. It can be none, name, or value", ErrorSeverity.[Error])
                        errorsContainer.Add(invalidArg, ErrorManager.BuildCategory)
                    End If
                End If
            End If
    

    Relevante Typen und Methoden: ErrorManager und DataSchemaError.

So definieren Sie den Text der SummarizeModelInfo-Methode

  • Fügen Sie dem Text der SummarizeModelInfo-Methode den folgenden Code hinzu:

                // use a Dictionary to accumulate the information
                // that will later be output.
                var info = new Dictionary<string, string>();
    
                // Two things of interest: the database schema
                // provider for the model, and the language id and
                // case sensitivity of the collation of that
                // model
                info.Add("DSP", model.DatabaseSchemaProvider.GetType().Name);
                info.Add("Collation", string.Format("{0}({1})", model.Collation.Lcid, model.Collation.CaseSensitive ? "CS" : "CI"));
    
                // Output the accumulated information and add it to 
                // the XML.
                OutputResult("Basic model info", info, xContainer, errorsContainer);
    
        ' use a Dictionary to accumulate the information
        ' that will later be output.
        Dim info = New Dictionary(Of String, String)()
    
        ' Two things of interest: the database schema
        ' provider for the model, and the language id and
        ' case sensitivity of the collation of that
        ' model
        info.Add("DSP", model.DatabaseSchemaProvider.[GetType]().Name)
        info.Add("Collation", String.Format("{0}({1})", model.Collation.Lcid, If(model.Collation.CaseSensitive, "CS", "CI")))
    
        ' Output the accumulated information and add it to 
        ' the XML.
        OutputResult("Basic model info", info, xContainer, errorsContainer)
    

    Wichtige Typen und Member: DataSchemaModel, ModelStore, DatabaseSchemaProvider und ModelCollation.

So fügen Sie den Text der Summarize-Methode hinzu

  • Fügen Sie dem Text der Summarize-Methode den folgenden Code hinzu:

                // Use a Dictionary to keep all summarized information
                var statistics = new Dictionary<string, int>();
    
                // For each element in the provided list,
                // count items based on the specified grouping
                var groups =
                    from item in set
                    group item by groupValue(item) into g
                    select new { g.Key, Count = g.Count() };
    
                // order the groups as requested by the user
                if (this._sortBy == SortBy.Name)
                {
                    groups = groups.OrderBy(group => group.Key);
                }
                else if (this._sortBy == SortBy.Value)
                {
                    groups = groups.OrderBy(group => group.Count);
                }
    
                // build the Dictionary of accumulated statistics
                // that will be passed along to the OutputResult method.
                foreach (var item in groups)
                {
                    statistics.Add(item.Key, item.Count);
                }
    
                statistics.Add("subtotal", set.Count);
                statistics.Add("total items", groups.Count());
    
                // output the results, and build up the XML
                OutputResult(category, statistics, xContainer, errorsContainer);
    
        ' Use a Dictionary to keep all summarized information
        Dim statistics = New Dictionary(Of String, Integer)()
    
        ' For each element in the provided list,
        ' count items based on the specified grouping
        Dim groups = From item In [set] _ 
            Group item By groupValue(item)Intog _ 
            Select New ()
    
        ' order the groups as requested by the user
        If Me._sortBy = SortBy.Name Then
            groups = groups.OrderBy(Function(group) group.Key)
        ElseIf Me._sortBy = SortBy.Value Then
            groups = groups.OrderBy(Function(group) group.Count)
        End If
    
        ' build the Dictionary of accumulated statistics
        ' that will be passed along to the OutputResult method.
        For Each item In groups
            statistics.Add(item.Key, item.Count)
        Next
    
        statistics.Add("subtotal", [set].Count)
        statistics.Add("total items", groups.Count())
    
        ' output the results, and build up the XML
        OutputResult(category, statistics, xContainer, errorsContainer)
    

    Auch hier enthalten die Codekommentare wichtige Informationen.

So fügen Sie den Text der SurveyRelationships-Methode hinzu

  • Fügen Sie dem Text der SurveyRelationships-Methode den folgenden Code hinzu:

                // get a list that contains all elements in the model
                var elements = model.GetElements(typeof(IModelElement), ModelElementQueryFilter.All);
                // We are interested in all relationships that
                // reference each element.
                var entries =
                    from element in elements
                    from entry in element.GetReferencedRelationshipEntries()
                    select entry;
    
                // initialize our counting buckets
                var single = 0;
                var many = 0;
                var composing = 0;
                var hierachical = 0;
                var peer = 0;
                var reverse = 0;
    
                // process each relationship, adding to the 
                // appropriate bucket for style and type.
                foreach (var entry in entries)
                {
                    switch (entry.RelationshipClass.ModelRelationshipCardinalityStyle)
                    {
                        case ModelRelationshipCardinalityStyle.Many:
                            ++many;
                            break;
                        case ModelRelationshipCardinalityStyle.Single:
                            ++single;
                            break;
                        default:
                            break;
                    }
    
                    switch (entry.RelationshipClass.ModelRelationshipType)
                    {
                        case ModelRelationshipType.Composing:
                            ++composing;
                            break;
                        case ModelRelationshipType.Hierarchical:
                            ++hierachical;
                            break;
                        case ModelRelationshipType.Peer:
                            ++peer;
                            break;
                        case ModelRelationshipType.Reverse:
                            // We count these, but reverse relationships
                            // are not actually stored*, so the count
                            // will always be zero.
                            // * - reverse relationships are generated
                            // dynamically when they are accessed.
                            ++reverse;
                            break;
                        default:
                            break;
                    }
                }
    
                // build a dictionary of data to pass along
                // to the OutputResult method.
                var stat = new Dictionary<string, int>();
                stat.Add("Multiple", many);
                stat.Add("Single", single);
                stat.Add("Composing", composing);
                stat.Add("Hierarchical", hierachical);
                stat.Add("Peer", peer);
                // As noted, no need to output the count of reverse
                // relationships as it will always be zero.
                //stat.Add("Reverse", reverse);
                stat.Add("subtotal", entries.Count());
    
                OutputResult("Relationships", stat, xContainer, errorsContainer);
    
            ' get a list that contains all elements in the model 
            Dim elements = model.GetElements(GetType(IModelElement), ModelElementQueryFilter.All)
            ' We are interested in all relationships that 
            ' reference each element. 
            Dim entries = From element In elements _
                From entry In element.GetReferencedRelationshipEntries() _
                Select entry
    
            ' initialize our counting buckets 
            Dim [single] = 0
            Dim many = 0
            Dim composing = 0
            Dim hierachical = 0
            Dim peer = 0
            Dim reverse = 0
    
            ' process each relationship, adding to the 
            ' appropriate bucket for style and type. 
            For Each entry In entries
                Select Case entry.RelationshipClass.ModelRelationshipCardinalityStyle
                    Case ModelRelationshipCardinalityStyle.Many
                        many += 1
                        Exit Select
                    Case ModelRelationshipCardinalityStyle.[Single]
                        [single] += 1
                        Exit Select
                    Case Else
                        Exit Select
                End Select
    
                Select Case entry.RelationshipClass.ModelRelationshipType
                    Case ModelRelationshipType.Composing
                        composing += 1
                        Exit Select
                    Case ModelRelationshipType.Hierarchical
                        hierachical += 1
                        Exit Select
                    Case ModelRelationshipType.Peer
                        peer += 1
                        Exit Select
                    Case ModelRelationshipType.Reverse
                        ' We count these, but reverse relationships 
                        ' are not actually stored*, so the count 
                        ' will always be zero. 
                        ' * - reverse relationships are generated 
                        ' dynamically when they are accessed. 
                        reverse += 1
                        Exit Select
                    Case Else
                        Exit Select
                End Select
            Next
    
            ' build a dictionary of data to pass along 
            ' to the OutputResult method. 
            Dim stat = New Dictionary(Of String, Integer)()
            stat.Add("Multiple", many)
            stat.Add("Single", [single])
            stat.Add("Composing", composing)
            stat.Add("Hierarchical", hierachical)
            stat.Add("Peer", peer)
            ' As noted, no need to output the count of reverse 
            ' relationships as it will always be zero. 
            'stat.Add("Reverse", reverse); 
            stat.Add("subtotal", entries.Count())
    
        OutputResult("Relationships", stat, xContainer, errorsContainer)
    

    In den Codekommentaren werden die wichtigsten Aspekte dieser Methode erläutert. Relevante Typen und Methoden: DataSchemaModel, ModelStore, GetElements und ModelElement.

So fügen Sie den Text der OutputResult-Methode hinzu

  • Fügen Sie der OutputResult-Methode den folgenden Text hinzu:

                var maxLen = statistics.Max(stat => stat.Key.Length) + 2;
                var format = string.Format("{{0, {0}}}: {{1}}", maxLen);
    
                List<DataSchemaError> args = new List<DataSchemaError>();
                args.Add(new DataSchemaError(category, ErrorSeverity.Message));
                args.Add(new DataSchemaError("-----------------", ErrorSeverity.Message));
    
                // Remove any blank spaces from the category name
                var xCategory = new XElement(category.Replace(" ", ""));
                xContainer.Add(xCategory);
    
                foreach (var item in statistics)
                {
                    //Console.WriteLine(format, item.Key, item.Value);
                    var entry = string.Format(format, item.Key, item.Value);
                    args.Add(new DataSchemaError(entry, ErrorSeverity.Message));
                    // Replace any blank spaces in the element key with
                    // underscores.
                    xCategory.Add(new XElement(item.Key.Replace(' ', '_'), item.Value));
                }
                args.Add(new DataSchemaError(" ", ErrorSeverity.Message));
                errorsContainer.Add(args, ErrorManager.BuildCategory);
    
        Dim maxLen = statistics.Max(Function(stat) stat.Key.Length) + 2
        Dim format = String.Format("{{0, {0}}}: {{1}}", maxLen)
    
        Dim args As New List(Of DataSchemaError)()
        args.Add(New DataSchemaError(category, ErrorSeverity.Message))
        args.Add(New DataSchemaError("-----------------", ErrorSeverity.Message))
    
        ' Remove any blank spaces from the category name
        Dim xCategory = New XElement(category.Replace(" ", ""))
        xContainer.Add(xCategory)
    
        For Each item In statistics
            'Console.WriteLine(format, item.Key, item.Value);
            Dim entry = String.Format(format, item.Key, item.Value)
            args.Add(New DataSchemaError(entry, ErrorSeverity.Message))
            ' Replace any blank spaces in the element key with
            ' underscores.
            xCategory.Add(New XElement(item.Key.Replace(" "c, "_"c), item.Value))
        Next
        args.Add(New DataSchemaError(" ", ErrorSeverity.Message))
        errorsContainer.Add(args, ErrorManager.BuildCategory)
    
  • Speichern Sie die Änderungen in der Datei "Class1.cs".

    Im nächsten Schritt wird die Klassenbibliothek erstellt.

So signieren und erstellen Sie die Assembly

  1. Klicken Sie im Menü Projekt auf Eigenschaften von MyBuildContributor.

  2. Klicken Sie auf die Registerkarte Signierung.

  3. Klicken Sie auf Assembly signieren.

  4. Klicken Sie im Feld Schlüsseldatei mit starkem Namen auswählen auf <Neu>.

  5. Geben Sie im Dialogfeld Schlüssel für einen starken Namen erstellen unter Schlüsseldateiname den Namen MyRefKey ein.

  6. (optional) Sie können für die Schlüsseldatei mit starkem Namen ein Kennwort angeben.

  7. Klicken Sie auf OK.

  8. Klicken Sie im Menü Datei auf Alle speichern.

  9. Klicken Sie im Menü Erstellen auf Projektmappe erstellen.

    Danach müssen Sie die Assembly installieren und registrieren, damit sie geladen wird, wenn Sie Datenbankprojekte erstellen.

Installieren eines Buildcontributors

Führen Sie zum Installieren eines Buildcontributors die folgenden Aufgaben aus:

  • Kopieren der Assembly und der zugeordneten PDB-Datei in den Ordner "Extensions"

  • Erstellen einer Datei vom Typ "Extensions.xml", um den Buildcontributor zu registrieren, damit er bei der Erstellung von Datenbankprojekten geladen wird

So installieren Sie die Assembly "MyBuildContributor"

  1. Erstellen Sie im Verzeichnis "%Programme%\Microsoft Visual Studio 10.0\VSTSDB\Extensions" den Ordner MyExtensions.

  2. Kopieren Sie die signierte Assembly ("MyBuildContributor.dll") in das Verzeichnis "%Programme%\Microsoft Visual Studio 10.0\VSTSDB\Extensions\MyExtensions".

    Tipp

    Es empfiehlt sich, die XML-Dateien nicht direkt in das Verzeichnis "%Programme%\Microsoft Visual Studio 10.0\VSTSDB\Extensions" zu kopieren. Wenn Sie stattdessen einen Unterordner verwenden, vermeiden Sie versehentliche Änderungen an den anderen Dateien, die mit Visual Studio Premium bereitgestellt werden.

    Im nächsten Schritt muss die Assembly (eine Art Funktionserweiterung) registriert werden, damit sie in Visual Studio Premium angezeigt wird.

So registrieren Sie die Assembly "MyBuildContributor"

  1. Klicken Sie im Menü Ansicht auf Weitere Fenster, und klicken Sie anschließend auf Befehlsfenster, um das Befehlsfenster zu öffnen.

  2. Geben Sie im Befehlsfenster folgenden Code ein. Ersetzen Sie FilePath durch den Pfad und Dateinamen der kompilierten DLL-Datei. Schließen Sie Pfad und Dateiname in Anführungszeichen ein.

    Tipp

    Der Pfad der kompilierten DLL-Datei lautet standardmäßig Projektmappenpfad\bin\Debug oder Projektmappenpfad\bin\Release.

    ? System.Reflection.Assembly.LoadFrom("FilePath").FullName
    
    ? System.Reflection.Assembly.LoadFrom(@"FilePath").FullName
    
  3. Drücken Sie die EINGABETASTE.

  4. Kopieren Sie die resultierende Zeile in die Zwischenablage. Die Zeile sollte der folgenden entsprechen:

    "MyBuildContributor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=nnnnnnnnnnnnnnnn"
    
  5. Öffnen Sie einen Text-Editor, z. B. Editor.

    Wichtig

    Öffnen Sie den Editor unter Windows Vista und unter Microsoft Windows Server 2008 als Administrator, damit Sie die Datei im Ordner Programme speichern können.

  6. Geben Sie die folgenden Informationen an. Sie können die Informationen einfügen, die Sie in Schritt 4 kopiert haben. Geben Sie einen eigenen Assemblynamen, ein eigenes öffentliches Schlüsseltoken sowie einen eigenen Erweiterungstyp an:

    <?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="MyBuildContributor.ModelStatistics" 
    assembly="MyBuildContributor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=<enter key here>" enabled="true" />
    </extensions>
    

    Diese XML-Datei dient zum Registrieren der Klasse, die von BuildContributor erbt.

  7. Speichern Sie die Datei im Verzeichnis "%Programme%\Microsoft Visual Studio 10.0\VSTSDB\Extensions\MyExtensions" als "MyBuildContributor.extensions.xml".

    Tipp

    Überprüfen Sie in Dateityp, ob Sie Alle Dateien angegeben haben. Andernfalls ignoriert Editor die Erweiterung und speichert die Dateien mit der Erweiterung .txt.

  8. Schließen Sie Visual Studio.

    Im nächsten Schritt wird ein Datenbankprojekt erstellt, um den Contributor zu testen.

Testen des Buildcontributors

Führen Sie zum Testen des Buildcontributors die folgenden Aufgaben aus:

  • Hinzufügen von Eigenschaften zur DBPROJ-Datei, die Sie erstellen möchten

  • Erstellen des Datenbankprojekts mithilfe von MSBuild und Angeben der entsprechenden Parameter

Hinzufügen von Eigenschaften zur Datenbankprojektdatei (DBPROJ-Datei)

Da von diesem Buildcontributor Befehlszeilenparameter aus MSBuild akzeptiert werden, müssen Sie das Datenbankprojekt ändern, um Benutzern das Übergeben dieser Parameter mithilfe von MSBuild zu ermöglichen. Dazu haben Sie zwei Möglichkeiten: Sie können die DBPROJ-Datei manuell ändern und die erforderlichen Argumente hinzufügen. Diese Vorgehensweise kann verwendet werden, wenn Sie lediglich das Datenbankprojekt mit MSBuild erstellen. Fügen Sie bei Verwendung dieser Option der DBPROJ-Datei die folgenden Anweisungen hinzu (zwischen dem letzten Knoten vom Typ "</ItemGroup>" und dem letzten Knoten vom Typ "</Project>"):

  <ItemGroup>
    <BuildContributorArgument Include="OutDir=$(OutDir)" />
    <BuildContributorArgument Include="GenerateModelStatistics=$(GenerateModelStatistics)" />
    <BuildContributorArgument Include="SortModelStatisticsBy=$(SortModelStatisticsBy)" />
  </ItemGroup>

Einfacher ist die folgende Methode: Laden Sie das Datenbankprojekt in Visual Studio, erstellen Sie Datenbankprojekt einmal, und beenden Sie Visual Studio anschließend. Speichern Sie beim Beenden die Änderungen am Projekt. Wenn Sie diese Methode verwenden, werden die zusätzlichen Argumente automatisch der Datenbankprojektdatei (DBPROJ-Datei) hinzugefügt.

Nach dem Ausführen einer dieser Methoden können die Parameter für Befehlszeilenbuilds mithilfe von MSBuild übergeben werden.

So fügen Sie der Datenbankprojektdatei Eigenschaften hinzu

  1. Öffnen Sie das Datenbankprojekt in Visual Studio. Weitere Informationen finden Sie unter Gewusst wie: Öffnen eines Datenbank- oder Serverprojekts.

  2. Erstellen Sie das Datenbankprojekt. Weitere Informationen finden Sie unter Gewusst wie: Erstellen eines Datenbankprojekts zum Generieren einer kompilierten Schemadatei (.dbschema).

  3. Schließen Sie Visual Studio. Speichern Sie die Lösung und das Projekt, wenn Sie dazu aufgefordert werden.

    Danach können Sie das Datenbankprojekt mithilfe von MSBuild erstellen und Argumente angeben, die dem Buildcontributor das Generieren der Modellstatistik ermöglichen.

Erstellen des Datenbankprojekts

So können Sie das Datenbankprojekt mithilfe von MSBuild erneut erstellen und die Statistik generieren

  1. Öffnen Sie eine Visual Studio-Eingabeaufforderung. Klicken Sie im Startmenü auf Alle Programme, auf Microsoft Visual Studio 2010, auf Visual Studio Tools und anschließend auf Visual Studio-Eingabeaufforderung (2010).

  2. Navigieren Sie an der Eingabeaufforderung zu dem Ordner, der das Datenbankprojekt enthält.

  3. Geben Sie an der Eingabeaufforderung die folgende Befehlszeile ein:

    MSBuild /t:Rebuild MyDatabaseProject.dbproj /p:GenerateModelStatistics=true /p:SortModelStatisticsBy=name /p:OutDir=.\
    

    Ersetzen Sie MyDatabaseProject durch den Namen des Datenbankprojekts, das Sie erstellen möchten. Wenn das Projekt seit der letzten Erstellung geändert wurde, können Sie anstelle von "/t:Rebuild" die Option "/t:Build" verwenden.

    Die angezeigte Ausgabe ähnelt dem folgenden Beispiel:

Microsoft (R) Build Engine Version 4.0.20817.0
[Microsoft .NET Framework, Version 4.0.20817.0]
Copyright (C) Microsoft Corporation 2007. All rights reserved.

Build started 8/19/2009 2:46:04 PM.
Project "C:\Users\UserName\Documents\Visual Studio 2010\Projects\MyDatabaseProject\MyDatabaseProject\MyDatabaseProject.dbproj" on node 1 (Rebuild target(s)).
CoreClean:
  Deleting file "c:\users\UserName\documents\visual studio 2010\Projects\MyDatabaseProject\MyDatabaseProject\sql\debug\MyDatabaseProject_Script.PreDeployment.sql".
  Deleting file "c:\users\UserName\documents\visual studio 2010\Projects\MyDatabaseProject\MyDatabaseProject\sql\debug\MyDatabaseProject_Script.PostDeployment.sql".
  Deleting file "c:\users\UserName\documents\visual studio 2010\Projects\MyDatabaseProject\MyDatabaseProject\sql\debug\MyDatabaseProject_Database.sqlsettings".
  Deleting file "c:\users\UserName\documents\visual studio 2010\Projects\MyDatabaseProject\MyDatabaseProject\sql\debug\MyDatabaseProject_Database.sqldeployment".
  Deleting file "c:\users\UserName\documents\visual studio 2010\Projects\MyDatabaseProject\MyDatabaseProject\sql\debug\MyDatabaseProject_Database.sqlcmdvars".
  Deleting file "c:\users\UserName\documents\visual studio 2010\Projects\MyDatabaseProject\MyDatabaseProject\sql\debug\MyDatabaseProject.deploymanifest".
  Deleting file "c:\users\UserName\documents\visual studio 2010\Projects\MyDatabaseProject\MyDatabaseProject\sql\debug\MyDatabaseProject.dbschema".
  Deleting file "c:\users\UserName\documents\visual studio 2010\Projects\MyDatabaseProject\MyDatabaseProject\obj\Debug\MyDatabaseProject.dbschema".
DspBuild:
  Creating a model to represent the project...
  Loading project files...
  Building the project model and resolving object interdependencies...
  Validating the project model...

Model Statistics:
=================

Basic model info
-----------------
        DSP: Sql100DatabaseSchemaProvider
  Collation: 1033(CI)

Internal Elements
-----------------
                    ISql100DatabaseOptions: 1
                          ISql100Filegroup: 1
                      ISql100FullTextIndex: 3
                              ISql100Index: 95
  ISql100MultiStatementTableValuedFunction: 1
               ISql100PrimaryKeyConstraint: 71
                          ISql100Procedure: 10
                     ISql100ScalarFunction: 10
                       ISql100SimpleColumn: 481
                ISql100SubroutineParameter: 41
                              ISql100Table: 71
                   ISql100UniqueConstraint: 1
                               ISql100View: 20
                           ISql100XmlIndex: 8
                     ISql90CheckConstraint: 89
                      ISql90ComputedColumn: 302
                  ISql90DatabaseDdlTrigger: 1
                   ISql90DefaultConstraint: 152
                          ISql90DmlTrigger: 10
                                ISql90File: 3
                ISql90ForeignKeyConstraint: 90
                     ISql90FullTextCatalog: 1
                               ISql90Route: 1
                              ISql90Schema: 5
           ISql90TriggerEventTypeSpecifier: 125
                       ISql90TypeSpecifier: 524
                 ISql90UserDefinedDataType: 6
                 ISql90XmlSchemaCollection: 6
                    ISql90XmlTypeSpecifier: 8
                   ISqlDynamicColumnSource: 5
                      ISqlExtendedProperty: 1161
          ISqlFullTextIndexColumnSpecifier: 4
            ISqlIndexedColumnSpecification: 220
          ISqlScriptFunctionImplementation: 11
                                  subtotal: 3538
                               total items: 34

External Elements
-----------------
           ISql100Filegroup: 1
               ISql100Queue: 3
             ISql100Service: 3
             ISql90Assembly: 1
       ISql90AssemblySource: 1
            ISql90ClrMethod: 151
   ISql90ClrMethodParameter: 138
          ISql90ClrProperty: 16
             ISql90Contract: 6
             ISql90Endpoint: 5
          ISql90MessageType: 14
                 ISql90Role: 10
               ISql90Schema: 13
        ISql90TypeSpecifier: 305
                 ISql90User: 4
  ISql90UserDefinedDataType: 1
      ISql90UserDefinedType: 3
            ISqlBuiltInType: 32
             ISqlServerRole: 9
                   subtotal: 716
                total items: 19

Relationships
-----------------
      Multiple: 3002
        Single: 4017
     Composing: 2332
  Hierarchical: 1812
          Peer: 2875
      subtotal: 7019

Annotations
-----------------
                         ExternalPropertyAnnotation: 1475
                        ExternalReferenceAnnotation: 187
                           ExternalSourceAnnotation: 2
                         ModuleInvocationAnnotation: 20
                      ParameterOrVariableAnnotation: 68
  ResolveTimeVerifiedDanglingRelationshipAnnotation: 119
                      SqlInlineConstraintAnnotation: 1
                SqlModelBuilderResolvableAnnotation: 7825
                        SysCommentsObjectAnnotation: 52
                                           subtotal: 9749
                                        total items: 9

Custom Data
-----------------
          AnsiNulls: 1
   ClrTypesDbSchema: 1
  CompatibilityMode: 1
    ModelCapability: 1
        Permissions: 1
   QuotedIdentifier: 1
           subtotal: 6
        total items: 6

Result was saved to .\ModelStatistics.xml

  Writing model to MyDatabaseProject.dbschema...
CopyFilesToOutputDirectory:
  Copying file from "obj\Debug\MyDatabaseProject.dbschema" to ".\sql\debug\MyDatabaseProject.dbschema".
  MyDatabaseProject -> C:\Users\UserName\Documents\Visual Studio 2010\Projects\MyDatabaseProject\MyDatabaseProject\sql\debug\MyDatabaseProject.dbschema
Done Building Project "C:\Users\UserName\Documents\Visual Studio 2010\Projects\MyDatabaseProject\MyDatabaseProject\MyDatabaseProject.dbproj" (Rebuild target(s)).

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:11.20
  1. Öffnen Sie "ModelStatistics.xml", und untersuchen Sie den Inhalt.

    Die gemeldeten Ergebnisse werden auch in der XML-Datei beibehalten.

Nächste Schritte

Sie können weitere Tools erstellen, um die XML-Ausgabedatei zu verarbeiten. Dies ist lediglich ein einzelnes Beispiel für einen Buildcontributor. Beispielsweise können Sie einen Buildcontributor erstellen, um als Teil des Builds eine Datenwörterbuchdatei auszugeben.

Siehe auch

Konzepte

Erweitern der Datenbankfunktionen von Visual Studio

Weitere Ressourcen

Anpassen von Datenbankbuild und -bereitstellung mithilfe von Build- und Bereitstellungsmitwirkenden

Exemplarische Vorgehensweise: Erweitern der Bereitstellung von Datenbankprojekten zum Analysieren des Bereitstellungsplans