Dataverse SDK를 사용하여 솔루션 작업

개발에서 프로덕션 수명 주기의 일부로 특정 작업을 처리하는 사용자 지정 자동화를 만들 수 있습니다. 예를 들어, DevOps 프로젝트 파이프라인에서 사용자 지정 코드나 스크립트를 작성해 샌드박스 환경을 생성하고 비관리형 솔루션을 불러오고 관리형으로 비관리형 솔루션을 추출하고 마지막으로 환경을 삭제하고 싶을 수 있습니다. 지원되는 API를 사용하여 이를 수행 할 수 있습니다. 다음은 .NET용 Dataverse SDK 및 사용자 지정 코드를 사용하여 수행할 수 있는 작업의 몇 가지 예입니다.

노트

또한, 웹 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는 표시 이름에 따라 고유 이름을 생성합니다. 고유 이름을 편집할 수 있습니다. 고유 이름에는 영숫자 문자 또는 밑줄 문자만 사용할 수 있습니다.
게시자 게시자 조회를 사용하여 솔루션을 게시자에 연결합니다.
버전 major.minor.build.revision 형식으로 버전을 지정합니다. (예: 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 클래스를 사용하여 비관리형 솔루션을 나타내는 압축 파일을 내보냅니다. 관리형 솔루션을 만드는 옵션은 Managed 속성을 사용하여 설정됩니다. 이 샘플에서는 이름이 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);

가져오기 성공 추적

ImportJob 엔터티를 사용하여 솔루션 가져오기에 대한 데이터를 캡처할 수 있습니다. ImportSolutionRequest에 대해 ImportJobId를 지정할 때 해당 값을 사용하여 가져오기 상태에 대해 ImportJob 엔터티를 쿼리할 수 있습니다. ImportJobIdRetrieveFormattedImportJobResultsRequest 메시지를 사용하여 가져오기 파일을 다운로드하는 데도 사용할 수 있습니다.

// 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를 추출하는 방법을 보여줍니다. 그러면 샘플은 IOrganizationServicesolutionid를 사용합니다. 솔루션을 삭제하는 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를 사용하여 추가 솔루션 작업을 수행할 수 있습니다. 복제 및 패치 솔루션의 경우 CloneAsPatchRequestCloneAsSolutionRequest를 사용합니다. 복제 및 패치에 대한 내용은 솔루션 패치 생성을 참고하십시오.

솔루션 업그레이드를 수행할 때 StageAndUpgradeRequestDeleteAndPromoteRequest를 사용합니다. 준비 및 업그레이드 프로세스에 대한 자세한 내용은 솔루션 업그레이드 또는 업데이트를 참조하십시오.

솔루션 종속성 검색

이 샘플에서는 솔루션 구성 요소 간의 종속성을 보여 주는 보고서를 만드는 방법을 보여 줍니다.

이 코드는 다음 작업을 수행합니다.

  • 솔루션의 모든 구성 요소를 검색합니다.

  • 각 구성 요소에 대한 모든 종속성을 검색합니다.

  • 찾은 각 종속성은 종속성을 설명하는 보고서를 표시합니다.

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