Compartilhar via


Trabalhar com soluções usando o SDK do Dataverse

Como parte do desenvolvimento do ciclo de vida da produção, convém criar automação personalizada para lidar com determinadas tarefas. Por exemplo, no pipeline de projeto do DevOps, você pode querer executar algum código ou script personalizado que crie um ambiente de área restrita, importe uma solução não gerenciada, exporte essa solução não gerenciada como uma solução gerenciada e, por fim, exclua o ambiente. Você pode fazer isso e muito mais usando as APIs disponíveis para você. A seguir, alguns exemplos do que você pode realizar usando o SDK do Dataverse para .NET e código personalizado.

Nota

Você também pode executar essas mesmas operações usando a API Web. As ações relacionadas são: ImportSolution, ExportSolution, CloneAsPatch e CloneAsSolution.

Criar, exportar ou importar uma solução não gerenciada

Vamos ver como executar algumas operações comuns da solução usando o código C#. Para visualizar o exemplo de código C# completo que demonstra esses tipos de operações de solução (e mais), consulte Exemplo: Trabalhar com soluções.

Criar um fornecedor

Cada solução requer um editor, representado pela entidade Fornecedor. Um fornecedor requer o seguinte:

  • Um prefixo de personalização
  • Um nome exclusivo
  • Um nome amigável

Nota

Para ter uma abordagem íntegra do ALM, sempre use um fornecedor e uma solução personalizados, não a solução e o fornecedor padrão, para implantar suas personalizações.

O exemplo de código a seguir define primeiro um fornecedor e então verifica se o fornecedor já existe baseado no nome exclusivo. Se já existir, o prefixo de personalização pode ter sido alterado, por isso este exemplo procura capturar o prefixo de personalização atual. O PublisherId também é capturado de forma que o registro do fornecedor possa ser excluído. Se o editor não for encontrado, um novo editor será criado usando o método IOrganizationService.Criar.

// 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;
}

Criar uma solução não gerenciada

Depois de ter um fornecedor personalizado disponível, você poderá criar uma solução não gerenciada. A tabela a seguir lista os campos com as descrições que uma solução contém.

Rótulo do Campo Descrição
Nome para Exibição O nome da solução.
Nome O Microsoft Dataverse gera um nome único com base no Nome para Exibição. Você pode editar o nome único. O nome único deve conter apenas caracteres alfanuméricos ou o caractere sublinhado.
Publicador Use a pesquisa do Fornecedor para associar a solução ao fornecedor.
Versão Especifique uma versão com o seguinte formato: maior.menor.criação.revisão (por exemplo, 1.0.0.0.)
Página de Configuração Se você incluir um recurso da Web HTML na sua solução, poderá usar essa pesquisa para adicioná-lo como sua página designada de configuração da solução.
Descrição Use este campo para incluir todos os detalhes relevantes sobre a sua solução.

Veja abaixo um código de exemplo para criar uma solução não gerenciada que usa o editor que criamos na seção anterior.

// 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);
}

Depois de criar uma solução não gerenciada, é possível adicionar componentes de solução criado-os no contexto dessa solução ou adicionando componentes de soluções existentes. Mais informações: Adicionar um novo componente de solução e Adicionar um componente da solução existente

Exporte a solução não gerenciada.

Este exemplo de código mostra como exportar uma solução não gerenciada ou empacotar uma solução gerenciada. O código usa a classe ExportSolutionRequest para exportar um arquivo compactado que representa uma solução não gerenciada. A opção para criar uma solução gerenciada é configurada usando a propriedade Gerenciada. Este exemplo salva um arquivo chamado samplesolution.zip para a pasta de saída.

// 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);

Importar uma solução gerenciada

A importação (ou atualização) de uma solução usando código é realizada com ImportSolutionRequest.

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

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

_serviceProxy.Execute(impSolReq);

Acompanhando o sucesso da importação

Você pode usar a entidade ImportJob para capturar dados sobre o sucesso da importação da solução. Quando você especifica um ImportJobId para o ImportSolutionRequest, você pode usar esse valor para consultar a entidade ImportJob sobre o status da importação. O ImportJobId também pode ser usado para baixar um arquivo de log de importação usando a mensagem 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);
   }
}

O conteúdo da propriedade Data é uma sequência que representa o arquivo XML da solução.

Adicionar e remover componentes de solução

Saiba como adicionar e remover componentes de solução usando código.

Adicionar um novo componente de solução

Este exemplo mostra como criar um componente da solução associado a uma solução específica. Se você não associar o componente de solução a uma solução específica quando for criado, ele será adicionado somente à solução padrão e será necessário adicioná-lo a uma solução manualmente ou usando o código incluído em Adicionar um componente de solução existente.

Este código cria um novo conjunto global de opções e o adiciona à solução com um nome exclusivo igual a _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);

Adicionar um componente da solução existente

Este exemplo mostra como adicionar um componente da solução existente a uma solução.

O seguinte código utiliza o AddSolutionComponentRequest para adicionar a entidade Account como um componente de solução a uma solução não gerenciada.

// 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);

Remover um componente da solução

Este exemplo mostra como remover um componente da solução de uma solução não gerenciada. O seguinte código utiliza o RemoveSolutionComponentRequest para remover um componente de solução da entidade de uma solução não gerenciada. O solution.UniqueName faz referência à solução criada em Criar uma solução não gerenciada.

// 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);

Excluir uma solução

O exemplo a seguir mostra como recuperar uma solução usando a solução uniquename e, em seguida, extrair a solutionid dos resultados. O exemplo então usa a solutionid com o IOrganizationService. MétodoDelete para excluir a solução.

// 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);

Clonagem, patches e atualização

Você pode executar operações de solução adicionais usando as APIs disponíveis. Para soluções de clonagem e patches, use CloneAsPatchRequest e CloneAsSolutionRequest. Para obter informações sobre clonagem e correção, consulte Criar patches de solução.

Ao fazer upgrades de solução, use StageAndUpgradeRequest e DeleteAndPromoteRequest. Para obter mais informações sobre o processo de preparo e upgrades, consulte Fazer upgrade ou atualizar uma solução.

Detectar as dependências da solução

Este exemplo mostra como criar um relatório que mostra as dependências entre os componentes da solução.

Este código irá:

  • Recuperar todos os componentes de uma solução.

  • Recuperar todas as dependências de cada componente.

  • Para cada dependência localizada, exibir um relatório que descreve a dependência.

// 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);
    }
}

O método DependencyReport está no seguinte exemplo de código.

Relatório de dependência

O método DependencyReport fornece uma mensagem mais amigável baseada nas informações encontradas dentro da dependência.

Nota

Neste exemplo, o método é implementado apenas parcialmente. Ele pode exibir apenas mensagens de atributos e componentes da solução do conjunto de opções.

/// <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 component 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);
}

Detectar se um componente da solução pode ser excluído

Use a mensagem RetrieveDependenciesForDeleteRequest para identificar todos os outros componentes de solução que impeçam que um dado componente da solução seja excluído. A seguinte amostra de código procura todos os atributos usando um conjunto conhecido global de opções. Qualquer atributo que usa o conjunto global de opções impede que o conjunto global de opções seja excluído.

// 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);
  }
 }
}