Trabalhar com soluções utilizando o SDK do Dataverse

Como parte do seu desenvolvimento para o ciclo de vida da produção, poderá pretender criar uma automatização personalizada para processar determinadas tarefas. Por exemplo, no pipeline do projeto de DevOps, poderá pretender executar algum código personalizado ou script que crie um ambiente de sandbox, importe uma solução não gerida, exporte essa solução não gerida como uma solução gerida e, por fim, elimine o ambiente. Pode efetuar este procedimento e muito mais utilizando as APIs que estão disponíveis para si. Abaixo estão alguns exemplos do que pode conseguir utilizando o SDK do Dataverse para .NET e o código personalizado.

Nota

Também pode efetuar estas mesmas operações utilizando a API Web. As ações relacionadas são: ImportSolution, ExportSolution, CloneAsPatch e CloneAsSolution.

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

Veja como efetuar algumas operações de solução comuns utilizando o código C#. Para ver o exemplo completo de código C# que demonstra estes tipos de operações de solução (e muito mais), consulte Exemplo: trabalhar com soluções.

Criar um editor

Todas as soluções necessitam de um editor, representado pela Entidade do editor. Um editor requer o seguinte:

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

Nota

Para obter uma abordagem em bom estado do ALM, utilize sempre um editor e solução personalizados, e não a solução e o editor predefinidos, para implementar as personalizações.

O seguinte exemplo de código define primeiro um editor e, em seguida, verifica se o editor já existe com base no nome exclusivo. Se já existir, o prefixo de personalização poderá ter sido alterado, pelo que este exemplo procura capturar o prefixo de personalização atual. O PublisherId também é capturado para que o registo do editor possa ser eliminado. Se o editor não for encontrado, é criado um novo editor através do método IOrganizationService.Criarmétodo.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 gerida

Depois de ter um editor personalizado disponível, pode criar uma solução não gerida. A tabela seguinte lista os campos com descrições que uma solução contém.

Etiqueta do Campo Descrição
Nome a Apres. O nome da solução.
Nome O Microsoft Dataverse gera um nome exclusivo com base no Nome a apresentar. Pode editar o nome exclusivo. O nome exclusivo só pode conter carateres alfanuméricos e o carácter de sublinhado.
Editor Utilize a pesquisa Editor para associar a solução a um editor.
Versão Especifique uma versão utilizando o formato seguinte: principal.secundária.compilação.revisão (por exemplo, 1.0.0.0).
Página de Configuração Se incluir um recurso Web HTML na solução, pode utilizar esta pesquisa para adicioná-lo como página de configuração da solução designada.
Descrição Utilize este campo para incluir quaisquer detalhes relevantes sobre a solução.

Segue-se um código de exemplo para criar uma solução não gerida que utiliza o editor criado na secçã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 gerida, pode adicionar componentes da solução criando-os no contexto desta solução ou adicionando componentes existentes a partir de outras soluções. Mais informações: Adicionar um novo componente da solução e Adicionar um componente da solução existente

Exportar uma solução não gerida

Este exemplo de código mostra como exportar uma solução não gerida ou empacotar uma solução gerida. O código utiliza a classe ExportSolutionRequest para exportar um ficheiro comprimido que representa uma solução não gerida. A opção para criar um solução gerida é definida utilizando a propriedade Gerida. Este exemplo guarda um ficheiro samplesolution.zip na 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 não gerida

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

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

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

_serviceProxy.Execute(impSolReq);

Monitorização da importação com êxito

Pode utilizar a entidade ImportJob para capturar dados sobre o êxito da importação da solução. Quando especifica um ImportJobId para o ImportSolutionRequest, pode utilizar esse valor para consultar a entidade ImportJob acerca do estado da importação. O ImportJobId também pode ser utilizado para transferir um ficheiro de registo de importação utilizando 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 cadeia que representa o ficheiro XML da solução.

Adicionar e remover componentes da solução

Saiba como adicionar e remover componentes de solução através de código.

Adicionar um novo componente de solução

Esta amostra mostra como criar um componente de solução que esteja associado a uma solução específica. Se não associar o componente de solução a uma solução específica quando for criada, só será adicionado à solução predefinida e terá de adicioná-la a uma solução manualmente ou através do código incluído em Adicionar um componente de solução existente.

Este código cria um novo conjunto de opções global e adiciona-a à 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 de solução existente

Esta amostra mostra como adicionar um componente de solução existente a uma solução.

O código que se segue utiliza o AddSolutionComponentRequest para adicionar a entidade Account como um componente de solução a uma solução não gerida.

// 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 de solução

Esta amostra mostra como remover um componente de solução de uma solução não gerida. O código que se segue utiliza o RemoveSolutionComponentRequest para remover um componente de solução da entidade de uma solução não gerida. As referências solution.UniqueName à solução criada em Criar uma solução não gerida.

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

Eliminar uma solução

A amostra que se segue mostra como obter uma solução com a solução uniquename e, em seguida, extraia solutionid dos resultados. Em seguida, a amostra utiliza solutionid com IOrganizationService. Método Delete para eliminar 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, correção e atualização

Pode efetuar operações de solução adicionais através das APIs disponíveis. Para as soluções de clonagem e correção, utilize CloneAsPatchRequest e CloneAsSolutionRequest. Para obter informações sobre a clonagem e correção, consulte Criar correções de solução.

Ao efetuar atualizações de versão de soluções, utilize StageAndUpgradeRequest e DeleteAndPromoteRequest. Para mais informações sobre o processo de teste e atualização de versões, consulte Atualizar ou atualizar versão de uma solução.

Detetar dependências de solução

Esta amostra mostra como criar um relatório que mostra as dependências entre os componentes de solução.

Este código irá:

  • Obter todos os componentes para uma solução.

  • Obter todas as dependências para cada componente.

  • Para cada dependência encontrada, apresente 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á na seguinte amostra de código.

Relatório de dependência

O método DependencyReport fornece uma mensagem mais reconhecível baseada em informações encontradas dentro da dependência.

Nota

Nesta amostra, o método é implementado apenas parcialmente. Só pode apresentar mensagens para componentes de solução do conjunto de opções e atributos.

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

Detetar se um componente de solução pode ser eliminado

Utilize a mensagem RetrieveDependenciesForDeleteRequest para identificar quaisquer outros componentes de solução que impeçam a eliminação de um determinado componente de solução. A seguinte amostra de código procura quaisquer atributos através de um optionset global conhecido. Qualquer atributo que utilize o optionset global impediria a eliminação do optionset global.

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