C # 範例:使用遊戲選項和尾端提交應用程式C# sample: app submission with game options and trailers

本文提供 C# 程式碼範例,示範如何使用 Microsoft Store 提交 API來執行這些任務:This article provides C# code examples that demonstrate how to use the Microsoft Store submission API for these tasks:

  • 取得 Azure AD 存取權杖以便用於 Microsoft Store 提交 API。Obtain an Azure AD access token to use with the Microsoft Store submission API.
  • 建立應用程式提交Create an app submission
  • 設定 App 提交的 Store 清單資料,包括遊戲預告片進階清單選項。Configure Store listing data for the app submission, including the gaming and trailers advanced listing options.
  • 上傳包含 App 提交的套件、清單影像和預告片檔案的 ZIP 檔案。Upload the ZIP file containing the packages, listing images, and trailer files for the app submission.
  • 認可 App 提交。Commit the app submission.

您可以檢閱每個範例以深入了解示範的工作,或者您可以將本篇文章中的所有程式碼範例建置到主控台應用程式。You can review each example to learn more about the task it demonstrates, or you can build all the code examples in this article into a console application. 若要建置範例,請在 Visual Studio 中建立名為 DevCenterApiSample 的 C# 主控台應用程式,分別將每個範例複製到專案中單獨的程式碼檔案,然後建置專案。To build the examples, create a C# console application named DevCenterApiSample in Visual Studio, copy each example to a separate code file in the project, and build the project.

先決條件Prerequisites

這些範例包含下列必要條件:These examples have the following requirements:

  • 新增參考至您專案中的 System.Web 組件。Add a reference to the System.Web assembly in your project.
  • 從 Newtonsoft 安裝 Newtonsoft.Json NuGet 套件到您的專案。Install the Newtonsoft.Json NuGet package from Newtonsoft to your project.

建立應用程式提交Create an app submission

CreateAndSubmitSubmissionExample 類別定義公用 Execute 方法,此方法會呼叫其他範例方法,以使用 Microsoft Store 提交 API 來建立並認可包含遊戲選項和預告片的 App 提交。The CreateAndSubmitSubmissionExample class defines a public Execute method that calls other example methods to use the Microsoft Store submission API to create and commit an app submission that contains game options and a trailer. 若要自行調整這個程式碼:To adapt this code for your own use:

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 存取權杖Obtain an Azure AD access token

DevCenterAccessTokenClient 類別定義協助程式方法,使用您的 tenantIdclientIdclientSecret 值來建立 Azure AD 存取權杖以搭配 Microsoft Store 提交 API 使用。The DevCenterAccessTokenClient class defines a helper method that uses the your tenantId, clientId and clientSecret values to create an Azure AD access token to use with the Microsoft Store submission 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 並上傳提交檔案的協助程式方法Helper methods to invoke the submission API and upload submission files

DevCenterClient 類別定義協助程式方法,叫用 Microsoft Store 提交 API 中的各種不同方法,並上傳包含 App 提交的套件、清單影像和預告片檔案的 ZIP 檔案。The DevCenterClient class defines helper methods that invoke a variety of methods in the Microsoft Store submission API and upload the ZIP file containing the packages, listing images, and trailer files for the app submission.

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;
        }
    }
}