Réaliser un composant source pour interroger une Web API : Le déploiement du composant

Cet article fait partie de la série décrite dans cet article.
Pour que le composant soit disponible dans SSDT, il doit figurer dans le répertoire d’installation de SQL Server, pour la version 2012 :
C:\Program Files (x86)\Microsoft SQL Server\110\DTS\PipelineComponents
Je copie donc l’assembly compilée dans ce répertoire.
Dans SSDT, l’alimentation de la toolbox a été simplifiée rapport à la version 2008. Il n’est plus nécessaire de manipuler une interface de choix des composants de la toolbox, ils sont tous présents par défaut. Néanmoins, si votre SSDT était déjà ouvert lorsque vous avez déposé le composant il vous faut rafraichir (click droit Rafraichir) la toolbox SSIS pour le voir apparaitre.
Le composant dans la SSIS Toolbox
A ce stade, vous pouvez utiliser en mode conception le composant MAIS pas à l’exécution.
Lors de l’exécution, ce sont les composants présents dans le GAC qui sont instanciés. Il vous faut donc également déployer dans le GAC.
Mon assembly est signée, je peux donc exécuter l’instruction : Gacutil -i WebApiSourceComponent.dll
NB : Si vous avez besoin de mettre à jour un composant, vous devez fermer votre SSDT. Celui-ci prend des verrous sur les assemblies des composants.
Ces étapes sont également à déroulées sur le serveur sur lequel vous déploierez les packages après la phase de développement (Serveur de test ou de production SQL par exemple).

PS : J’ai rencontré une erreur mal maîtrisée lors du déploiement de mon composant. Lors de l’exécution de celui-ci depuis SSDT, j’avais une exception m’indiquant que la méthode PerformUpgrade échouait. Sachant qu’aucune upgrade n’était nécessaire, j’ai surchargé dans mon composant cette méthode :

public override void PerformUpgrade(int pipelineVersion){}

Mon composant est désormais pleinement fonctionnel et peut donc être inséré dans un data flow.

Réaliser un composant source pour interroger une Web API : L’implémentation de son comportement lors de l’exécution

Cet article fait partie de la série décrite dans cet article.
Lors de la phase d’exécution, mon composant devra alimenter le flux sortant avec des lignes créées à partir d’une liste d’objets récupérés en appelant la Web API décrite dans cet article.
La méthode principale à implémenter sur une source est la méthode PrimeOutput qui permet d’alimenter un flux avec des lignes.
C’est dans cette méthode que nous allons interroger la Web API et créer une ligne pour chaque instance retournée. Le code de la méthode est ici fortement inspiré de celui qui m’avait permis d’interroger une Web API depuis un composant script.
A noter l’utilisation pour ajouter des lignes du premier élément de la collection de buffer qui représente le buffer du premier Output.



        public override void PrimeOutput(int outputs, int[] outputIDs, PipelineBuffer[] buffers)
        {
            var httpConnectionManager = ComponentMetaData.RuntimeConnectionCollection[0].ConnectionManager;

            if (httpConnectionManager == null) return;

            PipelineBuffer buffer = buffers[0];
            string webApiRoute = ComponentMetaData.CustomPropertyCollection[0].Value;

            var client = new HttpClient { BaseAddress = new Uri(httpConnectionManager.ConnectionString) };

            // Appel du Get de la Web API
            HttpResponseMessage response = client.GetAsync(webApiRoute).Result;
            if (response.IsSuccessStatusCode)
            {
                var parutions = response.Content.ReadAsAsync<IEnumerable>().Result;
                foreach (var parution in parutions)
                {
                    buffer.AddRow();
                    buffer[0] = parution.Codebarre;
                    buffer[1] = parution.Codification;
                    buffer[2] = parution.Libelle;
                    buffer[3] = parution.Numero;
                }
                buffer.SetEndOfRowset();
            }
        }

La phase d’exécution ne fait pas uniquement appel à la méthode PrimeOutput. Vous pouvez en surchargeant les bonnes méthodes :

  • initialiser votre composant (surcharger la méthode initialize)
  • nettoyer (méthode cleanup)
  • Etablir des connections (AcquireConnection)

Suite : Le déploiement du composant

Réaliser un composant source pour interroger une Web API : Fournir une interface de configuration

Cet article fait partie de la série décrite dans cet article.

Je dois également fournir une interface graphique de configuration de mon composant pour permettre la configuration des métadonnées exposées de manière guidées.
Je peux également vouloir :

  • Appliquer des vérifications fonctionnelles au moment du design
  • Proposer des assistants d’aide à la saisie des paramètres utilisant des données externes
  • Disposer d’une interface plus explicite

Pour mettre en place cette interface, je dois créer une classe implémentant l’interface IDtsComponentUI.
J’ajoute une telle classe à mon assembly (je pourrais très bien développer l’interface graphique dans une autre assembly pour dissocier les mises en production).
Pour l’implémentation de cette classe, je dois ajouter une référence vers l’assembly :

  • Microsoft.SqlServer.Dts.Design
  • Microsoft.SqlServer.ManagedDTS

Cette classe doit disposer des using suivants :

  • using Microsoft.SqlServer.Dts.Runtime;
  • using Microsoft.SqlServer.Dts.Pipeline.Design;
  • using Microsoft.SqlServer.Dts.Pipeline.Wrapper;

L’implémentation de cette interface induit l’implémentation des méthodes suivantes appelées par SSDT sur mon composant :

  • Delete(System.Windows.Forms.IWin32Window parentWindow) : Appelé lorsque le composant est supprimé d’un data-flow dans SSDT
  • Edit(System.Windows.Forms.IWin32Window parentWindow, Variables variables, Connections connections) : Appelé lorsque le composant est édité dans SSDT (click droit Edit)
  • Help(System.Windows.Forms.IWin32Window parentWindow) : Appelé lorsque l’aide d’un composant est consulté dans SSDT
  • New(System.Windows.Forms.IWin32Window parentWindow) : Appelé lorsque le composant est ajouté à un data-flow dans SSDT
  • Initialize(IDTSComponentMetaData100 dtsComponentMetadata, IServiceProvider serviceProvider) : Appelé avant chaque appel au new ou edit d’un composant

Dans mon cas, je vais implémenter les méthodes Initialize et Edit. Ce code est très inspiré de l’article MSDN décrivant les interfaces de configurations des composants de Data-flow SSIS.
La méthode Initialize me permet de conserver une référence sur les métadonnées du composant puisqu’elle me les passe en paramètre. Lorsque je modifierai cet objet, je modifierai effectivement la configuration de mon composant. Pour conserver une référence, je vais donc ajouter en donnée membre un objet du même type et l’affecter dans l’initialize.
La méthode Edit me permettra d’ouvrir une Form qui présentera la liste des connections candidates et les champs nécessaire à la configuration de mon composant.


using System;
using System.Windows.Forms;
using Microsoft.SqlServer.Dts.Runtime;
using Microsoft.SqlServer.Dts.Pipeline.Design;
using Microsoft.SqlServer.Dts.Pipeline.Wrapper;

namespace WebApiSourceComponentUI
{
    public class WebApiSourceComponentUI : IDtsComponentUI
    {
        private IDTSComponentMetaData100 _metadata;

        public void Initialize(IDTSComponentMetaData100 dtsComponentMetadata, IServiceProvider serviceProvider)
        {
            _metadata = dtsComponentMetadata;
        }

        public bool Edit(IWin32Window parentWindow, Variables variables, Connections connections)
        {            
            // Create and display the form for the user interface.
            var componentEditor = new WebApiSourceComponentUIForm(connections,  _metadata);

            DialogResult result = componentEditor.ShowDialog(parentWindow);

            if (result == DialogResult.OK)
                return true;

            return false;
        }

        public void Delete(IWin32Window parentWindow){}
        public void Help(IWin32Window parentWindow){}     
        public void New(IWin32Window parentWindow){}
    }
}

Form de configuration du composant

Le code de cette forme est simplement constitué du constructeur qui initialise à partir de la collection de connexion le contenu de la liste déroulante et d’un méthode validation affectant des valeurs aux métadonnées du composant.
A noter l’utilisation de la méthode SetComponentProperty pour affecter une valeur à une propriété custom qui doit être appelée sur une instance « runtime » des métadonnées. Voici le code :


using System;
using System.Collections.Generic;
using System.Windows.Forms;
using Microsoft.SqlServer.Dts.Pipeline.Wrapper;
using Microsoft.SqlServer.Dts.Runtime;

namespace WebApiSourceComponentUI
{
    public partial class WebApiSourceComponentUIForm : Form
    {
        private Connections _connections;
        private IDTSComponentMetaData100 _metaData;

        public WebApiSourceComponentUIForm(Connections connections, IDTSComponentMetaData100 metadata)
        {
            _connections = connections;
            _metaData = metadata;

            InitializeComponent();

            var avalaibleHttpConnections = new List<KeyValuePair>();
            foreach (var connection in _connections)
            {
                if(connection.CreationName == "HTTP")
                    avalaibleHttpConnections.Add(new KeyValuePair(connection.ID,connection.Name));
            }
            cbHttpConnectionManager.ValueMember = "Key";
            cbHttpConnectionManager.DisplayMember = "Value";
            cbHttpConnectionManager.DataSource = avalaibleHttpConnections;
        }



        private void btnOk_Click(object sender, EventArgs e)
        {
            CManagedComponentWrapper designTimeInstance = _metaData.Instantiate();
            designTimeInstance.SetComponentProperty("Web Api Route", txtWebApiRoute.Text);

            _metaData.RuntimeConnectionCollection[0].ConnectionManagerID = cbHttpConnectionManager.SelectedValue.ToString();
        }
    }
}

Pour associer cette interface graphique à mon composant, je vais légèrement modifier le décorateur de mon composant pour ajouter la propriété UITypeName. Je dois affecter à cette propriété le nom fort de la classe à utiliser comme interface de configuration, par conséquent, il faut signer numériquement l’assembly avant de pouvoir écrire cette ligne :


 [DtsPipelineComponent(DisplayName = "Web API Source", ComponentType = ComponentType.SourceAdapter, CurrentVersion = 1, 
        UITypeName = "WebApiSourceComponentUI.WebApiSourceComponentUI,WebApiSourceComponent, Version=1.0.0.0, Culture=neutral, PublicKeyToken=fcae8f24ab665826")]

Suite : L’implémentation de son comportement lors de l’exécution

Réaliser un composant source pour interroger une Web API : La gestion des métadonnées du composant

Cet article fait partie de la série décrite dans cet article.

Pour la phase de configuration, il s’agit de déclarer dans une méthode, la liste de métadonnées à configurer. Ces métadonnées sont essentiellement constituées :
De propriétés communes à tout élément de data-flow

  • ContactInfo : Pour contacter l’éditeur du composant
  • Description
  • Version

D’une liste d’entrée contenant des colonnes d’entrée (vide dans le cas d’une source de données).

  • Accessible via InputCollection

D’une liste de sortie contenant des colonnes de sortie (qui ici sera statique mais pourrait être dynamique)

  • Accessible via OutputCollection

D’une liste de gestionnaire de connection que mon composant est capable d’exploiter.

  • Accessible via RuntimeConnectionCollection

D’une liste de propriétés custom, définit arbitrairement par des couples clé – valeur.

  • Accessible via CustomPropertyCollection

Pour exposer les propriétés de notre composant il faut alimenter la propriété ComponentMetadata dans la méthode ProvideComponentProperties. Dans mon cas, je souhaite disposer :

  • D’une sortie contenant 4 colonnes (Codebarre, Codification, Libelle, Numero)
  • D’un gestionnaire de connection de type HTTPConnection Manager
  • D’une propriété custom contenant la route de l’API à appeler
  • Ce que je traduis par le code suivant :

    
    public override void ProvideComponentProperties()
            {
                // Réinitialisation des méta-données du composant
                ComponentMetaData.RuntimeConnectionCollection.RemoveAll();
                ComponentMetaData.CustomPropertyCollection.RemoveAll();
                RemoveAllInputsOutputsAndCustomProperties();
    
                IDTSOutput100 output = ComponentMetaData.OutputCollection.New();
                output.Name = "Flux Web";
    
                IDTSOutputColumn100 colCodebarre = output.OutputColumnCollection.New();
                colCodebarre.Name = "Codebarre";
                colCodebarre.SetDataTypeProperties(DataType.DT_STR, 14, 0, 0, 1252);
    
                IDTSOutputColumn100 colCodification = output.OutputColumnCollection.New();
                colCodification.SetDataTypeProperties(DataType.DT_STR, 4, 0, 0, 1252);
                colCodification.Name = "Codification";
    
                IDTSOutputColumn100 colLibelle = output.OutputColumnCollection.New();
                colLibelle.SetDataTypeProperties(DataType.DT_WSTR, 500, 0, 0, 0);
                colLibelle.Name = "Libelle";
    
                IDTSOutputColumn100 colNumero = output.OutputColumnCollection.New();
                colNumero.SetDataTypeProperties(DataType.DT_I4, 0, 0, 0, 0);
                colNumero.Name = "Numero";
    
                IDTSRuntimeConnection100 connection = ComponentMetaData.RuntimeConnectionCollection.New();
                connection.Name = "Web API Connection manager";
    
                IDTSCustomProperty100 propApiRoute = ComponentMetaData.CustomPropertyCollection.New();
                propApiRoute.Name = "Web Api Route";
            }
    

    Lors du développement de data-flow de SSDT, à chaque modification de configuration de votre composant, la méthode Validate sera appelée pour entériner la configuration. Il s’agit donc dans cette méthode d’appliquer les validations relatives aux métadonnées fournies par le développeur.
    Je vais dans cette méthode, tenter d’établir une relation avec la Web API pour tester son existence et contrôler que les valeurs de retour son bien des instances de la classe attendue :

    
    public override DTSValidationStatus Validate()
            {
                var httpConnectionManager = ComponentMetaData.RuntimeConnectionCollection[0].ConnectionManager;
    
                if (httpConnectionManager != null)
                {
                    var client = new HttpClient { BaseAddress = new Uri(httpConnectionManager.ConnectionString) };
    
                    // Appel du Get de la Web API
                    HttpResponseMessage response = client.GetAsync("api/parutions").Result;
                    if (response.IsSuccessStatusCode)
                    {
                        try
                        {
                            var parutions = response.Content.ReadAsAsync<IEnumerable>().Result;
                            return DTSValidationStatus.VS_ISVALID;
                        }
                        catch (Exception) { }
                    }
                }
                return DTSValidationStatus.VS_ISBROKEN;
            }
    

    Toutes les opérations de configuration réalisées via SSDT font l’objet d’appel de méthode sur votre composant, vous pouvez donc réagir avec un code dédié à chacune de ces opérations. Par exemple, recalculer des méta-données lorsqu’une nouvelle propriété custom est affectée si votre éditeur permet cette opération.

    Suite : Fournir une interface de configuration

Réaliser un composant source pour interroger une Web API : La création du composant

Sur la base de cet article précédent, je vous propose de créer un composant source personnalisé capable d’appeler la Web API décrite dans le même article. L’objectif est de disposer dans ma boite à outil SSIS d’un composant dédié à l’appel d’une Web API ayant les mêmes caractèristiques.
On distingue 2 temps pour un composant SSIS : la phase de configuration et la phase d’exécution.
Lors de la phase de configuration, le composant doit exposer les métadonnées à configurer, qui seront sauvegardées dans le package SSIS. C’est lors de cette phase que nous pouvons proposer une interface de configuration, des validations et des mécanismes de détermination automatique des colonnes par exemple.
Lors de l’exécution, les métadonnées persistées pour le composant lors de la phase de configuration sont restaurées et utilisées par le composant. Lors de cette phase le flux est réellement exécuté.
Je vous propose de distinguer les étapes de la création d’un tel composant :

Création du composant source


Un composant de data-flow SSIS est une classe qui hérite de PipelineComponent. Pour qu’il s’agisse d’une source, il faut que le composant expose au moins une sortie et pas d’entrée. La classe est également décorée de l’attribut DtsPipelineComponent pour indiquer le nom du composant et son type.
Je crée donc un projet C# de type assembly.
Pour étendre cette classe, il faut référencer les assemblies suivantes (présentes dans le GAC lorsque SSIS est installé sur la machine de développement) :
• Microsoft.SqlServer.PipelineHost
• Microsoft.SqlServer.DTSPipelineWrap
• Microsoft.SqlServer.DTSRuntimeWrap

D’autre part, je référence des assemblies nécessaires à l’interrogation de la Web API, les mêmes que celles référencées dans l’article précédent :
• System.Net.Http
• System.Net.Http.Formatting
Et finalement une référence à mon projet contenant les objets à manipuler (cf. cet article)
• ParutionDomain
Maintenant, je vais ajouter les using nécessaires à mon implémentation et je déclare une classe MyWebAPISourceComponent décoré de l’attribut DtsPipelineComponent ce qui me donne le squelette suivant :


using System;
using System.Collections.Generic;
using System.Net.Http;
using Microsoft.SqlServer.Dts.Pipeline;
using Microsoft.SqlServer.Dts.Pipeline.Wrapper;
using Microsoft.SqlServer.Dts.Runtime.Wrapper;

namespace WebApiSourceComponent
{
    [DtsPipelineComponent(DisplayName = "Web API Source", ComponentType = ComponentType.SourceAdapter, CurrentVersion = 1)]
    public class MyWebApiSourceComponent : PipelineComponent
    {}
}

Suite : La gestion des métadonnées du composant