C# 示例:应用、加载项和外部测试版的提交
本文提供了 C# 代码示例,演示如何对以下任务使用 Microsoft 应用商店提交 API :
你可以查看每个示例,了解有关它所演示的任务的详细信息,也可以将本文中的所有代码示例生成到控制台应用程序。 若要生成示例,请在 Visual Studio 中创建名为 DeveloperApiCSharpSample 的 C# 控制台应用程序,将每个示例复制到项目中的单独代码文件,然后生成项目。
先决条件
这些示例使用以下库:
- Microsoft.WindowsAzure.Storage.dll。 此库在适用于 .NET 的 Azure SDK 中提供,也可以通过安装 WindowsAzure.Storage NuGet 包来获取它。
- 来自 Newtonsoft 的 Newtonsoft.Json NuGet 包。
主程序
以下示例实现一个命令行程序,该程序调用本文中的其他示例方法,以演示使用 Microsoft Store 提交 API 的不同方法。 要调整此代码以供自己使用,请执行以下操作:
- 将
ApplicationId
和InAppProductId
FlightId
属性分配给要管理的应用、加载项和软件包外部测试版的 ID。 - 将
ClientId
属性ClientSecret
分配给应用的客户端 ID 和密钥,并将 URL 中的TokenEndpoint
tenantid 字符串替换为应用的租户 ID。 有关详细信息,请参阅 如何将 Azure AD 应用程序与你的合作伙伴中心帐户相关联
namespace DeveloperApiCSharpSample
{
class Program
{
static void Main(string[] args)
{
var config = new ClientConfiguration()
{
ApplicationId = "...",
InAppProductId = "...",
FlightId = "...",
ClientId = "...",
ClientSecret = "...",
ServiceUrl = "https://manage.devcenter.microsoft.com",
TokenEndpoint = "https://login.microsoftonline.com/<tenantid>/oauth2/token",
Scope = "https://manage.devcenter.microsoft.com",
};
new FlightSubmissionUpdateSample(config).RunFlightSubmissionUpdateSample();
new InAppProductSubmissionUpdateSample(config).RunInAppProductSubmissionUpdateSample();
new InAppProductSubmissionCreateSample(config).RunInAppProductSubmissionCreateSample();
new AppSubmissionUpdateSample(config).RunAppSubmissionUpdateSample();
}
}
}
ClientConfiguration 帮助程序类
示例应用使用 ClientConfiguration
帮助程序类将 Azure Active Directory 数据和应用数据传递给使用 Microsoft 应用商店提交 API 的每个示例方法。
namespace DeveloperApiCSharpSample
{
/// <summary>
/// Configuration class
/// </summary>
public class ClientConfiguration
{
/// <summary>
/// Client Id of your AAD app.
/// Example" ba3c223b-03ab-4a44-aa32-38aa10c27e32
/// </summary>
public string ClientId { get; set; }
/// <summary>
/// Client secret of your AAD app
/// </summary>
public string ClientSecret { get; set; }
/// <summary>
/// Service root endpoint.
/// Example: https://manage.devcenter.microsoft.com
/// </summary>
public string ServiceUrl { get; set; }
/// <summary>
/// Token endpoint to which the request is to be made. Specific to your AAD app
/// Example: https://login.windows.net/d454d300-128e-2d81-334a-27d9b2baf002/oauth2/token
/// </summary>
public string TokenEndpoint { get; set; }
/// <summary>
/// Resource scope. If not provided (set to null), default one is used for the production API
/// endpoint ("https://manage.devcenter.microsoft.com")
/// </summary>
public string Scope { get; set; }
/// <summary>
/// Application ID.
/// Example: 9WZANCRD4AMD
/// </summary>
public string ApplicationId { get; set; }
/// <summary>
/// In-app-product ID;
/// Example: 9WZBMAAD4VVV
/// </summary>
public string InAppProductId { get; set; }
/// <summary>
/// Flight Id
/// Example: 62211033-c2fa-3934-9b03-d72a6b2a171d
/// </summary>
public string FlightId { get; set; }
}
}
创建应用提交
以下示例使用的类调用了 Microsoft Store 提交 API 中的多个方法以更新应用提交。 该类中的 RunAppSubmissionUpdateSample
方法创建新提交作为上次发布的提交的克隆,然后将克隆的提交更新并提交到合作伙伴中心。 具体来说,RunAppSubmissionUpdateSample
方法执行以下任务:
- 首先,此方法获取指定应用的数据。
- 接下来,此方法会删除应用的挂起提交(如果存在)。
- 然后,此方法会创建新的应用提交(新提交时是上次发布的提交副本)。
- 它会更改新提交的部分详细信息并将新的提交包上载到 Azure Blob 存储。
- 接下来,它会更新并将新提交提交到合作伙伴中心。
- 最后,它会定期检查新提交的状态,直到其成功提交。
namespace DeveloperApiCSharpSample
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
/// <summary>
/// This sample update does a full submission update, updating listings info, images, and packages
/// </summary>
public class AppSubmissionUpdateSample
{
private ClientConfiguration ClientConfig;
/// <summary>
/// Constructor
/// </summary>
/// <param name="c">An instance of ClientConfiguration that contains all parameters populated</param>
public AppSubmissionUpdateSample(ClientConfiguration c)
{
this.ClientConfig = c;
}
public void RunAppSubmissionUpdateSample()
{
// **********************
// SETTINGS
// **********************
var appId = this.ClientConfig.ApplicationId;
var clientId = this.ClientConfig.ClientId;
var clientSecret = this.ClientConfig.ClientSecret;
var serviceEndpoint = this.ClientConfig.ServiceUrl;
var tokenEndpoint = this.ClientConfig.TokenEndpoint;
// Get authorization token.
Console.WriteLine("Getting authorization token ");
var accessToken = IngestionClient.GetClientCredentialAccessToken(
tokenEndpoint,
clientId,
clientSecret).Result;
Console.WriteLine("Getting application ");
var client = new IngestionClient(accessToken, serviceEndpoint);
dynamic app = client.Invoke<dynamic>(
HttpMethod.Get,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.GetApplicationUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
appId),
requestContent: null).Result;
Console.WriteLine(app.ToString());
// Let's get the last published submission, and print its contents, just for information.
if (app.lastPublishedApplicationSubmission == null)
{
// It is not possible to create the very first submission through the API.
throw new InvalidOperationException(
"You need at least one published submission to create new submissions through API.");
}
// Let's see if there is a pending submission. Warning! If it was created through the API,
// it will be deleted so that we could create a new one in its stead.
if (app.pendingApplicationSubmission != null)
{
var submissionId = app.pendingApplicationSubmission.id.Value as string;
// Try deleting it. If it was NOT created via the API, then you need to manually
// delete it from the dashboard. This is done as a safety measure to make sure that a
// user and an automated system don't make conflicting edits.
Console.WriteLine("Deleting the pending submission");
client.Invoke<dynamic>(
HttpMethod.Delete,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.GetSubmissionUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
appId,
submissionId),
requestContent: null).Wait();
}
// Create a new submission, which will be an exact copy of the last published submission.
Console.WriteLine("Creating a new submission");
dynamic clonedSubmission = client.Invoke<dynamic>(
HttpMethod.Post,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.CreateSubmissionUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
appId),
requestContent: null).Result;
// Update some property on the root submission object.
clonedSubmission.notesForCertification = "This is a test update, updating listing info, images, and packages";
// Now, assume we have an en-us listing. Let's try to change its description.
clonedSubmission.listings["en-us"].baseListing.description = "This is my new en-Us description!";
// Update images.
// Assuming we have at least 1 image, let's delete one image.
clonedSubmission.listings["en-us"].baseListing.images[0].fileStatus = "PendingDelete";
var images = new List<dynamic>();
images.Add(clonedSubmission.listings["en-us"].baseListing.images[0]);
images.Add(
new
{
fileStatus = "PendingUpload",
fileName = "rectangles.png",
imageType = "Screenshot",
description = "This is a new image uploaded through the API!",
});
clonedSubmission.listings["en-us"].baseListing.images = JToken.FromObject(images.ToArray());
// Update packages.
// Let's say we want to delete the existing package.
clonedSubmission.applicationPackages[0].fileStatus = "PendingDelete";
// Now, let's add a new package.
var packages = new List<dynamic>();
packages.Add(clonedSubmission.applicationPackages[0]);
packages.Add(
new
{
fileStatus = "PendingUpload",
fileName = "package.appx",
minimumDirectXVersion = "None",
minimumSystemRam = "None"
});
clonedSubmission.applicationPackages = JToken.FromObject(packages.ToArray());
var clonedSubmissionId = clonedSubmission.id.Value as string;
// Uploaded the zip archive with all new files to the SAS url returned with the submission.
var fileUploadUrl = clonedSubmission.fileUploadUrl.Value as string;
Console.WriteLine("FileUploadUrl: " + fileUploadUrl);
Console.WriteLine("Uploading file");
IngestionClient.UploadFileToBlob(@"..\..\files.zip", fileUploadUrl).Wait();
// Update the submission.
Console.WriteLine("Updating the submission");
client.Invoke<dynamic>(
HttpMethod.Put,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.UpdateUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
appId,
clonedSubmissionId),
requestContent: clonedSubmission).Wait();
// Tell the system that we are done updating the submission.
// Update the submission.
Console.WriteLine("Committing the submission");
client.Invoke<dynamic>(
HttpMethod.Post,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.CommitSubmissionUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
appId,
clonedSubmissionId),
requestContent: null).Wait();
// Let's periodically check the status until it changes from "CommitsStarted" to either
// successful status or a failure.
Console.WriteLine("Waiting for the submission commit processing to complete. This may take a couple of minutes.");
string submissionStatus = null;
do
{
Task.Delay(TimeSpan.FromSeconds(5)).Wait();
dynamic statusResource = client.Invoke<dynamic>(
HttpMethod.Get,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.ApplicationSubmissionStatusUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
appId,
clonedSubmissionId),
requestContent: null).Result;
submissionStatus = statusResource.status.Value as string;
Console.WriteLine("Current status: " + submissionStatus);
}
while ("CommitStarted".Equals(submissionStatus));
if ("CommitFailed".Equals(submissionStatus))
{
Console.WriteLine("Submission has failed. Please checkt the Errors collection of the submissionResource response.");
return;
}
else
{
Console.WriteLine("Submission commit success! Here are some data:");
dynamic submission = client.Invoke<dynamic>(
HttpMethod.Get,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.GetSubmissionUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
appId,
clonedSubmissionId),
requestContent: null).Result;
Console.WriteLine("Packages: " + submission.applicationPackages);
Console.WriteLine("en-US description: " + submission.listings["en-us"].baseListing.description);
Console.WriteLine("Images: " + submission.listings["en-us"].baseListing.images);
}
}
}
}
创建加载项提交
以下示例实现一个类,该类使用 Microsoft 应用商店提交 API 中的多种方法创建新的加载项提交。 类 RunInAppProductSubmissionCreateSample
中的方法执行以下任务:
- 首先,该方法 将创建新的加载项。
- 接下来,它会 为加载项创建新的提交。
- 它会将包含提交图标的 ZIP 存档上载到 Azure Blob 存储。
- 接下来,该方法会将新提交提交到合作伙伴中心。
- 最后,它会定期检查新提交的状态,直到其成功提交。
namespace DeveloperApiCSharpSample
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.Http;
using System.Threading.Tasks;
/// <summary>
/// Sample code for how to create add-ons, and how to create and update add-on submissions.
/// </summary>
public class InAppProductSubmissionCreateSample
{
private ClientConfiguration ClientConfig;
/// <summary>
/// Constructor
/// </summary>
/// <param name="c">An instance of ClientConfiguration that contains all parameters populated</param>
public InAppProductSubmissionCreateSample(ClientConfiguration c)
{
this.ClientConfig = c;
}
public void RunInAppProductSubmissionCreateSample()
{
// **********************
// SETTINGS
// **********************
var appId = this.ClientConfig.ApplicationId;
var clientId = this.ClientConfig.ClientId;
var clientSecret = this.ClientConfig.ClientSecret;
var serviceEndpoint = this.ClientConfig.ServiceUrl;
var tokenEndpoint = this.ClientConfig.TokenEndpoint;
// Get authorization token
Console.WriteLine("Getting authorization token ");
var accessToken = IngestionClient.GetClientCredentialAccessToken(
tokenEndpoint,
clientId,
clientSecret).Result;
Console.WriteLine("Creating a new add-on");
dynamic newIap = new
{
applicationIds = new List<string>() { appId },
productType = "Durable",
productId = "Sample-" + Guid.NewGuid().ToString(),
};
var client = new IngestionClient(accessToken, serviceEndpoint);
dynamic iapCreated = client.Invoke<dynamic>(
HttpMethod.Post,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.CreateInAppUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant),
requestContent: newIap).Result;
Console.WriteLine(iapCreated.ToString());
var iapId = iapCreated.id.Value as string;
// Create a new submission, which will be an exact copy of the last published submission
Console.WriteLine("Creating a new submission");
dynamic newSubmission = new
{
contentType = "BookDownload",
keywords = new List<string> { "book", "download" },
lifeTime = "ThreeDays",
targetPublishMode = "Immediate",
visibility = "Public",
pricing = new
{
priceId = "Free",
},
listings = new Dictionary<string, dynamic>()
{
{
"en-us",
new
{
description = "Sample IAP description",
title = "Sample IAP title",
icon = new
{
FileName = "icon300x300.png",
FileStatus = "PendingUpload",
},
}
}
}
};
// Because it's a new add-on, we are going to create a new submission instead of
// modifying the last published one. If you had a published add-on, you could
// pass "null" as request body to clone the latest published submission and then
// perform a PUT call. Alternatively, you can always post the new submission entirely
// even if you already have a published submission but you'll have to upload the image each time.
dynamic createdSubmission = client.Invoke<dynamic>(
HttpMethod.Post,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.InAppSubmissionUrl,
IngestionClient.Version,
IngestionClient.Tenant,
iapId),
requestContent: newSubmission).Result;
Console.WriteLine(createdSubmission);
var submissionId = createdSubmission.id.Value as string;
// Upload the zip archive with all new files to the SAS URL returned with the submission.
var fileUploadUrl = createdSubmission.fileUploadUrl.Value as string;
Console.WriteLine("FileUploadUrl: " + fileUploadUrl);
Console.WriteLine("Uploading file");
IngestionClient.UploadFileToBlob(@"..\..\files.zip", fileUploadUrl).Wait();
// Tell the system that we are done updating the submission.
// Update the submission
Console.WriteLine("Committing the submission");
client.Invoke<dynamic>(
HttpMethod.Post,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.InAppProductCommitSubmissionUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
iapId,
submissionId),
requestContent: null).Wait();
// Periodically check the status until it changes from "CommitsStarted" to either
// successful status or a failure.
Console.WriteLine("Waiting for the submission commit processing to complete. This may take a couple of minutes.");
string submissionStatus = null;
do
{
Task.Delay(TimeSpan.FromSeconds(5)).Wait();
dynamic statusResource = client.Invoke<dynamic>(
HttpMethod.Get,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.InAppSubmissionStatusUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
iapId,
submissionId),
requestContent: null).Result;
submissionStatus = statusResource.status.Value as string;
Console.WriteLine("Current status: " + submissionStatus);
}
while ("CommitStarted".Equals(submissionStatus));
if ("CommitFailed".Equals(submissionStatus))
{
Console.WriteLine("Submission has failed. Please check the Errors collection of the submissionResource response.");
return;
}
else
{
Console.WriteLine("Submission commit success!");
}
}
}
}
更新加载项提交
以下示例实现一个类,该类使用 Microsoft 应用商店提交 API 中的多种方法更新现有加载项提交。 该类中的 RunInAppProductSubmissionUpdateSample
方法创建新提交作为上次发布的提交的克隆,然后将克隆的提交更新并提交到合作伙伴中心。 具体来说,RunInAppProductSubmissionUpdateSample
方法执行以下任务:
- 首先,此方法获取指定加载项的数据。
- 接下来,此方法会删除加载项的挂起提交(如果存在)。
- 然后,该方法会 创建新的加载项提交(新提交是上次发布的提交的副本)。
- 接下来,它会更新并将新提交提交到合作伙伴中心。
- 最后,它会定期检查新提交的状态,直到其成功提交。
namespace DeveloperApiCSharpSample
{
using System;
using System.Globalization;
using System.Net.Http;
using System.Threading.Tasks;
/// <summary>
/// Sample code for how to update add-on submissions
/// </summary>
public class InAppProductSubmissionUpdateSample
{
private ClientConfiguration ClientConfig;
/// <summary>
/// Constructor
/// </summary>
/// <param name="c">An instance of ClientConfiguration that contains all parameters populated</param>
public InAppProductSubmissionUpdateSample(ClientConfiguration c)
{
this.ClientConfig = c;
}
public void RunInAppProductSubmissionUpdateSample()
{
// **********************
// SETTINGS
// **********************
var iapId = this.ClientConfig.InAppProductId;
var clientId = this.ClientConfig.ClientId;
var clientSecret = this.ClientConfig.ClientSecret;
var serviceEndpoint = this.ClientConfig.ServiceUrl;
var tokenEndpoint = this.ClientConfig.TokenEndpoint;
// Get authorization token
Console.WriteLine("Getting authorization token ");
var accessToken = IngestionClient.GetClientCredentialAccessToken(
tokenEndpoint,
clientId,
clientSecret).Result;
Console.WriteLine("Getting the add-on");
var client = new IngestionClient(accessToken, serviceEndpoint);
dynamic iap = client.Invoke<dynamic>(
HttpMethod.Get,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.GetInAppUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
iapId),
requestContent: null).Result;
Console.WriteLine(iap.ToString());
// Let's see if there is a pending submission. Warning! If it was created through the API,
// it will be deleted so that we could create a new one in its stead.
if (iap.pendingInAppProductSubmission != null)
{
var submissionId = iap.pendingInAppProductSubmission.id.Value as string;
// Let's try deleting it. If it was NOT created via the API, then you need to manually
// delete it from the dashboard. This is a safety measure to make sure that a human user and
// an automated system don't make conflicting edits.
Console.WriteLine("Deleting the pending submission");
client.Invoke<dynamic>(
HttpMethod.Delete,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.InAppSubmissionUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
iapId,
submissionId),
requestContent: null).Wait();
}
// Create a new submission, which will be an exact copy of the last published submission.
Console.WriteLine("Creating a new submission");
dynamic clonedSubmission = client.Invoke<dynamic>(
HttpMethod.Post,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.InAppSubmissionUrl,
IngestionClient.Version,
IngestionClient.Tenant,
iapId),
requestContent: null).Result;
var clonedSubmissionId = clonedSubmission.id.Value as string;
Console.WriteLine(clonedSubmission.ToString());
// Update the add-on price and keep the rest unchanged.
clonedSubmission.pricing.priceId = "Tier2"; // $0.99
// Because we are not uploading any new images, we don't need to upload the zip file.
// Update the submission.
Console.WriteLine("Updating the submission");
client.Invoke<dynamic>(
HttpMethod.Put,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.InAppSubmissionUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
iapId,
clonedSubmissionId),
requestContent: clonedSubmission).Wait();
// Tell the system that we are done updating the submission.
Console.WriteLine("Committing the submission");
client.Invoke<dynamic>(
HttpMethod.Post,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.InAppProductCommitSubmissionUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
iapId,
clonedSubmissionId),
requestContent: null).Wait();
// Periodically check the status until it changes from "CommitsStarted" to either
// successful status or a failure.
Console.WriteLine("Waiting for the submission commit processing to complete. This may take a couple of minutes.");
string submissionStatus = null;
do
{
Task.Delay(TimeSpan.FromSeconds(5)).Wait();
dynamic statusResource = client.Invoke<dynamic>(
HttpMethod.Get,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.InAppSubmissionStatusUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
iapId,
clonedSubmissionId),
requestContent: null).Result;
submissionStatus = statusResource.status.Value as string;
Console.WriteLine("Current status: " + submissionStatus);
}
while ("CommitStarted".Equals(submissionStatus));
if ("CommitFailed".Equals(submissionStatus))
{
Console.WriteLine("Submission has failed. Please check the Errors collection of the submissionResource response.");
return;
}
else
{
Console.WriteLine("Submission commit success! Here is the new price:");
dynamic sub = client.Invoke<dynamic>(
HttpMethod.Get,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.InAppSubmissionUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
iapId,
clonedSubmissionId),
requestContent: null).Result;
Console.WriteLine(sub.pricing.priceId.Value as string);
}
}
}
}
创建软件包外部测试版提交
以下示例实现一个类,该类使用 Microsoft 应用商店提交 API 中的多种方法更新软件包外部测试版提交。 该类中的 RunFlightSubmissionUpdateSample
方法创建新提交作为上次发布的提交的克隆,然后将克隆的提交更新并提交到合作伙伴中心。 具体来说,RunFlightSubmissionUpdateSample
方法执行以下任务:
- 首先,此方法获取指定软件包外部测试版的数据。
- 接下来,此方法会删除软件包外部测试版的挂起提交(如果存在)。
- 然后,此方法会创建新的软件包外部测试版提交(新提交时是上次发布的提交副本)。
- 它会将新的提交程序包上载到 Azure Blob 存储。
- 接下来,它会更新并将新提交提交到合作伙伴中心。
- 最后,它会定期检查新提交的状态,直到其成功提交。
namespace DeveloperApiCSharpSample
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
/// <summary>
/// Demonstrates how to update a flight submission with a new package
/// </summary>
public class FlightSubmissionUpdateSample
{
private ClientConfiguration ClientConfig { get; set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="c">An instance of ClientConfiguration that contains all parameters populated</param>
[DebuggerStepThrough]
public FlightSubmissionUpdateSample(ClientConfiguration c)
{
this.ClientConfig = c;
}
public void RunFlightSubmissionUpdateSample()
{
// **********************
// SETTINGS
// **********************
var appId = this.ClientConfig.ApplicationId;
var flightId = this.ClientConfig.FlightId;
var clientId = this.ClientConfig.ClientId;
var clientSecret = this.ClientConfig.ClientSecret;
var serviceEndpoint = this.ClientConfig.ServiceUrl;
var tokenEndpoint = this.ClientConfig.TokenEndpoint;
var scope = this.ClientConfig.Scope;
// Get authorization token
Console.WriteLine("Getting authorization token ");
var accessToken = IngestionClient.GetClientCredentialAccessToken(
tokenEndpoint,
clientId,
clientSecret,
scope).Result;
Console.WriteLine("Getting flight");
var client = new IngestionClient(accessToken, serviceEndpoint);
dynamic flight = client.Invoke<dynamic>(
HttpMethod.Get,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.GetFlightUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
appId,
flightId),
requestContent: null).Result;
Console.WriteLine(flight.ToString());
if (flight.pendingFlightSubmission != null)
{
var submissionId = flight.pendingFlightSubmission.id.Value as string;
// Let's try deleting it. If it was NOT creationg via the API, then you need to
// manually delete it from the dashboard. This is a safety measure to make sure that a
// human user and an automated system don't make conflicting edits.
Console.WriteLine("Deleting the pending submission");
client.Invoke<dynamic>(
HttpMethod.Delete,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.GetFlightSubmissionUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
appId,
flightId,
submissionId),
requestContent: null).Wait();
}
// Create a new submission, which will be an exact copy of the last published submission.
Console.WriteLine("Creating a new submission");
dynamic flightSubmission = client.Invoke<dynamic>(
HttpMethod.Post,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.CreateFlightSubmissionUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
appId,
flightId),
requestContent: null).Result;
// Update packages.
// Let's say we want to delete the existing package:
flightSubmission.flightPackages[0].fileStatus = "PendingDelete";
// Let's add a new package.
var packages = new List<dynamic>();
packages.Add(flightSubmission.flightPackages[0]);
packages.Add(
new
{
fileStatus = "PendingUpload",
fileName = "package.appx",
});
flightSubmission.flightPackages = JToken.FromObject(packages.ToArray());
var flightSubmissionId = flightSubmission.id.Value as string;
// Upload the zip archive with all new files to the SAS URL returned with the submission.
var fileUploadUrl = flightSubmission.fileUploadUrl.Value as string;
Console.WriteLine("FileUploadUrl: " + fileUploadUrl);
Console.WriteLine("Uploading file");
IngestionClient.UploadFileToBlob(@"..\..\files.zip", fileUploadUrl).Wait();
// Update the submission.
Console.WriteLine("Updating the submission");
client.Invoke<dynamic>(
HttpMethod.Put,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.GetFlightSubmissionUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
appId,
flightId,
flightSubmissionId),
requestContent: flightSubmission).Wait();
// Tell the system that we are done updating the submission.
Console.WriteLine("Committing the submission");
client.Invoke<dynamic>(
HttpMethod.Post,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.CommitFlightSubmissionUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
appId,
flightId,
flightSubmissionId),
requestContent: null).Wait();
// Periodically check the status until it changes from "CommitsStarted" to either
// successful status or a failure.
Console.WriteLine("Waiting for the submission commit processing to complete. This may take a couple of minutes.");
string submissionStatus = null;
do
{
Task.Delay(TimeSpan.FromSeconds(5)).Wait();
dynamic statusResource = client.Invoke<dynamic>(
HttpMethod.Get,
relativeUrl: string.Format(
CultureInfo.InvariantCulture,
IngestionClient.FlightSubmissionStatusUrlTemplate,
IngestionClient.Version,
IngestionClient.Tenant,
appId,
flightId,
flightSubmissionId),
requestContent: null).Result;
submissionStatus = statusResource.status.Value as string;
Console.WriteLine("Current status: " + submissionStatus);
}
while ("CommitStarted".Equals(submissionStatus));
if ("CommitFailed".Equals(submissionStatus))
{
Console.WriteLine("Submission has failed. Please check the Errors collection of the submissionResource response.");
return;
}
else
{
Console.WriteLine("Submission commit success!");
}
}
}
}
IngestionClient 帮助程序类
该 IngestionClient
类提供示例应用中其他方法用来执行以下任务的帮助程序方法:
- 获取可用于调用 Microsoft 应用商店提交 API 中方法的 Azure AD 访问令牌 。 获取令牌后,可以在 60 分钟的令牌有效期内,使用该令牌调用“Microsoft Store 提交 API”。 该令牌到期后,可以重新生成一个。
- 将包含新的应用或加载项提交资源的 ZIP 存档上传至 Azure Blob 存储。 有关将 ZIP 存档上传至应用和加载项提交的 Azure Blob 存储的详细信息,请参阅 创建应用提交 和 创建加载项提交 中的相关说明。
- 处理 Microsoft Store 提交 API 的 HTTP 请求。
namespace DeveloperApiCSharpSample
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Microsoft.WindowsAzure.Storage.Blob;
/// <summary>
/// This class is a proxy that abstracts the functionality of the API service
/// </summary>
public class IngestionClient : IDisposable
{
public static readonly string Version = "1.0";
public static readonly string Tenant = "my";
public static readonly string GetSubmissionUrlTemplate = "/v{0}/{1}/applications/{2}/submissions/{3}";
public static readonly string CommitSubmissionUrlTemplate = "/v{0}/{1}/applications/{2}/submissions/{3}/commit";
public static readonly string UpdateUrlTemplate = "/v{0}/{1}/applications/{2}/submissions/{3}/";
public static readonly string ApplicationUrl = "/v{0}/{1}/applications";
public static readonly string ApplicationUrlWithContinuation = "/v{0}/{1}/{2}";
public static readonly string GetApplicationUrlTemplate = "/v{0}/{1}/applications/{2}";
public static readonly string GetApplicationIapsWithContinuationUrlTemplate = "/v{0}/{1}/{2}";
public static readonly string CreateSubmissionUrlTemplate = "/v{0}/{1}/applications/{2}/submissions";
public static readonly string GetApplicationIapsUrlTemplate = "/v{0}/{1}/applications/{2}/listinappproducts";
public static readonly string CreateInAppUrlTemplate = "/v{0}/{1}/inappproducts";
public static readonly string GetInAppUrlTemplate = "/v{0}/{1}/inappproducts/{2}";
public static readonly string InAppSubmissionUrlTemplate = "/v{0}/{1}/inappproducts/{2}/submissions/{3}";
public static readonly string InAppSubmissionUrl = "/v{0}/{1}/inappproducts/{2}/submissions";
public static readonly string InAppProductCommitSubmissionUrlTemplate = "/v{0}/{1}/inappproducts/{2}/submissions/{3}/commit";
public static readonly string GetApplicationFlightsUrlTemplate = "/v{0}/{1}/applications/{2}/listflights";
public static readonly string GetApplicationFlightsWithContinuationUrlTemplate = "/v{0}/{1}/{2}";
public static readonly string CreateNewFlightUrlTemplate = "/v{0}/{1}/applications/{2}/flights";
public static readonly string GetFlightUrlTemplate = "/v{0}/{1}/applications/{2}/flights/{3}";
public static readonly string CreateFlightSubmissionUrlTemplate = "/v{0}/{1}/applications/{2}/flights/{3}/submissions";
public static readonly string GetFlightSubmissionUrlTemplate = "/v{0}/{1}/applications/{2}/flights/{3}/submissions/{4}";
public static readonly string CommitFlightSubmissionUrlTemplate = "/v{0}/{1}/applications/{2}/flights/{3}/submissions/{4}/commit";
public static readonly string FlightSubmissionStatusUrlTemplate = "/v{0}/{1}/applications/{2}/flights/{3}/submissions/{4}/status";
public static readonly string ApplicationSubmissionStatusUrlTemplate = "/v{0}/{1}/applications/{2}/submissions/{3}/status";
public static readonly string InAppSubmissionStatusUrlTemplate = "/v{0}/{1}/inappproducts/{2}/submissions/{3}/status";
private HttpClient httpClient;
private readonly string accessToken;
/// <summary>
/// Initializes a new instance of the <see cref="IngestionClient" /> class.
/// </summary>
/// <param name="accessToken">
/// The acces token. This is JWT a token obtained from AAD allowing the caller to invoke the API
/// on behalf of a user
/// </param>
/// <param name="serviceUrl">The service URL.</param>
public IngestionClient(string accessToken, string serviceUrl)
{
if (string.IsNullOrEmpty(accessToken))
{
throw new ArgumentNullException("accessToken");
}
if (string.IsNullOrEmpty(serviceUrl))
{
throw new ArgumentNullException("serviceUrl");
}
this.accessToken = accessToken;
this.httpClient = new HttpClient
{
BaseAddress = new Uri(serviceUrl)
};
this.DefaultHeaders = new Dictionary<string, string>();
}
/// <summary>
/// Gets the default headers.
/// </summary>
public Dictionary<string, string> DefaultHeaders { get; private set; }
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting
/// unmanaged resources.
/// </summary>
public void Dispose()
{
if (this.httpClient != null)
{
this.httpClient.Dispose();
this.httpClient = null;
GC.SuppressFinalize(this);
}
}
/// <summary>
/// Gets the authorization token for the provided client id, client secret, and the scope.
/// This token is usually valid for 1 hour, so if your submission takes longer than that to complete,
/// make sure to get a new one periodically.
/// </summary>
/// <param name="tokenEndpoint">Token endpoint to which the request is to be made. Specific to your
/// AAD app. Example: https://login.windows.net/d454d300-128e-2d81-334a-27d9b2baf002/oauth2/token </param>
/// <param name="clientId">Client Id of your AAD app. Example" ba3c223b-03ab-4a44-aa32-38aa10c27e32</param>
/// <param name="clientSecret">Client secret of your AAD app</param>
/// <param name="scope">Scope. If not provided, default one is used for the production API endpoint.</param>
/// <returns>Autorization token. Prepend it with "Bearer: " and pass it in the request header as the
/// value for "Authorization: " header.</returns>
public static async Task<string> GetClientCredentialAccessToken(
string tokenEndpoint,
string clientId,
string clientSecret,
string scope = null)
{
if (scope == null)
{
scope = "https://manage.devcenter.microsoft.com";
}
dynamic result;
using (HttpClient client = new HttpClient())
{
string tokenUrl = tokenEndpoint;
using (
HttpRequestMessage request = new HttpRequestMessage(
HttpMethod.Post,
tokenUrl))
{
string strContent =
string.Format(
"grant_type=client_credentials&client_id={0}&client_secret={1}&resource={2}",
clientId,
clientSecret,
scope);
request.Content = new StringContent(strContent, Encoding.UTF8,
"application/x-www-form-urlencoded");
using (HttpResponseMessage response = await client.SendAsync(request))
{
string responseContent = await response.Content.ReadAsStringAsync();
result = JsonConvert.DeserializeObject(responseContent);
}
}
}
return result.access_token;
}
/// <summary>
/// Uploads a file to blob using a SAS url
/// </summary>
/// <param name="fileName">Path to your zip file</param>
/// <param name="sasUrl">The SAS url which was returned to you when you cloned the submission
/// in FileUploadUrl</param>
/// <returns>A task which will complete when the file finishes uploading</returns>
public static async Task UploadFileToBlob(string fileName, string sasUrl)
{
using (Stream stream = new FileStream(fileName, FileMode.Open))
{
var blockBob = new CloudBlockBlob(new Uri(sasUrl));
await blockBob.UploadFromStreamAsync(stream);
}
}
/// <summary>
/// Invokes the specified HTTP method.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="httpMethod">The HTTP method.</param>
/// <param name="relativeUrl">The relative URL.</param>
/// <param name="requestContent">Content of the request.</param>
/// <returns>instance of the type T</returns>
/// <exception cref="ServiceException"></exception>
public async Task<T> Invoke<T>(HttpMethod httpMethod,
string relativeUrl,
object requestContent)
{
using (var request = new HttpRequestMessage(httpMethod, relativeUrl))
{
this.SetRequest(request, requestContent);
using (HttpResponseMessage response = await this.httpClient.SendAsync(request))
{
T result;
if (this.TryHandleResponse(response, out result))
{
return result;
}
if (response.IsSuccessStatusCode)
{
var resource = JsonConvert.DeserializeObject<T>(await response.Content.ReadAsStringAsync());
return resource;
}
throw new Exception(response.Content.ReadAsStringAsync().Result);
}
}
}
/// <summary>
/// Sets the request.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="requestContent">Content of the request.</param>
protected virtual void SetRequest(HttpRequestMessage request, object requestContent)
{
request.Headers.Add(Constants.RequestHeaders.CorrelationIdHeader, Guid.NewGuid().ToString());
request.Headers.Add(Constants.RequestHeaders.MSRequestIdHeader, Guid.NewGuid().ToString());
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this.accessToken);
foreach (var header in this.DefaultHeaders)
{
request.Headers.Add(header.Key, header.Value);
}
if (requestContent != null)
{
request.Content = new StringContent(JsonConvert.SerializeObject(requestContent),
Encoding.UTF8,
Constants.HttpMimeTypes.JsonContentType);
}
}
/// <summary>
/// Tries the handle response.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="response">The response.</param>
/// <param name="result">The result.</param>
/// <returns>true if the response was handled</returns>
protected virtual bool TryHandleResponse<T>(HttpResponseMessage response, out T result)
{
result = default(T);
return false;
}
private static class Constants
{
public static class RequestHeaders
{
/// <summary>
/// Corresponds to TraceCorrelationId in SLL. This is a GUID that is newly generated
/// by FD for every request coming from the client.
/// </summary>
public const string CorrelationIdHeader = "MS-CorrelationId";
/// <summary>
/// Corresponds to RequestCorrelationId in SLL. This is a GUID that is newly generated
/// by FD for every request that it makes to the downstream services.
/// </summary>
public const string MSRequestIdHeader = "MS-RequestId";
}
public static class HttpMimeTypes
{
/// <summary>
/// The json content type
/// </summary>
public const string JsonContentType = "application/json";
}
}
}
}