C# 範例:提交含遊戲選項與預告片的應用程式
本文提供了 C# 程式碼範例,示範如何使用 Microsoft Store 提交 API 執行以下任務:
- 取得 Azure AD 存取權杖,以搭配 Microsoft Store 提交 API 使用。
- 建立應用程式提交
- 設定應用程式提交的 Store 清單,包括遊戲和預告片進階清單選項。
- 上傳包含應用程式提交的套件、清單影像和預告片檔案的 ZIP 檔案。
- 認可應用程式提交。
您可以檢閱每個範例,以深入了解其中示範的工作,也可以將本文中的所有程式碼範例建置到主控台應用程式。 若要建置範例,請在 Visual Studio 中建立名為 DevCenterApiSample 的 C# 主控台應用程式、將每個範例複製到專案中的個別程式碼檔案,然後建置專案。
必要條件
這些範例具有下列需求:
- 在專案中新增 System.Web 組件的參考。
- 將 Newtonsoft.Json NuGet 套件從 Newtonsoft 安裝到您的專案。
建立應用程式提交
CreateAndSubmitSubmissionExample
類別定義了一個公用 Execute
方法,呼叫其他範例方法以使用 Microsoft Store 提交 API 來建立和提交包含遊戲選項和預告片的應用程式提交。 若要調整此程式碼以供您自己使用:
- 將
tenantId
變數指派給應用程式的租用戶識別碼,並為您應用程式的用戶端識別碼和金鑰指派clientId
和clientSecret
變數。 如需詳細資訊,請參閱如何將 Azure AD 應用程式與您的合作夥伴中心帳戶產生關聯 - 將
applicationId
變數指派給您所要建立提交應用程式的 Store 識別碼。
using System;
using System.Threading;
using Newtonsoft.Json.Linq;
namespace DevCenterApiSample
{
public class CreateAndSubmitSubmissionExample
{
public static void Execute()
{
// Add your tenant ID, client ID, and client secret here.
string tenantId = "";
string clientId = "";
string clientSecret = "";
var accessTokenClient = new DevCenterAccessTokenClient(tenantId, clientId, clientSecret);
string accessToken = accessTokenClient.GetAccessToken("https://manage.devcenter.microsoft.com");
var devCenter = new DevCenterClient(accessToken);
// The application ID is taken from your app dashboard page's URI in Dev Center,
// e.g. https://developer.microsoft.com/en-us/dashboard/apps/{application_id}/
string applicationId = "{application_id}";
// Get the application object, and cancel any in progress submissions.
JObject app = devCenter.GetApplication(applicationId);
JToken inProgressSubmission = app.GetValue("pendingApplicationSubmission");
if (inProgressSubmission != null)
{
string inProgressSubmissionId = inProgressSubmission.Value<string>("id");
devCenter.CancelInProgressSubmission(applicationId, inProgressSubmissionId);
}
// Create a new submission, based on the last published submission.
JObject submission = devCenter.CreateSubmission(applicationId);
string submissionId = submission.GetValue("id").Value<string>();
// The following fields are required.
submission["applicationCategory"] = "Games_Fighting";
submission["listings"] = GetListingsObject();
submission["pricing"] = GetPricingObject();
submission["packages"] = new JArray() { GetPackageObject() };
submission["allowTargetFutureDeviceFamilies"] = GetDeviceFamiliesObject();
// The app must have the hasAdvancedListingPermission set to True in order for gaming options
// and trailers to be applied. If that's not the case, you can still update the app and
// its submissions through the API, but gaming options and trailers won't be saved.
if (app["hasAdvancedListingPermission"] == null || app["hasAdvancedListingPermission"].Value<bool>() == false)
{
Console.WriteLine("This application does not support gaming options or trailers.");
}
else
{
// Gaming options is an array. A maximum of one value may be provided.
submission["gamingOptions"] = new JArray(GetGamingOptionsObject());
// A maximum of 15 trailers may be provided in the trailers array.
submission["trailers"] = new JArray(GetTrailerObject());
}
// Continue updating the submission_json object with additional options as needed.
// After you've finished, call the Update API with the code below to save it.
JObject updatedSubmission = devCenter.UpdateSubmission(applicationId, submissionId, submission);
// All images and packages should be located in a single ZIP file. In the submission JSON,
// the file names for all objects requiring them (icons, packages, etc.) must exactly
// match the file names from the ZIP file.
string zipFilePath = "";
devCenter.UploadZipFileForSubmission(applicationId, submissionId, zipFilePath);
// Committing the submission will start the submission process for it. Once committed,
// the submission can no longer be changed.
devCenter.CommitSubmission(applicationId, submissionId);
// After committing, you can poll the commit API for the status of the submission's process using
// the following code.
bool waitingForCommitToStart = true;
while (waitingForCommitToStart)
{
string status = devCenter.GetSubmissionStatus(applicationId, submissionId);
Console.WriteLine($"Submission status: {status}");
waitingForCommitToStart = status.Equals("CommitStarted");
if (waitingForCommitToStart)
{
Thread.Sleep(TimeSpan.FromMinutes(1)); // Wait to check Dev Center again.
}
}
}
private static JObject GetListingsObject()
{
// This structure holds basic information to display in the store.
var baseListing = new JObject();
baseListing.Add("copyrightAndTrademarkInfo", "(C) 2017 Microsoft");
baseListing.Add("licenseTerms", "http://example.com/licenseTerms.aspx");
baseListing.Add("privacyPolicy", "http://example.com/privacyPolicy.aspx");
baseListing.Add("supportContact", "support@example.com");
baseListing.Add("websiteUrl", "http://example.com");
baseListing.Add("description", "A sample game showing off gameplay options code.");
baseListing.Add("releaseNotes", "Initial release");
// The title of the app must match a reserved name for the app in Dev Center.
// If it doesn't, attempting to update the submission will fail.
baseListing.Add("title", "Super Game Options API Simulator 2017");
var keywords = new JArray();
keywords.Add("SampleApp");
keywords.Add("SampleFightingGame");
keywords.Add("GameOptions");
baseListing.Add("keywords", keywords);
var features = new JArray();
features.Add("Doesn't crash");
features.Add("Likes to eat chips");
baseListing.Add("features", features);
// If your app works better with specific hardware (or needs it), you can
// add or update values here.
var hardwarePreferences = new JArray()
{
"Keyboard",
"Mouse"
};
baseListing.Add("hardwarePreferences", hardwarePreferences);
var images = new JArray();
// There are several types of images available; at least one screenshot
// is required.
var image = new JObject();
// The file name is relative to the root of the uploaded ZIP file.
image.Add("fileName", "img/screenshot.png");
image.Add("description", "A basic screenshot of the app.");
image.Add("imageType", "Screenshot");
images.Add(image);
baseListing.Add("images", images);
var listing = new JObject();
listing.Add("baseListing", baseListing);
// If there are any specific overrides to above information for Windows 8,
// Windows 8.1, Windows Phone 7.1, 8.0, or 8.1, you can add information here.
listing.Add("platformOverrides", new JObject());
// Each listing is targeted at a specific language-locale code, e.g. EN-US.
var listings = new JObject();
listings.Add("en-us", listing);
return listings;
}
private static JObject GetPackageObject()
{
var package = new JObject()
{
// The file name is relative to the root of the uploaded ZIP file.
["fileName"] = "bin/super_dev_ctr_api_sim.appxupload",
// If you haven't begun to upload the file yet, set this value to "PendingUpload".
["fileStatus"] = "PendingUpload"
};
return package;
}
private static JObject GetPricingObject()
{
var pricing = new JObject();
// How long the trial period is, if one is allowed. Valid values are NoFreeTrial,
// OneDay, SevenDays, FifteenDays, ThirtyDays, or TrialNeverExpires.
pricing.Add("trialPeriod", "NoFreeTrial");
// Maps to the default price for the app.
pricing.Add("priceId", "Free");
// If you'd like to offer your app in different markets at different prices, you
// can provide priceId values per language/locale code.
pricing.Add("marketSpecificPricing", new JObject());
return pricing;
}
private static JObject GetDeviceFamiliesObject()
{
var futureDeviceFamilies = new JObject();
// Supported values are Desktop, Mobile, Xbox, and Holographic. To make
// the app available on that specific platform, set the value to True.
futureDeviceFamilies.Add("Desktop", true);
futureDeviceFamilies.Add("Mobile", false);
futureDeviceFamilies.Add("Xbox", true);
futureDeviceFamilies.Add("Holographic", false);
return futureDeviceFamilies;
}
private static JObject GetTrailerObject()
{
// Add an example trailer.
var trailer = new JObject();
// This is the filename of the trailer. The file name is a relative path to the
// root of the ZIP file to be uploaded to the API.
trailer["VideoFileName"] = "trailers/main/my_awesome_trailer.mpeg";
// Aside from the video itself, a trailer can have image assets such as screenshots
// or alternate images.
var trailerAssets = new JObject();
trailer["TrailerAssets"] = trailerAssets;
// Add trailer assets for the EN-US market.
var trailerAsset = new JObject();
trailerAssets["en-us"] = trailerAsset;
// The title of the trailer to display in the store.
trailerAsset["Title"] = "Main Trailer";
// The list of images provided with the trailer that are shown
// when the trailer isn't playing.
var imageList = new JArray();
trailerAsset["ImageList"] = imageList;
// Add a few images to the image list.
var thumbnailImage = new JObject()
{
// The file name of the image. The file name is a relative
// path to the root of the ZIP
// file to be uploaded to the API.
["FileName"] = "trailers/main/thumbnail.png",
// A plaintext description of what the image represents.
["Description"] = "The thumbnail for the trailer shown " + "before the user clicks play"
};
imageList.Add(thumbnailImage);
var altImage = new JObject()
{
["FileName"] = "trailers/main/alt-img.png",
["Description"] = "The image to show after the trailer plays"
};
imageList.Add(altImage);
return trailer;
}
private static JObject GetGamingOptionsObject()
{
var gamingOptions = new JObject();
// The genres of your game.
var genres = new JArray();
genres.Add("Games_Fighting");
gamingOptions["genres"] = genres;
// Set this to true if your game supports local multiplayer. This field
// is required.
gamingOptions["isLocalMultiplayer"] = true;
// If local multiplayer is supported, you must provide the minimum and
// maximum players supported. Valid values are between 2 and 1000 inclusive.
gamingOptions["localMultiplayerMinPlayers"] = 2;
gamingOptions["localMultiplayerMaxPlayers"] = 4;
// Set this to True if your game supports local co-op play. This field is required.
gamingOptions["isLocalCooperative"] = true;
// If local co-op is supported, you must provide the minimum and maximum players
// supported. Valid values are between 2 and 1000 inclusive.
gamingOptions["localCooperativeMinPlayers"] = 2;
gamingOptions["localCooperativeMaxPlayers"] = 4;
// Set this to True if your game supports online multiplayer. This field is required.
gamingOptions["isOnlineMultiplayer"] = true;
// If online multiplayer is supported, you must provide the minimum and maximum players
// supported. Valid values are between 2 and 1000 inclusive.
gamingOptions["onlineMultiplayerMinPlayers"] = 2;
gamingOptions["onlineMultiplayerMaxPlayers"] = 4;
// Set this to true if your game supports online co-op play. This field is required.
gamingOptions["isOnlineCooperative"] = true;
// If online co-op is supported, you must provide the minimum and maximum players
// supported. Valid values are between 2 and 1000 inclusive.
gamingOptions["onlineCooperativeMinPlayers"] = 2;
gamingOptions["onlineCooperativeMaxPlayers"] = 4;
// If your game supports broadcasting a stream to other players, set this field to True.
// This field is required.
gamingOptions["isBroadcastingPrivilegeGranted"] = true;
// If your game supports cross-device play (e.g. a player can play on an Xbox One with
// their friend who's playing on a PC), set this field to True. This field is required.
gamingOptions["isCrossPlayEnabled"] = true;
// If your game supports Kinect usage, set this field to "Enabled", otherwise, set it to
// "Disabled". This field is required.
gamingOptions["kinectDataForExternal"] = "Disabled";
// Free text about any other peripherals that your game supports. This field is optional.
gamingOptions["otherPeripherals"] = "Supports the usage of all fighting joysticks.";
return gamingOptions;
}
}
}
取得 Azure AD 存取權杖
DevCenterAccessTokenClient
類別會定義協助程式方法,該方法會使用您的 tenantId
、clientId
和 clientSecret
值來建立 Azure AD 存取權杖,以搭配 Microsoft Store 提交 API 使用。
using System.Net.Http;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DevCenterApiSample
{
/// <summary>
/// A client for getting access tokens to the Dev Center API.
/// </summary>
public class DevCenterAccessTokenClient
{
private string _tenantId;
private string _clientId;
private string _clientSecret;
/// <summary>
/// Creates a new instance of the <see cref="DevCenterAccessTokenClient"/> class.
/// </summary>
/// <param name="tenantId">The AAD tenant ID.</param>
/// <param name="clientId">The AAD client ID.</param>
/// <param name="clientSecret">The AAD client secret.</param>
public DevCenterAccessTokenClient(string tenantId, string clientId, string clientSecret)
{
_tenantId = tenantId;
_clientId = clientId;
_clientSecret = clientSecret;
}
/// <summary>
/// Generates an access token to the specified resource URI.
/// </summary>
/// <param name="resource">The resource URI.</param>
/// <returns>An access token for authentication.</returns>
public string GetAccessToken(string resource)
{
// Generate access token. Access token is valid for 1 hour. Regenerate access token when needed
HttpRequestMessage tokenRequest = new HttpRequestMessage(HttpMethod.Post, $"https://login.microsoftonline.com/{_tenantId}/oauth2/token");
string tokenRequestBody = $"grant_type=client_credentials&client_id={_clientId}&client_secret={_clientSecret}&resource={resource}";
tokenRequest.Content = new StringContent(tokenRequestBody, Encoding.UTF8, "application/x-www-form-urlencoded");
HttpClient client = new HttpClient();
HttpResponseMessage response = client.SendAsync(tokenRequest).GetAwaiter().GetResult();
string responseBody = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
JObject responseJson = (JObject)JsonConvert.DeserializeObject(responseBody);
tokenRequest.Dispose();
client.Dispose();
response.Dispose();
return responseJson["access_token"].Value<string>() ?? string.Empty;
}
}
}
叫用提交 API 並上傳提交檔案的協助程式方法
DevCenterClient
類別會定義協助程式方法,可在 Microsoft Store 提交 API 中叫用各種方法,並上傳包含應用程式提交的套件、清單影像和預告片檔案的 ZIP 檔案。
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Web;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.IO;
namespace DevCenterApiSample
{
/// <summary>
/// A client for accessing the Dev Center APIs.
/// </summary>
public class DevCenterClient
{
private string _accessToken;
private Uri _baseUri;
/// <summary>
/// Creates a new instance of the <see cref="DevCenterClient"/> class.
/// </summary>
/// <param name="accessToken">The access token to authenticate to the service with.</param>
public DevCenterClient(string accessToken)
{
_baseUri = new Uri("https://manage.devcenter.microsoft.com");
_accessToken = accessToken;
}
/// <summary>
/// Retrieves the JSON object representing the application from Dev Center.
/// </summary>
/// <param name="applicationId">The application ID.</param>
/// <returns>A JObject that may be navigated.</returns>
/// <remarks>
/// The application ID is taken from your app dashboard page's URI in Dev Center,
/// e.g. https://developer.microsoft.com/en-us/dashboard/apps/{application_id}/
/// </remarks>
public JObject GetApplication(string applicationId)
=> Invoke(HttpMethod.Get, $"/v1.0/my/applications/{applicationId}");
/// <summary>
/// Cancels an in-progress submission for the app.
/// </summary>
/// <param name="applicationId">The application ID.</param>
/// <param name="submissionId">The submission ID.</param>
/// <returns></returns>
public void CancelInProgressSubmission(string applicationId, string submissionId)
=> Invoke(HttpMethod.Delete, $"/v1.0/my/applications/{applicationId}/submissions/{submissionId}");
/// <summary>
/// Creates a new in-progress submission for the application.
/// </summary>
/// <param name="applicationId">The application ID.</param>
/// <returns>A JObject that may navigated.</returns>
public JObject CreateSubmission(string applicationId)
=> Invoke(HttpMethod.Post, $"/v1.0/my/applications/{applicationId}/submissions");
/// <summary>
/// Updates the submission with the new data provided.
/// </summary>
/// <param name="applicationId">The application ID.</param>
/// <param name="submissionId">The submission ID.</param>
/// <param name="submission">The submission body.</param>
/// <returns>The updated submission JObject.</returns>
public JObject UpdateSubmission(string applicationId, string submissionId, JObject submission)
=> Invoke(HttpMethod.Put, $"/v1.0/my/applications/{applicationId}/submissions/{submissionId}", submission);
/// <summary>
/// Gets the submission from Dev Center.
/// </summary>
/// <param name="applicationId">The application ID.</param>
/// <param name="submissionId">The submission ID.</param>
/// <returns>The submission object from Dev Center.</returns>
public JObject GetSubmission(string applicationId, string submissionId)
=> Invoke(HttpMethod.Get, $"/v1.0/my/applications/{applicationId}/submissions/{submissionId}");
/// <summary>
/// Commits the submission to Dev Center.
/// </summary>
/// <param name="applicationId">The application ID.</param>
/// <param name="submissionId">The submission ID.</param>
/// <remarks>
/// Once a submission is committed, Dev Center will begin processing and certifying it;
/// it can no longer be changed after this point.
/// </remarks>
public void CommitSubmission(string applicationId, string submissionId)
=> Invoke(HttpMethod.Post, $"/v1.0/my/applications/{applicationId}/submissions/{submissionId}/commit");
/// <summary>
/// Returns the current submission commit status.
/// </summary>
/// <param name="applicationId">The application ID.</param>
/// <param name="submissionId">The submission ID.</param>
/// <returns>The submission status.</returns>
public string GetSubmissionStatus(string applicationId, string submissionId)
{
JObject response = GetSubmission(applicationId, submissionId);
string status = response.Value<string>("status") ?? "Unknown";
return status;
}
/// <summary>
/// Uploads the ZIP file containing assets for the submission to the submission in Dev Center.
/// </summary>
/// <param name="applicationId">The application ID.</param>
/// <param name="submissionId">The submission ID.</param>
/// <param name="zipFilePath">The path to the ZIP file.</param>
public void UploadZipFileForSubmission(string applicationId, string submissionId, string zipFilePath)
{
JObject submission = GetSubmission(applicationId, submissionId);
string fileUploadUrl = submission["fileUploadUri"].Value<string>();
HttpRequestMessage uploadRequest = new HttpRequestMessage(HttpMethod.Put, fileUploadUrl.Replace("+", "%2B")); // Encode '+', otherwise it will be decoded as ' '
uploadRequest.Content = new StreamContent(File.OpenRead(zipFilePath));
uploadRequest.Headers.Add("x-ms-blob-type", "BlockBlob");
HttpClient httpClient = new HttpClient();
HttpResponseMessage uploadResponse = httpClient.SendAsync(uploadRequest).GetAwaiter().GetResult();
uploadResponse.EnsureSuccessStatusCode();
uploadRequest.Dispose();
uploadResponse.Dispose();
httpClient.Dispose();
}
private JObject Invoke(HttpMethod method, string path, JObject body = null)
{
HttpRequestMessage request = new HttpRequestMessage(method, new Uri(_baseUri, path));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
request.Headers.UserAgent.ParseAdd("C-Sharp");
if (body != null)
{
request.Content = new StringContent(body.ToString(), Encoding.UTF8, "application/json");
}
HttpClient client = new HttpClient();
HttpResponseMessage response = client.SendAsync(request).GetAwaiter().GetResult();
string responseContent = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (!response.IsSuccessStatusCode)
{
string message = string.IsNullOrEmpty(responseContent) ? response.ReasonPhrase : responseContent;
throw new HttpException((int)response.StatusCode, message);
}
if (string.IsNullOrEmpty(responseContent))
{
return null;
}
client.Dispose();
request.Dispose();
response.Dispose();
JObject responseObject = (JObject)JsonConvert.DeserializeObject(responseContent);
return responseObject;
}
}
}