使用 Dataverse SDK 使用解決方案
作為開發到生產生命週期的一部分,您可能想要建立自訂自動化來處理某些任務。 例如,在您的 DevOps 專案管道中,您可能會想要執行部分自訂代碼或腳本,以建立沙箱環境、匯入未受管理解決方案、將該未受管理解決方案匯出為受管理的解決方案,最後刪除環境。 您可以使用可用的 API 執行這些作業及更多作業。 以下是使用 Dataverse SDK for .NET 和自訂程式碼可以完成的一些範例。
Note
您也可以使用 Web API 執行這些相同的作業。 相關的動作為:ImportSolution、ExportSolution、CloneAsPatch 和 CloneAsSolution。
建立、匯出或匯入未受管理的解決方案
了解如何使用 C# 程式碼執行一些常見的解決方案作業。 若要查看示範這些類型的解決方案作業的完整工作 C# 程式碼範例及更多內容,請前往範例:使用解決方案。
建立發行者
每個解決方案都需要一個發行者,由發行者資料表表示。 發行者需要具備以下屬性:
- 自訂首碼
- 唯一名稱
- 易記名稱
注意
為了實現健康的應用程式生命週期管理 (ALM) 方法,請務必使用自訂發行者和解決方案 (而不是預設解決方案和發行者) 來部署您的自訂項目。
以下程式碼範例首先定義一個發行者,然後根據唯一名稱檢查該發行者是否已存在。 如果已經存在,則自訂首碼可能已變更。 此範例試圖擷取目前自訂首碼。
PublisherId
也會擷取,讓發行者記錄會被刪除。 如果找不到發行者,可使用 IOrganizationService 建立新的發行者。建立 方法。
// 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 資源,您可以使用這個查詢,將其新增為指定的解決方案組態頁面。 |
Description | 使用此資料行來包含與解決方案相關的所有詳細資訊。 |
下面是範例程式碼,用來建立未受控解決方案,其中會使用我們在前一節中所建立的發行者。
// 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);
追蹤匯入成功
您可以使用 ImportJob 資料表來擷取有關解決方案匯入成功的資料。 當您為 ImportSolutionRequest 指定 ImportJobId
時,您可以使用該值查詢 ImportJob
表以了解匯入的狀態。
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
方法根據相依性中的資訊,提供更好記的訊息。
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 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);
}
偵測解決方案元件是否可以刪除
使用 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);
}
}
}