People must submit their catalog items for approval and an administrator must approve them before others can use them.
To submit an item to the catalog, you need:
A solution or package deployer package containing the item you want to submit.
A submission metadata JSON document.
Use the pac catalog create-submission command to get an example submission metadata JSON document. You must edit this document and more submission attributes can be added. More information: Submission attributes
Submission attributes
Before you can submit items to a catalog, you must prepare a JSON document that describes the items you want to submit.
Items submitted to the catalog need to be included within a package deployer package. A package deployer package contains a solution zip file and some optional instructions to apply when deploying the package. If you don't have a package deployer package, you can create one for the solution that contains your items.
pac catalog submit -p "BuildDemoSubmission.json" -sz "ContosoConference_1_0_0_1_managed.zip"
Creating package for catalog submit request...
Connected to... TestCatalog
Connected as user@domain
Tracking id for this submission is 0e6b119d-80f3-ed11-8849-000d3a0a2d9d
The static SubmitCatalogApprovalRequest method demonstrates using the mspcat_SubmitCatalogApprovalRequest message. This example uses the mspcat_SubmitCatalogApprovalRequestRequest and mspcat_SubmitCatalogApprovalRequestResponse classes generated using the pac modelbuilder build command.
This example method returns an instance of the mspcat_SubmitCatalogApprovalRequestResponse class, which contains CertificationRequestId and AsyncOperationId properties you can use the check the status of the submission.
The mspcat_SubmitCatalogApprovalRequest message requires that the submission JSON file CatalogItemDefinitionpackageFile property is set to specify a URL to download a package deployer package file.
/// <summary>
/// Submits a Catalog item for approval
/// </summary>
/// <param name="service">The authenticated IOrganizationService instance.</param>
/// <param name="pathToSubmissionFile">The location of the submission file</param>
/// <returns>
/// mspcat_SubmitCatalogApprovalRequestResponse contains AsyncOperationId
/// and CertificationRequestId
/// </returns>
static mspcat_SubmitCatalogApprovalRequestResponse SubmitCatalogApprovalRequest(
IOrganizationService service,
FileInfo pathToSubmissionFile)
{
byte[] fileBytes = File.ReadAllBytes(pathToSubmissionFile.FullName);
string encodedSubmissionFile = Convert.ToBase64String(fileBytes);
var request = new mspcat_SubmitCatalogApprovalRequestRequest
{
EncodedApprovalRequest = encodedSubmissionFile
};
return (mspcat_SubmitCatalogApprovalRequestResponse)service.Execute(request);
}
The following SubmitCatalogApprovalRequest PowerShell function demonstrates how to use the mspcat_SubmitCatalogApprovalRequest message.
The results returned are an instance of the mspcat_SubmitCatalogApprovalRequestResponse complex type, which contains CertificationRequestId and AsyncOperationId properties you can use the check the status of the submission.
The mspcat_SubmitCatalogApprovalRequest message requires that the submission JSON file CatalogItemDefinitionpackageFile property is set to specify a URL to download a package deployer package file.
This function depends on the $baseURI and $baseHeaders values set using the Connect function as described in Create a Connect function.
Create package deployer package from an unmanaged solution
When you use the mspcat_SubmitCatalogApprovalRequest message with the SDK for .NET or Web API as described in Submit items to the catalog, the submission JSON file must include a CatalogItemDefinitionpackageFile property set to specify a URL in the filesaslink to download a package deployer package file. You don't need to do this with the pac catalog submit command because it does this for you.
This URL can represent anywhere that Dataverse can download a file without any credentials, but we don't recommend you place the files on public download location. Instead, you can use the Package Submission Store (mspcat_PackageStore) table to generate a package deployer package using an unmanaged solution from any environment in your tenant. This process generates a record in this table that contains a package in the PackageFile (mspcat_PackageFile) file column. You can then use the GetFileSasUrl message to get a shared access signature (SAS) URL to enable anonymous downloading of the file within 1 hour. Because the URL is only valid within an hour, this process should be automated so that access to download the file doesn't expire.
The static CatalogItemFromSolution method shows how to create a catalog item from a solution following the steps described in Process. The catalogItemSubmissionJsonString parameter for this function shouldn't have a packageFile property set because this function adds it.
/// <summary>
/// Processes a solution and returns the catalog item ID
/// </summary>
/// <param name="service">The authenticated IOrganizationService instance</param>
/// <param name="solutionName">The name of the solution</param>
/// <param name="solutionUniqueName">The unique name of the solution</param>
/// <param name="catalogItemSubmissionJsonString">The string containing the submission json file</param>
/// <returns>Catalog Item ID</returns>
/// <exception cref="Exception"></exception>
static string CatalogItemFromSolution(
IOrganizationService service,
string solutionName,
string solutionUniqueName,
string catalogItemSubmissionJsonString
)
{
Entity packageStoreRecord = new("mspcat_packagestore")
{
Attributes = {
{"mspcat_name", solutionName},
{"mspcat_solutionuniquename", solutionUniqueName},
{"mspcat_intendeddeploymenttype", new OptionSetValue(526430000)}, // Standard
{"mspcat_operation", new OptionSetValue(958090001)} //Create Package
}
};
Guid packageStoreRecordId = service.Create(packageStoreRecord);
Console.WriteLine($"Created package store record with ID {packageStoreRecordId}");
packageStoreRecord.Attributes.Clear(); //Don't send values again
packageStoreRecord.Id = packageStoreRecordId;
int statusCodeValue = 958090004; // Submitted
string statusReason; // Set in the loop
packageStoreRecord["statuscode"] = new OptionSetValue(statusCodeValue);
service.Update(packageStoreRecord); //Set status to Submitted
Console.WriteLine("Updated package store record status to Submitted");
// Columns to retrieve while polling the package store record
ColumnSet packageStoreColumns = new("statuscode");
do
{
Task.Delay(10000).Wait(); //Wait 10 seconds between polling
// Retrieve the record
packageStoreRecord = service.Retrieve(
"mspcat_packagestore",
packageStoreRecord.Id,
packageStoreColumns);
// Get the status code value
statusCodeValue = packageStoreRecord
.GetAttributeValue<OptionSetValue>("statuscode").Value;
statusReason = packageStoreRecord
.FormattedValues["statuscode"];
Console.WriteLine($" - Package store record status is {statusReason}");
// Continue while statusCodeValue is Submitted, Pending, or Running
} while (statusCodeValue.Equals(958090004) ||
statusCodeValue.Equals(1) ||
statusCodeValue.Equals(958090000));
// If it isn't Completed, throw an exception
if (!statusCodeValue.Equals(958090001))
{
statusReason = packageStoreRecord
.FormattedValues["statuscode"];
// 958090002 is 'Failed'
throw new Exception($"Package submission {statusReason}");
}
Console.WriteLine($"Package submission {statusReason}");
// If successful, retrieve the details about the file to download
GetFileSasUrlRequest getFileSasUrlRequest = new()
{
Target = new EntityReference("mspcat_packagestore", packageStoreRecord.Id),
FileAttributeName = "mspcat_packagefile"
};
var getFileSasUrlResponse = (GetFileSasUrlResponse)service
.Execute(getFileSasUrlRequest);
FileSasUrlResponse getFileSasUrlResponseResult = getFileSasUrlResponse.Result;
Console.WriteLine($"Retrieved SAS URL for {getFileSasUrlResponseResult.FileName}");
// Add the packageFile to the catalog item submission
var catalogItemSubmissionJsonObject = JsonNode.Parse(catalogItemSubmissionJsonString).AsObject();
var packageFile = new JsonObject
{
["name"] = getFileSasUrlResponseResult.FileName,
["filesaslink"] = getFileSasUrlResponseResult.SasUrl
};
// Add the packageFile to the catalog item submission
catalogItemSubmissionJsonObject["catalogItemDefinition"]["packageFile"] = packageFile;
catalogItemSubmissionJsonString = catalogItemSubmissionJsonObject.ToJsonString();
string encodedSubmissionJson = Convert
.ToBase64String(Encoding.UTF8.GetBytes(catalogItemSubmissionJsonString));
var submitCatalogApprovalRequest = new mspcat_SubmitCatalogApprovalRequestRequest
{
EncodedApprovalRequest = encodedSubmissionJson
};
var submitCatalogApprovalResponse = (mspcat_SubmitCatalogApprovalRequestResponse)service
.Execute(submitCatalogApprovalRequest);
Guid certificationRequestId = submitCatalogApprovalResponse.CertificationRequestId;
Console.WriteLine($"Submitted catalog approval request with ID {certificationRequestId}");
// Approval must be in either InProgress or Submitted to be processed
// Columns to retrieve while polling the certification request record
ColumnSet certificationRequestColumns = new("statuscode", "mspcat_application");
Entity certificationRequestRecord;
do
{
Task.Delay(10000).Wait(); //Wait 10 seconds between polling
// Retrieve the record
certificationRequestRecord = service.Retrieve(
"mspcat_certificationrequest",
certificationRequestId,
certificationRequestColumns);
// Get the status code value
statusCodeValue = certificationRequestRecord
.GetAttributeValue<OptionSetValue>("statuscode").Value;
statusReason = packageStoreRecord
.FormattedValues["statuscode"];
Console.WriteLine($" - Approval Request status is {statusReason}");
// Continue while statusCodeValue is:
} while (statusCodeValue.Equals(526430002) || // Waiting On Submitter,
statusCodeValue.Equals(526430003) || // Pending Deployment,
statusCodeValue.Equals(526430008) || // Draft
statusCodeValue.Equals(526430009)); // Processing
// If it isn't Submitted or InProgress, throw an exception
if (!(statusCodeValue.Equals(1) || statusCodeValue.Equals(526430001)))
{
string statusreason = certificationRequestRecord
.FormattedValues["statuscode"];
throw new Exception($"Certification request {statusreason}");
}
// Approve the request
mspcat_ResolveApprovalRequest resolveApprovalRequest = new()
{
Target = new EntityReference("mspcat_certificationrequest", certificationRequestId),
requestsuccess = true, //Approve the request
message = "Approved by CatalogItemFromSolution function"
};
// mspcat_ResolveApprovalResponse has no properties to return
service.Execute(resolveApprovalRequest);
Console.WriteLine("Approved the certification request");
// Get the Catalog Item
EntityReference catalogItemReference = certificationRequestRecord
.GetAttributeValue<EntityReference>("mspcat_application");
Entity catalogItem = service.Retrieve(
"mspcat_applications",
catalogItemReference.Id,
new ColumnSet("mspcat_tpsid"));
string tpsid = catalogItem.GetAttributeValue<string>("mspcat_tpsid");
Console.WriteLine($"Returning Catalog Item ID: {tpsid}");
return tpsid;
}
Output
The output of this function should look something like this:
Created package store record with ID 46f662aa-2137-ef11-8409-6045bdd3aec3
Updated package store record status to Submitted
- Package store record status is Submitted
- Package store record status is Pending
- Package store record status is Running
- Package store record status is Running
- Package store record status is Completed
Package submission Completed
Retrieved SAS URL for <solutionName>_1_0_0_0.zip
Submitted catalog approval request with ID b932c7c8-2137-ef11-8409-6045bdd3aec3
- Approval Request status is Completed
Approved the certification request
Returning Catalog Item ID: <solutionUniqueName>
This New-CatalogItemFromSolution PowerShell function shows how to create a catalog item from a solution following the steps described in Process. The catalogItemSubmissionJsonString parameter for this function shouldn't have a packageFile property set because this function adds it.
This New-CatalogItemFromSolution PowerShell function depends on the following variables and functions:
The $baseURI and $baseHeaders values set using the Connect function as described in Create a Connect function
.SYNOPSIS
Creates a new catalog item from a solution and submits it for approval.
.DESCRIPTION
The `New-CatalogItemFromSolution` function automates the process of creating a new catalog item from a specified solution and submitting it for approval. It performs the following steps:
1. Validates the existence of the solution.
2. Creates a package store record.
3. Submits the package for approval.
4. Monitors the approval status.
5. Retrieves the SAS URL for the package file.
6. Submits the catalog item for certification.
.PARAMETER solutionName
The name of the solution.
.PARAMETER solutionUniqueName
The unique name of the solution.
.PARAMETER catalogItemSubmissionJsonString
The JSON string containing the catalog item submission details.
.EXAMPLE
New-CatalogItemFromSolution `
-solutionName "MySolution" `
-solutionUniqueName "my_solution" `
-catalogItemSubmissionJsonString '{"catalogItemDefinition":{...}}'
This example creates a new catalog item from the solution named "MySolution" with the unique name "my_solution" and submits it for approval using the provided JSON string.
.NOTES
Ensure that the `Get-Records`, `New-Record`, `Update-Record`, `Get-Record`, and `Get-FileSasUrl` functions are defined and accessible in your environment.
The function uses specific status codes and operations that should be defined in your system.
function New-CatalogItemFromSolution {
param(
[Parameter(Mandatory)]
[string]
$solutionName,
[Parameter(Mandatory)]
[string]
$solutionUniqueName,
[Parameter(Mandatory)]
[string]
$catalogItemSubmissionJsonString
)
$statusCodeLabelName = 'statuscode@OData.Community.Display.V1.FormattedValue'
$solutionQuery = "?`$filter=uniquename eq '$solutionUniqueName'&`$select=solutionid"
$solutionCollection = (Get-Records `
-setName 'solutions' `
-query $solutionQuery).value
if (!$solutionCollection.Count -eq 1) {
throw "Solution with unique name $solutionUniqueName does not exist"
}
$packageStoreRecord = @{
mspcat_name = $solutionName
mspcat_solutionuniquename = $solutionUniqueName
mspcat_intendeddeploymenttype = 526430000 # Standard
mspcat_operation = 958090001 # Create Package
}
$packageId = New-Record `
-setName 'mspcat_packagestores' `
-body $packageStoreRecord
Write-Host ('Created package store record with ID ' + $packageId)
# Set statuscode to Submitted
$packageStoreRecord = @{
statuscode = 958090004
}
Update-Record `
-setName 'mspcat_packagestores' `
-id $packageId `
-body $packageStoreRecord | Out-Null
Write-Host 'Updated package store record status to Submitted'
do {
Start-Sleep -Seconds 10
$packageStore = Get-Record `
-setName 'mspcat_packagestores' `
-id $packageId `
-query '?$select=statuscode,mspcat_processingmessage'
$statusCodeValue = $packageStore.statuscode
$statusCodeLabel = $packageStore.$statusCodeLabelName
Write-Host (' - Package store record status is ' + $statusCodeLabel)
} while ($statusCodeValue -eq 958090004 -or # Submitted
$statusCodeValue -eq 1 -or # Pending
$statusCodeValue -eq 958090000) # Running
if ($statusCodeValue -ne 958090001) {
# 958090002 is 'Failed'
throw "Package submission $statusCodeLabel"
}
# If successful, retrieve the details about the file to download
$fileSasUrlResponse = Get-FileSasUrl `
-setName 'mspcat_packagestores' `
-id $packageId `
-columnName 'mspcat_packagefile'
Write-Host ('Retrieved SAS URL for ' + $fileSasUrlResponse.FileName)
$catalogItemSubmission = $catalogItemSubmissionJsonString | ConvertFrom-Json
$packageFile = @{
name = $fileSasUrlResponse.FileName
filesaslink = $fileSasUrlResponse.SasUrl
}
$catalogItemSubmission.catalogItemDefinition.packageFile = $packageFile
$catalogItemSubmissionJsonString = $catalogItemSubmission | ConvertTo-Json -Depth 10
$encodedCatalogItemSubmission = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($catalogItemSubmissionJsonString))
$body = @{
EncodedApprovalRequest = $encodedCatalogItemSubmission
} | ConvertTo-Json
$postHeaders = $baseHeaders.Clone()
$postHeaders.Add('Content-Type', 'application/json')
$results = Invoke-RestMethod `
-Method Post `
-Uri ($baseURI + 'mspcat_SubmitCatalogApprovalRequest') `
-Headers $postHeaders `
-Body $body
$certificationRequestId = $results.CertificationRequestId
Write-Host ('Submitted catalog approval request with ID ' + $certificationRequestId)
# Approval must be in either InProgress or Submitted to be processed
do {
Start-Sleep -Seconds 10
# Retrieve the record
$approvalRequestRecord = Get-Record `
-setName 'mspcat_certificationrequests' `
-id $certificationRequestId `
-query '?$select=statuscode'
# Get the status code value
$statusCodeValue = $approvalRequestRecord.statuscode
$statusCodeLabel = $approvalRequestRecord.$statusCodeLabelName
Write-Host (' - Approval request status is ' + $statusCodeLabel)
} while ($statusCodeValue -eq 526430002 -or # Waiting On Submitter
$statusCodeValue -eq 526430003 -or # Pending Deployment
$statusCodeValue -eq 526430008 -or # Draft
$statusCodeValue -eq 526430009) # Processing
# If statuscode isn't Submitted or InProgress, throw an exception
if (!($statusCodeValue -eq 1 -or $statusCodeValue -eq 526430001)) {
throw "Certification request $statusCodeLabel"
}
# Approve the request
ResolveApproval `
-certificationRequestId $certificationRequestId `
-requestsuccess $true `
-message 'Approved by script'
Write-Host 'Approved the certification request'
# Get the Catalog Item
$query = '?$select=mspcat_certificationrequestid'
$query += '&$expand=mspcat_Application($select=mspcat_tpsid)'
$approvalRequestRecord = Get-Record `
-setName 'mspcat_certificationrequests' `
-id $certificationRequestId `
-query $query
$tpsid = $approvalRequestRecord.mspcat_Application.mspcat_tpsid
Write-Host ('Returning Catalog Item ID:' + $tpsid)
return $tpsid
}
Output
The output of this function should look something like this:
Created package store record with ID 46f662aa-2137-ef11-8409-6045bdd3aec3
Updated package store record status to Submitted
- Package store record status is Submitted
- Package store record status is Pending
- Package store record status is Running
- Package store record status is Running
- Package store record status is Completed
Package submission Completed
Retrieved SAS URL for <solutionName>_1_0_0_0.zip
Submitted catalog approval request with ID b932c7c8-2137-ef11-8409-6045bdd3aec3
- Approval Request status is Completed
Approved the certification request
Returning Catalog Item ID: <solutionUniqueName>
Use the pac catalog status command to check the status of catalog submissions.
pac catalog status --tracking-id 0e6b119d-80f3-ed11-8849-000d3a0a2d9d --type submit
Connected to... TestCatalog
Connected as user@domain
Status of the Submit request: Submitted
The following static GetApprovalRequest method retrieves selected columns from the Approval Request (mspcat_certificationrequest) table for the item where the trackingId parameter matches the primary key of the record.
The following Get-ApprovalRequest PowerShell function retrieves selected columns from the Approval Request (mspcat_certificationrequest) table for the item where the $trackingId parameter matches the primary key of the record.
There's no PAC CLI command to perform this operation.
This static ResolveApproval method demonstrates how to resolve a request for a catalog submission using the mspcat_ResolveApproval message. This example uses the mspcat_ResolveApprovalRequest class generated by the pac modelbuilder build command.
/// <summary>
/// Resolves a catalog submission approval
/// </summary>
/// <param name="service">The authenticated IOrganizationService instance.</param>
/// <param name="certificationRequestId">The ID of the certification request.</param>
/// <param name="requestsuccess">The decision to approve or reject the request.</param>
/// <param name="message">Information for the submitter about the resolution</param>
static void ResolveApproval(
IOrganizationService service,
Guid certificationRequestId,
bool requestsuccess,
string message)
{
mspcat_ResolveApprovalRequest request = new()
{
Target = new EntityReference("mspcat_certificationrequest", certificationRequestId),
requestsuccess = requestsuccess,
message = message
};
// mspcat_ResolveApprovalResponse has no properties to return
service.Execute(request);
}
This ResolveApproval PowerShell function demonstrates how to resolve a request for a catalog submission using the mspcat_ResolveApproval action.
This function depends on the $baseURI and $baseHeaders values set using the Connect function as described in Create a Connect function
<#
.SYNOPSIS
This function resolves an approval request.
.DESCRIPTION
mspcat_ResolveApproval is an action bound to the mspcat_certificationrequests table.
.PARAMETER certificationRequestId
This is a mandatory GUID parameter that represents the ID of the certification request.
.PARAMETER requestsuccess
This is a mandatory Boolean parameter that indicates the decision to approve or reject the request..
.PARAMETER message
This is a mandatory string parameter that contains information for the submitter about the resolution.
.EXAMPLE
ResolveApproval `
-certificationRequestId "<Guid>" `
-requestsuccess $true `
-message "Request processed successfully."
.NOTES
The function does not return any value.
Any output from the Invoke-RestMethod cmdlet is sent to Out-Null.
#>
function ResolveApproval {
param (
[Parameter(Mandatory)]
[guid]
$certificationRequestId,
[Parameter(Mandatory)]
[bool]
$requestsuccess,
[Parameter(Mandatory)]
[string]
$message
)
$uri = $baseURI + "mspcat_certificationrequests($certificationRequestId)"
$uri += "/Microsoft.Dynamics.CRM.mspcat_ResolveApproval"
$body = @{
requestsuccess = $requestsuccess
message = $message
} | ConvertTo-Json
$postHeaders = $baseHeaders.Clone()
$postHeaders.Add('Content-Type', 'application/json')
Invoke-RestMethod `
-Method Post `
-Uri $uri `
-Headers $postHeaders `
-Body $body | Out-Null
}