Utiliser des solutions à l’aide du kit de développement logiciel (SDK) Dataverse

Dans le cadre de votre cycle de vie de développement à la production, vous souhaiterez peut-être créer une automatisation personnalisée pour gérer certaines tâches. Par exemple, dans votre pipeline de projet DevOps, vous souhaiterez peut-être exécuter du code ou un script personnalisé qui crée un environnement bac à sable, importe une solution non gérée, exporte cette solution non gérée en tant que solution gérée et, enfin, supprime l’environnement. Vous pouvez faire cela et plus encore en utilisant les API qui sont à votre disposition. Vous trouverez ci-dessous quelques exemples de ce que vous pouvez accomplir à l’aide du SDK Dataverse pour .NET et code personnalisé.

Note

Vous pouvez également effectuer ces mêmes opérations à l’aide de l’API web. Les actions associées sont : ImportSolution, ExportSolution, CloneAsPatch et CloneAsSolution.

Créer, exporter ou importer une solution non gérée

Voyons comment effectuer certaines opérations de solution courantes à l’aide du code C#. Pour afficher l’exemple de code C# de travail complet qui illustre ces types d’opérations de solution (et plus), consultez Exemple : Utiliser des solutions.

Créer un éditeur

Toutes les solutions requièrent un éditeur, représenté par l’entité Publisher. Un éditeur requiert les informations suivantes :

  • Un préfixe de personnalisation
  • Un nom unique
  • Un nom convivial

Note

Pour une approche ALM saine, utilisez toujours un éditeur et une solution personnalisés, et non la solution et l’éditeur par défaut, pour déployer vos personnalisations.

L’exemple de code suivant définit tout d’abord un éditeur, puis vérifie si l’éditeur existe déjà en fonction du nom unique. S’il existe déjà, le préfixe de personnalisation peut avoir été modifié. L’exemple cherche alors à capturer le préfixe de personnalisation actuel. PublisherId est également capturé afin que l’enregistrement d’éditeur puisse être supprimé. Si l’éditeur est introuvable, un éditeur est créé à l’aide de la méthode IOrganizationService.Create.

// Define a new publisher
Publisher _myPublisher = new Publisher
{
   UniqueName = "contoso-publisher",
   FriendlyName = "Contoso publisher",
   SupportingWebsiteUrl =
      "https://learn.microsoft.com/powerapps/developer/data-platform/overview",
   CustomizationPrefix = "contoso",
   EMailAddress = "someone@contoso.com",
   Description = "This publisher was created from sample code"
};

// Does the publisher already exist?
QueryExpression querySamplePublisher = new QueryExpression
{
   EntityName = Publisher.EntityLogicalName,
   ColumnSet = new ColumnSet("publisherid", "customizationprefix"),
   Criteria = new FilterExpression()
};

querySamplePublisher.Criteria.AddCondition("uniquename", ConditionOperator.Equal,
   _myPublisher.UniqueName);

EntityCollection querySamplePublisherResults =
   _serviceProxy.RetrieveMultiple(querySamplePublisher);

Publisher SamplePublisherResults = null;

// If the publisher already exists, use it
if (querySamplePublisherResults.Entities.Count > 0)
{
   SamplePublisherResults = (Publisher)querySamplePublisherResults.Entities[0];
   _publisherId = (Guid)SamplePublisherResults.PublisherId;
   _customizationPrefix = SamplePublisherResults.CustomizationPrefix;
}

// If the publisher doesn't exist, create it
if (SamplePublisherResults == null)
{
   _publisherId = _serviceProxy.Create(_myPublisher);

   Console.WriteLine(String.Format("Created publisher: {0}.",
   _myPublisher.FriendlyName));

   _customizationPrefix = _myPublisher.CustomizationPrefix;
}

Créer une solution non gérée

Une fois qu’un éditeur personnalisé est disponible, vous pouvez ensuite créer une solution non gérée. Le tableau suivant répertorie les champs avec les descriptions contenus par une solution.

Étiquette de champ Description
Nom complet Nom de la solution.
Nom Microsoft Dataverse génère un nom unique basé sur le Nom d’affichage. Vous pouvez modifier le nom unique. Le nom unique contient uniquement des caractères alphanumériques ou des traits de soulignement.
Éditeur Utilisez la recherche Éditeur pour associer la solution à un éditeur.
Version Spécifiez une version en utilisant le format suivant : révision.build.mineure.majeure (Par exemple : 1.0.0.0).
Page de configuration Si vous incluez une ressource web HTML dans votre solution, vous pouvez utiliser cette recherche pour l’ajouter comme page de configuration de solution désignée.
Description Utilisez ce champ pour inclure toutes les informations nécessaires sur votre solution.

Voici un exemple de code pour créer une solution non gérée qui utilise l’éditeur que nous avons créé dans la section précédente.

// Create a solution
Solution solution = new Solution
{
   UniqueName = "sample-solution",
   FriendlyName = "Sample solution",
   PublisherId = new EntityReference(Publisher.EntityLogicalName, _publisherId),
   Description = "This solution was created by sample code.",
   Version = "1.0"
};

// Check whether the solution already exists
QueryExpression queryCheckForSampleSolution = new QueryExpression
{
   EntityName = Solution.EntityLogicalName,
   ColumnSet = new ColumnSet(),
   Criteria = new FilterExpression()
};

queryCheckForSampleSolution.Criteria.AddCondition("uniquename",
   ConditionOperator.Equal, solution.UniqueName);

// Attempt to retrieve the solution
EntityCollection querySampleSolutionResults =
   _serviceProxy.RetrieveMultiple(queryCheckForSampleSolution);

// Create the solution if it doesn't already exist
Solution SampleSolutionResults = null;

if (querySampleSolutionResults.Entities.Count > 0)
{
   SampleSolutionResults = (Solution)querySampleSolutionResults.Entities[0];
   _solutionsSampleSolutionId = (Guid)SampleSolutionResults.SolutionId;
}

if (SampleSolutionResults == null)
{
   _solutionsSampleSolutionId = _serviceProxy.Create(solution);
}

Après avoir créé une solution non gérée, vous pouvez ajouter des composants de solution en les créant dans le contexte de cette solution ou ajoutant des composants existants d’autres solutions. Plus d’information : Ajouter un nouveau composant de solution et Ajouter un composant de solution existant

Exporter une solution non gérée

Cet exemple de code montre comment exporter une solution non gérée ou empaqueter une solution gérée. Le code utilise la classe ExportSolutionRequest pour exporter un fichier compressé représentant une solution non gérée. La possibilité de créer une solution gérée est définie avec la propriété Gérée. Cet exemple enregistre un fichier nommé samplesolution.zip dans le dossier de sortie.

// Export a solution
ExportSolutionRequest exportSolutionRequest = new ExportSolutionRequest();
exportSolutionRequest.Managed = false;
exportSolutionRequest.SolutionName = solution.UniqueName;

ExportSolutionResponse exportSolutionResponse =
   (ExportSolutionResponse)_serviceProxy.Execute(exportSolutionRequest);

byte[] exportXml = exportSolutionResponse.ExportSolutionFile;
string filename = solution.UniqueName + ".zip";

File.WriteAllBytes(outputDir + filename, exportXml);

Console.WriteLine("Solution exported to {0}.", outputDir + filename);

Importer une solution non gérée

L’importation (ou la mise à niveau) d’une solution à l’aide de code s’effectue avec ImportSolutionRequest.

// Install or upgrade a solution
byte[] fileBytes = File.ReadAllBytes(ManagedSolutionLocation);

ImportSolutionRequest impSolReq = new ImportSolutionRequest()
{
   CustomizationFile = fileBytes
};

_serviceProxy.Execute(impSolReq);

Suivi de la réussite de l’importation

Vous pouvez utiliser l’entité ImportJob pour capturer des données sur la réussite de l’importation de la solution. Lorsque vous spécifiez un ImportJobId pour ImportSolutionRequest, vous pouvez utiliser la valeur pour interroger l’entité ImportJob à propos du statut de l’importation. Le ImportJobId peut également être utilisé pour télécharger un fichier journal à l’aide du message RetrieveFormattedImportJobResultsRequest.

// Monitor solution import success
byte[] fileBytesWithMonitoring = File.ReadAllBytes(ManagedSolutionLocation);

ImportSolutionRequest impSolReqWithMonitoring = new ImportSolutionRequest()
{
   CustomizationFile = fileBytes,
   ImportJobId = Guid.NewGuid()
};

_serviceProxy.Execute(impSolReqWithMonitoring);

ImportJob job = (ImportJob)_serviceProxy.Retrieve(ImportJob.EntityLogicalName,
   impSolReqWithMonitoring.ImportJobId, new ColumnSet(new System.String[] { "data",
   "solutionname" }));

System.Xml.XmlDocument doc = new System.Xml.XmlDocument();
doc.LoadXml(job.Data);

String ImportedSolutionName =
   doc.SelectSingleNode("//solutionManifest/UniqueName").InnerText;

String SolutionImportResult =
   doc.SelectSingleNode("//solutionManifest/result/\@result").Value;

Console.WriteLine("Report from the ImportJob data");

Console.WriteLine("Solution Unique name: {0}", ImportedSolutionName);

Console.WriteLine("Solution Import Result: {0}", SolutionImportResult);

Console.WriteLine("");

// This code displays the results for Global Option sets installed as part of a
// solution.

System.Xml.XmlNodeList optionSets = doc.SelectNodes("//optionSets/optionSet");

foreach (System.Xml.XmlNode node in optionSets)
{
   string OptionSetName = node.Attributes["LocalizedName"].Value;
   string result = node.FirstChild.Attributes["result"].Value;

   if (result == "success")
   {
      Console.WriteLine("{0} result: {1}",OptionSetName, result);
   }
   else
   {
      string errorCode = node.FirstChild.Attributes["errorcode"].Value;
      string errorText = node.FirstChild.Attributes["errortext"].Value;

      Console.WriteLine("{0} result: {1} Code: {2} Description: {3}",OptionSetName,
      result, errorCode, errorText);
   }
}

Le contenu de la propriété Data correspond à une chaîne représentant un fichier XML de solution.

Ajouter et supprimer des composants de solution

Découvrez comment ajouter et supprimer des composants de solution à l’aide du code.

Ajouter un nouveau composant de solution

Cet exemple montre comment créer un composant de solution associé à une solution spécifique. Si vous n’associez pas le composant de solution à une solution spécifique lors de sa création, il est uniquement ajouté à la solution par défaut et vous devrez l’ajouter à une solution manuellement ou à l’aide du code inclus dans Ajouter un composant de solution existant.

Ce code crée un groupe d’options global et l’ajoute à la solution avec un nom unique égal à _primarySolutionName.

OptionSetMetadata optionSetMetadata = new OptionSetMetadata()
{
   Name = _globalOptionSetName,
   DisplayName = new Label("Example Option Set", _languageCode),
   IsGlobal = true,
   OptionSetType = OptionSetType.Picklist,
   Options =
{
   new OptionMetadata(new Label("Option 1", _languageCode), 1),
   new OptionMetadata(new Label("Option 2", _languageCode), 2)
}
};
CreateOptionSetRequest createOptionSetRequest = new CreateOptionSetRequest
{
   OptionSet = optionSetMetadata                
};

createOptionSetRequest.SolutionUniqueName = _primarySolutionName;
_serviceProxy.Execute(createOptionSetRequest);

Ajouter un composant de solution existant

Cet exemple montre comment ajouter un composant de solution existant à une solution.

Le code suivant utilise AddSolutionComponentRequest pour ajouter l’entité Account en tant que composant de solution à une solution non gérée.

// Add an existing Solution Component
// Add the Account entity to the solution
RetrieveEntityRequest retrieveForAddAccountRequest = new RetrieveEntityRequest()
{
   LogicalName = Account.EntityLogicalName
};
RetrieveEntityResponse retrieveForAddAccountResponse = (RetrieveEntityResponse)_serviceProxy.Execute(retrieveForAddAccountRequest);
AddSolutionComponentRequest addReq = new AddSolutionComponentRequest()
{
   ComponentType = (int)componenttype.Entity,
   ComponentId = (Guid)retrieveForAddAccountResponse.EntityMetadata.MetadataId,
   SolutionUniqueName = solution.UniqueName
};
_serviceProxy.Execute(addReq);

Supprimer un composant de solution

Cet exemple montre comment supprimer un composant de solution d’une solution non gérée. Le code suivant utilise RemoveSolutionComponentRequest pour supprimer un composant de solution d’entité d’une solution non gérée. solution.UniqueName fait référence à la solution créée dans Créer une solution non gérée.

// Remove a Solution Component
// Remove the Account entity from the solution
RetrieveEntityRequest retrieveForRemoveAccountRequest = new RetrieveEntityRequest()
{
   LogicalName = Account.EntityLogicalName
};
RetrieveEntityResponse retrieveForRemoveAccountResponse = (RetrieveEntityResponse)_serviceProxy.Execute(retrieveForRemoveAccountRequest);

RemoveSolutionComponentRequest removeReq = new RemoveSolutionComponentRequest()
{
   ComponentId = (Guid)retrieveForRemoveAccountResponse.EntityMetadata.MetadataId,
   ComponentType = (int)componenttype.Entity,
   SolutionUniqueName = solution.UniqueName
};
_serviceProxy.Execute(removeReq);

Suppression d’une solution

Cet exemple montre comment supprimer une solution. Cet exemple explique comment récupérer une solution à l’aide de l’attribut uniquename de la solution, puis récupérer l’attribut solutionid dans les résultats. L’exemple utilise ensuite solutionid avec IOrganizationService. Méthode Delete pour supprimer la solution.

// Delete a solution

QueryExpression queryImportedSolution = new QueryExpression
{
    EntityName = Solution.EntityLogicalName,
    ColumnSet = new ColumnSet(new string[] { "solutionid", "friendlyname" }),
    Criteria = new FilterExpression()
};


queryImportedSolution.Criteria.AddCondition("uniquename", ConditionOperator.Equal, ImportedSolutionName);

Solution ImportedSolution = (Solution)_serviceProxy.RetrieveMultiple(queryImportedSolution).Entities[0];

_serviceProxy.Delete(Solution.EntityLogicalName, (Guid)ImportedSolution.SolutionId);

Console.WriteLine("Deleted the {0} solution.", ImportedSolution.FriendlyName);

Clonage, correctif et mise à niveau

Vous pouvez effectuer des opérations de solution supplémentaires à l’aide des API disponibles. Pour les solutions de clonage et de correction, utilisez CloneAsPatchRequest et CloneAsSolutionRequest. Pour plus d’informations sur le clonage et les correctifs, consultez Créer des correctifs de solution.

Lors de la mise à niveau de la solution, utilisez StageAndUpgradeRequest et DeleteAndPromoteRequest. Pour plus d’informations sur le processus de préparation et de mise à niveau, voir Mettre à niveau ou mettre à jour une solution.

Détecter les dépendances de solutions

Cet exemple montre comment créer un rapport montrant les dépendances entre les composants de solution.

Ce code :

  • récupère tous les composants d’une solution ;

  • récupère toutes les dépendances de chaque composant ;

  • affiche un rapport décrivant la dépendance, pour chaque dépendance détectée.

// Grab all Solution Components for a solution.
QueryByAttribute componentQuery = new QueryByAttribute
{
    EntityName = SolutionComponent.EntityLogicalName,
    ColumnSet = new ColumnSet("componenttype", "objectid", "solutioncomponentid", "solutionid"),
    Attributes = { "solutionid" },

    // In your code, this value would probably come from another query.
    Values = { _primarySolutionId }
};

IEnumerable<SolutionComponent> allComponents =
    _serviceProxy.RetrieveMultiple(componentQuery).Entities.Cast<SolutionComponent>();

foreach (SolutionComponent component in allComponents)
{
    // For each solution component, retrieve all dependencies for the component.
    RetrieveDependentComponentsRequest dependentComponentsRequest =
        new RetrieveDependentComponentsRequest
        {
            ComponentType = component.ComponentType.Value,
            ObjectId = component.ObjectId.Value
        };
    RetrieveDependentComponentsResponse dependentComponentsResponse =
        (RetrieveDependentComponentsResponse)_serviceProxy.Execute(dependentComponentsRequest);

    // If there are no dependent components, we can ignore this component.
    if (dependentComponentsResponse.EntityCollection.Entities.Any() == false)
        continue;

    // If there are dependencies upon this solution component, and the solution
    // itself is managed, then you will be unable to delete the solution.
    Console.WriteLine("Found {0} dependencies for Component {1} of type {2}",
        dependentComponentsResponse.EntityCollection.Entities.Count,
        component.ObjectId.Value,
        component.ComponentType.Value
        );
    //A more complete report requires more code
    foreach (Dependency d in dependentComponentsResponse.EntityCollection.Entities)
    {
        DependencyReport(d);
    }
}

La méthode DependencyReport est utilisée dans l’exemple de code suivant.

Rapport de dépendance

La méthode DependencyReport fournit un message plus convivial à partir des informations disponibles dans la dépendance.

Note

Dans cet exemple, la méthode est uniquement partiellement mise en œuvre. Elle affiche des messages uniquement pour les composants de solution d’attributs et de groupes d’options.

/// <summary>
/// Shows how to get a more friendly message based on information within the dependency
/// <param name="dependency">A Dependency returned from the RetrieveDependentComponents message</param>
/// </summary> 
public void DependencyReport(Dependency dependency)
{
 // These strings represent parameters for the message.
    String dependentComponentName = "";
    String dependentComponentTypeName = "";
    String dependentComponentSolutionName = "";
    String requiredComponentName = "";
    String requiredComponentTypeName = "";
    String requiredComponentSolutionName = "";

 // The ComponentType global Option Set contains options for each possible component.
    RetrieveOptionSetRequest componentTypeRequest = new RetrieveOptionSetRequest
    {
     Name = "componenttype"
    };

    RetrieveOptionSetResponse componentTypeResponse = (RetrieveOptionSetResponse)_serviceProxy.Execute(componentTypeRequest);
    OptionSetMetadata componentTypeOptionSet = (OptionSetMetadata)componentTypeResponse.OptionSetMetadata;
 // Match the Component type with the option value and get the label value of the option.
    foreach (OptionMetadata opt in componentTypeOptionSet.Options)
    {
     if (dependency.DependentComponentType.Value == opt.Value)
     {
      dependentComponentTypeName = opt.Label.UserLocalizedLabel.Label;
     }
     if (dependency.RequiredComponentType.Value == opt.Value)
     {
      requiredComponentTypeName = opt.Label.UserLocalizedLabel.Label;
     }
    }
 // The name or display name of the compoent is retrieved in different ways depending on the component type
    dependentComponentName = getComponentName(dependency.DependentComponentType.Value, (Guid)dependency.DependentComponentObjectId);
    requiredComponentName = getComponentName(dependency.RequiredComponentType.Value, (Guid)dependency.RequiredComponentObjectId);

 // Retrieve the friendly name for the dependent solution.
    Solution dependentSolution = (Solution)_serviceProxy.Retrieve
     (
      Solution.EntityLogicalName,
      (Guid)dependency.DependentComponentBaseSolutionId,
      new ColumnSet("friendlyname")
     );
    dependentComponentSolutionName = dependentSolution.FriendlyName;
    
 // Retrieve the friendly name for the required solution.
    Solution requiredSolution = (Solution)_serviceProxy.Retrieve
      (
       Solution.EntityLogicalName,
       (Guid)dependency.RequiredComponentBaseSolutionId,
       new ColumnSet("friendlyname")
      );
    requiredComponentSolutionName = requiredSolution.FriendlyName;

 // Display the message
     Console.WriteLine("The {0} {1} in the {2} depends on the {3} {4} in the {5} solution.",
     dependentComponentName,
     dependentComponentTypeName,
     dependentComponentSolutionName,
     requiredComponentName,
     requiredComponentTypeName,
     requiredComponentSolutionName);
}

Détecter si un composant de solution peut être supprimé

Utilisez le message RetrieveDependenciesForDeleteRequest pour identifier les autres composants de solution qui pourraient empêcher un composant de solution donné d’être supprimé. L’exemple de code suivant recherche tous les attributs utilisant un groupe d’options global connu. Tout attribut utilisant le groupe d’options global empêcherait le groupe d’options défini d’être supprimé.

// Use the RetrieveOptionSetRequest message to retrieve  
// a global option set by it's name.
RetrieveOptionSetRequest retrieveOptionSetRequest =
    new RetrieveOptionSetRequest
    {
     Name = _globalOptionSetName
    };

// Execute the request.
RetrieveOptionSetResponse retrieveOptionSetResponse =
    (RetrieveOptionSetResponse)_serviceProxy.Execute(
    retrieveOptionSetRequest);
_globalOptionSetId = retrieveOptionSetResponse.OptionSetMetadata.MetadataId;
if (_globalOptionSetId != null)
{ 
 // Use the global OptionSet MetadataId with the appropriate componenttype
 // to call RetrieveDependenciesForDeleteRequest
 RetrieveDependenciesForDeleteRequest retrieveDependenciesForDeleteRequest = new RetrieveDependenciesForDeleteRequest 
{ 
 ComponentType = (int)componenttype.OptionSet,
 ObjectId = (Guid)_globalOptionSetId
};

 RetrieveDependenciesForDeleteResponse retrieveDependenciesForDeleteResponse =
  (RetrieveDependenciesForDeleteResponse)_serviceProxy.Execute(retrieveDependenciesForDeleteRequest);
 Console.WriteLine("");
 foreach (Dependency d in retrieveDependenciesForDeleteResponse.EntityCollection.Entities)
 {

  if (d.DependentComponentType.Value == 2)//Just testing for Attributes
  {
   String attributeLabel = "";
   RetrieveAttributeRequest retrieveAttributeRequest = new RetrieveAttributeRequest
   {
    MetadataId = (Guid)d.DependentComponentObjectId
   };
   RetrieveAttributeResponse retrieveAttributeResponse = (RetrieveAttributeResponse)_serviceProxy.Execute(retrieveAttributeRequest);

   AttributeMetadata attmet = retrieveAttributeResponse.AttributeMetadata;

   attributeLabel = attmet.DisplayName.UserLocalizedLabel.Label;
  
    Console.WriteLine("An {0} named {1} will prevent deleting the {2} global option set.", 
   (componenttype)d.DependentComponentType.Value, 
   attributeLabel, 
   _globalOptionSetName);
  }
 }
}