Поділитися через


Робота з рішеннями за допомогою Dataverse SDK

Разом із життєвим циклом розробки до виробництва можна створити настроювану автоматизацію для обробки певних завдань. Наприклад, у воронці проекту DevOps можна виконати певний настроюваний код або сценарій, який створює ізольоване середовище, імпортує некероване рішення, експортує це некероване рішення як кероване рішення і, нарешті, видаляє середовище. Це та більше можна зробити за допомогою доступних для вас API. Нижче наведено кілька прикладів дій, які можна виконати за допомогою Dataverse SDK для .NET і настроюваного коду.

Нотатка

Крім того, ці операції можна виконати за допомогою Web API. Пов'язані дії: імпорт рішення ImportSolution, експорт рішення ExportSolution, клонувати як виправлення CloneAsPatch і клонувати як рішення CloneAsSolution.

Створення, експорт або імпорт некерованого рішення

Перегляньмо, як виконати деякі типові операції рішень за допомогою коду C#. Щоб переглянути повний справний зразок коду C#, який демонструє ці типи операцій з рішеннями (і більше), див. Приклад: робота з рішеннями.

Створення видавця

Для кожного рішення потрібен видавець, представлений сутністю видавця. Для видавця потрібно вказати такі значення:

  • Префікс настроювання
  • Унікальне ім’я
  • Зрозуміле ім’я

Нотатка

Щоб забезпечити справний підхід ALM, завжди використовуйте настроюваний видавець і рішення для розгортання настроювань, а не рішення та видавець за промовчанням.

Нижченаведений зразок коду спочатку визначає видавця, а потім перевірятиме, чи видавець вже існує на основі унікального імені. Якщо він вже існує, префікс настроювання могло було змінено, тому цей зразок намагається знайти поточний префікс настроювання. Проводиться пошук PublisherId також, щоб запис видавця можна було видалити. Якщо видавця не знайдено, створюється новий видавець за допомогою методу 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;
}

Створення некерованого рішення

Після того, як настроюваний видавець став доступним, можна створити некероване рішення. У наведеній нижче таблиці перелічені поля з описами, які містяться в рішенні.

Підпис поля Опис
Зрозуміле ім'я Ім’я рішення.
Ім'я Microsoft Dataverse створює унікальне ім’я на основі Короткого імені. Унікальне ім'я можна змінити. Унікальне ім’я повинно містити лише букви, цифри або символ підкреслення.
Видавець Скористайтесь підстановкою Видавця, щоб зв’язати рішення з видавцем.
Версія Укажіть версію в такому форматі: основна.проміжна.збірка.редакція (наприклад, 1.0.0.0.
Сторінка конфігурації Якщо включити веб-ресурс HTML у рішення, можна скористатися підстановкою, щоб додати його як призначену сторінку конфігурації рішення.
Опис Вкажіть усі відповідні відомості про рішення у цьому полі.

Нижче наведено приклад коду, щоб створити некероване рішення, яке використовує видавець, створений у попередньому розділі.

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

Після створення некерованого рішення можна додати компоненти рішення, створюючи їх в контексті цього рішення або додавши наявні компоненти з інших рішень. Додаткові відомості: Додавання нового компонента рішення і Додавання наявного компонента рішення

Експорт некерованого рішення

У цьому зразку коду показано, як експортувати некероване рішення або упакувати кероване рішення. У коді використовується клас ExportSolutionRequest, щоб експортувати стиснутий файл, який представляє некероване рішення. Можливість створення керованого рішення встановлюється за допомогою властивості Кероване. У цьому зразку файл з іменем samplesolution.zip зберігається до вихідної папки.

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

Імпорт некерованого рішення

Імпортувати (або оновити) рішення за допомогою коду можна через повідомлення ImportSolutionRequest.

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

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

_serviceProxy.Execute(impSolReq);

Відстеження успішності імпорту

Щоб отримати дані про успіх імпорту рішення, можна використовувати сутність "Завдання імпорту". Якщо вказати ImportJobId для ImportSolutionRequest, можна використати це значення, щоб запитувати сутність "Завдання імпорту" щодо стану імпорту. ImportJobId можна також використати для завантаження файлу журналу імпорту за допомогою повідомлення 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);
   }
}

Вміст властивості Data – це рядок, що представляє XML-файл рішення.

Додавання та видалення компонентів рішення

Дізнайтеся, як додавати та видаляти компоненти рішення за допомогою коду.

Додавання нового компонента рішення

Цей зразок показує, як створити компонент рішення, пов'язаний із певним рішенням. Якщо не пов'язати компонент рішення із певним рішенням під час створення, його буде додано тільки до рішення за замовчуванням, і вам доведеться додавати його до рішення вручну або використовуючи код з розділу Додавання наявних компонентів до рішення.

Код створює новий глобальний набір параметрів та додає його до рішення із унікальним іменем, яке співпадає із _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);

Додавання наявних компонентів рішення

Цей зразок показує, як додати наявний компонент рішення до рішення.

Наведений нижче код використовує AddSolutionComponentRequest, щоб додати сутність Account в якості компонента рішення до некерованого рішення.

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

Видалення компонента рішення

Цей зразок показує, як видалити компонент рішення з некерованого рішення. Наведений нижче код використовує RemoveSolutionComponentRequest для видалення сутності, яка є компонентом рішення, з некерованого рішення. solution.UniqueName посилається на сутність, створену у розділі Створення некерованого рішення.

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

Видалення рішення

Наведений нижче зразок демонструє, як можна отримати рішення, використовуючи значення uniquename рішення, а потім видобути solutionid з результатів. Далі зразок використовує solutionid із IOrganizationService. Delete, щоб видалити рішення.

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

Клонування, виправлення та оновлення

Ви можете виконувати додаткові операції із рішеннями, використовуючи наявні API. Для клонування та виправлення рішень використовуйте CloneAsPatchRequest та CloneAsSolutionRequest. Для отримання додаткових відомостей про клонування та виправлення див. Створення виправлень для рішення.

При виконанні оновлення рішень до наступних версій використовуйте StageAndUpgradeRequest та DeleteAndPromoteRequest. Додаткові відомості щодо процесу проміжної обробки даних та оновлень, див. Оновлення рішення або перехід на його нову версію.

Виявлення залежностей рішень

Цей зразок показує, як створити звіт, що покаже залежності між компонентами рішення.

Цей код виконає зазначені нижче дії.

  • Отримає усі компоненти рішення.

  • Отримає усі залежності для кожного компонента.

  • Для кожної виявленої залежності відобразить звіт, що описуватиме цю залежність.

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

Метод DependencyReport використовується у наведеному нижче зразку коду.

Звіт щодо залежностей

Метод DependencyReport надає більш дружнє повідомлення, основане на винайдених в межах залежності відомостях.

Нотатка

У цьому зразку метод застосовано лише частково. Він може відображати повідомлення лише для компонентів рішення типу «атрибут» та «набір параметрів».

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

Виявлення того, чи можна видалити компонент рішення

Використовуйте повідомлення RetrieveDependenciesForDeleteRequest для того, щоб виявити усі компоненти рішення, які можуть заважати видаленню даного компонента рішення. Зразок коду, наведений нижче, шукає усі атрибути, використовуючи відомий глобальний набір параметрів. Будь-який атрибут, що використовує глобальний набір параметрів, може не давати видалити цей глобальний набір параметрів.

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