Dataverse SDK を使用してソリューションで作業する

開発から運用へのライフサイクルの一環として、特定のタスクを処理するカスタム自動化を作成することができます。 たとえば、DevOps プロジェクト パイプラインで、サンドボックス環境を作成し、アンマネージド ソリューションをインポートし、そのアンマネージド ソリューションを管理ソリューションとしてエクスポートし、最後にその環境を削除するカスタム コードまたはスクリプトを実行できます。 利用可能な API を使用すれば、これ以上のことを行うことができます。 以下は、.NET 用 Dataverse SDK とカスタム コードを使用して達成できることのいくつかの例です。

Note

Web API を使用してこれらの同じ操作を実行することもできます。 関連するアクションは次のとおりです: ImportSolutionExportSolutionCloneAsPatch、およびCloneAsSolution

アンマネージド ソリューションの作成、エクスポート、またはインポート

C# コードを使用していくつかの一般的なソリューション操作を実行する方法を見てみましょう。 これらの種類のソリューション操作 (およびその他) を示す C# コード サンプルの一覧を表示するには、サンプル: ソリューションを操作する を参照してください。

発行元の作成

すべてのソリューションには、Publisher エンティティで表される発行元が必要です。 発行元には次の情報が必要です。

  • カスタマイズの接頭辞
  • 一意の名前
  • フレンドリ名

Note

正常な 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 Web リソースを含める場合、この検索を使用して HTML Web リソースを指定されたソリューションの構成ページとして追加できます。
内容 このフィールドを使用して、ソリューションに関する詳細を記入できます。

以下は、前のセクションで作成した発行元を使用するアンマネージド ソリューションを作成するためのサンプル コードです。

// 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 エンティティを使用すると、成功したソリューション インポートについてのデータを取得できます。 ImportSolutionRequestImportJobId を指定すると、その値を使用してインポートの状態について 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 を抽出する方法を示します。 次に、サンプルは solutionidIOrganizationService を使用します。 ソリューションを削除する 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 メソッドは、依存関係で見つかった情報を基にして、よりわかりやすいメッセージを提供します。

Note

このサンプルでは、メソッドは部分的にのみ実装されています。 属性およびオプション セット ソリューション コンポーネントについてのみメッセージを表示できます。

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