C# 샘플: 앱, 추가 기능 및 플라이트 제출하기
이 문서는 이런 작업에 Microsoft Store 제출 API를 사용하는 방법을 설명하는 C# 코드 예제를 제공합니다.
각 예시를 검토하여 예시에서 보여 주는 작업에 대해 자세히 알아보거나 이 문서의 모든 코드 예시를 콘솔 애플리케이션으로 빌드할 수 있습니다. 예제를 빌드하려면 Visual Studio에서 DeveloperApiCSharpSample이라는 C# 콘솔 애플리케이션을 만들고 각 예제를 프로젝트의 별도 코드 파일에 복사하여 프로젝트를 빌드합니다.
필수 조건
이러한 예시는 다음과 같은 라이브러리를 사용합니다.
- Microsoft.WindowsAzure.Storage.dll. 이 라이브러리는 .NET용 Azure SDK에서 이용할 수 있거나 WindowsAzure.Storage NuGet 패키지를 설치하여 가져올 수 있습니다.
- Newtonsoft의 Newtonsoft.Json NuGet 패키지.
주요 프로그램
다음의 예제는 Microsoft Store 제출 API를 사용하는 여러 가지 방법을 보여 주기 위해 이 문서의 다른 예제 메서드를 호출하는 명령줄 프로그램을 구현합니다. 다음을 따라 이 프로그램을 자신의 용도에 맞게 조정합니다.
- 관리하려는 앱, 추가 기능, 패키지 플라이트의 ID에
ApplicationId
,InAppProductId
및FlightId
속성을 할당합니다. - 앱의 클라이언트 ID 및 키에
ClientId
속성 및ClientSecret
속성을 할당하고TokenEndpoint
URL의 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
도우미 클래스를 사용하여 Microsoft Store 제출 API를 사용하는 예제 메서드 각각에 Azure Active Directory 데이터 및 앱 데이터를 전달합니다.
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 Storage에 업로드합니다.
- 다음으로, 파트너 센터에 새 제출을 업데이트한 다음, 커밋합니다.
- 마지막으로 제출이 성공적으로 커밋될 때까지 새 제출의 상태를 주기적으로 확인합니다.
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 Store 제출 API에서 여러 메서드를 사용하여 새 추가 기능 제출을 만드는 클래스를 구현합니다. 클래스의 RunInAppProductSubmissionCreateSample
메서드는 다음과 같은 작업을 수행합니다.
- 먼저 이 메서드로 새 추가 기능을 만듭니다.
- 다음으로 추가 기능에 대한 새 제출을 생성합니다.
- 제출할 아이콘이 포함된 ZIP 보관 파일을 Azure Blob Storage에 업로드합니다.
- 다음으로 파트너 센터에 새 제출을 커밋합니다.
- 마지막으로 제출이 성공적으로 커밋될 때까지 새 제출의 상태를 주기적으로 확인합니다.
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 Store 제출 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 Store 제출 API에서 여러 메서드를 사용하여 패키지 플라이트 제출을 업데이트하는 클래스를 구현합니다. 이 클래스의 RunFlightSubmissionUpdateSample
메서드는 마지막으로 게시된 제출의 복제본으로 새 제출을 만든 다음, 복제한 제출을 파트너 센터에 업데이트하고 커밋합니다. 특히 RunFlightSubmissionUpdateSample
메서드는 다음과 같은 작업을 수행합니다.
- 먼저 이 메서드로 지정된 패키지 플라이트에 대한 데이터 가져오기를 합니다.
- 다음으로 패키지 플라이트(있는 경우)에 대한 보류 중인 제출을 삭제합니다.
- 그런 다음 패키지 플라이트에 대한 새 제출을 만듭니다(새 제출은 마지막으로 게시된 제출의 복사본).
- 제출할 새 패키지를 Azure Blob Storage에 업로드합니다.
- 다음으로, 파트너 센터에 새 제출을 업데이트한 다음, 커밋합니다.
- 마지막으로 제출이 성공적으로 커밋될 때까지 새 제출의 상태를 주기적으로 확인합니다.
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 Store 제출 API에서 메서드를 호출하는 데 사용할 수 있는 Azure AD 액세스 토큰을 가져옵니다. 토큰을 가져온 후 만료되기 전에 이 토큰을 Microsoft Store 제출 API에 대한 호출에 사용할 수 있는 시간은 60분입니다. 토큰이 만료된 후 새 토큰을 생성할 수 있습니다.
- 앱 또는 추가 기능 제출을 위해 새 자산이 포함된 ZIP 보관 파일을 Azure Blob Storage에 업로드합니다. 앱 및 추가 기능 제출을 위해 ZIP 보관 파일을 Azure Blob Storage에 업로드하는 방법에 대한 자세한 내용은 앱 제출 만들기 및 추가 기능 제출 만들기의 관련 지침을 참조하세요.
- 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";
}
}
}
}