Async deploy of SPFx packages to SharePoint site app catalog


Asynchronous deploy of SPFx solution packages to site app catalog using CSharp, Flurl and SharePoint ALM rest services.

How to deploy multiple SharePoint Framework solution packages async

The code bellow demonstrates how multiple requests for solution package add and deploy can be send to SharePoint so multiple packages can be distributed to multiple sites and almost the same time. This is useful when a package should be uploaded to multiple SharePoint site collection app catalogs.


using Flurl.Http;
using MyNamespace.Api.Interfaces;
using MyNamespace.Router;
using MyNamespace.Router.Services;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace MyNamespace.PackageDistribution
{
    public class Deployer
    {
        public readonly ILogger Logger;
        protected readonly IAuthService authService;

        public Deployer(IAuthService authService, ILogger logger)
        {
            this.authService = authService;
            this.Logger = logger;
        }

        public async Task DeployAllPackagesAsync()
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();

            var token = await this.authService.GetAccessTokenAsync("https://my-tenant.sharepoint.com");

            this.Logger.Info($"Got the token: {token}. Deploy time");

            var siteUrls = new List<string>() { "https://my-tenant.sharepoint.com/sites/siteA", "https://my-tenant.sharepoint.com/sites/siteB" };
            var packagePaths = new List<string>() { "C:\\cors-request-sender.sppkg" };

            var tasks = new List<Task>();

            foreach (var packagePath in packagePaths)
            {
                foreach (var siteUrl in siteUrls)
                {
                    tasks.Add(this.DeployAsync(siteUrl, packagePath, token));
                }
            }

            await Task.WhenAll(tasks); // upload all in parallel job

            stopwatch.Stop();
            this.Logger.Info($"All done for {stopwatch.Elapsed}. Can't be any faster than the Deployer!");
        }

        public async Task DeployAsync(string siteUrl, string packagePath, string accessToken)
        {
            var stopLoop = false;
            var retryCount = 1;
            var fileName = System.IO.Path.GetFileName(packagePath);

            // has retry logic since SharePoint can throttle the request if to much requests from users at that time
            // needs better handling that tolerates the throttle headers, but to be implemented in another day
            do
            {
                try
                {
                    using (var stream = File.OpenRead(packagePath))
                    {
                        var byteArray = new ByteArrayContent(await new StreamContent(stream).ReadAsByteArrayAsync());

                        var resp1 = await $"{siteUrl}/_api/web/sitecollectionappcatalog/Add(overwrite=true, url='{fileName}')"
                            .WithHeader("accept", "application/json;odata=nometadata")
                            .WithHeader("Content-Type", "application/json")
                            .WithHeader("binaryStringRequestBody", true)
                            .WithOAuthBearerToken(accessToken)
                            .PostAsync(byteArray)
                            .ReceiveJson();

                        this.Logger.Info($"Package uniqueId {resp1.UniqueId} added to {siteUrl}.");

                        await $"{siteUrl}/_api/web/sitecollectionappcatalog/AvailableApps/GetById('{resp1.UniqueId}')/deploy"
                            .WithHeader("accept", "application/json;odata=nometadata")
                            .WithHeader("Content-Type", "application/json;odata=nometadata;charset=utf-8")
                            .WithHeader("binaryStringRequestBody", true)
                            .WithOAuthBearerToken(accessToken)
                            .PostJsonAsync(new { skipFeatureDeployment = true })
                            .ReceiveJson();

                        this.Logger.Info($"Package {fileName} deployed to {siteUrl}.");
                    }

                    stopLoop = true;
                }
                catch (Exception ex)
                {
                    if (retryCount > 4)
                    {
                        this.Logger.Error($"Could not deploy package. Exited the retry. Exception: {ex.ToString()}");

                        stopLoop = true;
                    }
                    else
                    {
                        // could not upload the file. will try again.
                        Thread.Sleep(10000);
                        retryCount++;
                    }
                }
            } while (stopLoop == false);
        }
    }
}

Usage

var deployer = new Deployer(yourAuthService, yourLogger);
deployer.DeployAllPackagesAsync();

There are few missing details like how I got the token, but this is omitted to give more presence to the deployment code. The way to get the access token is up to you and there are multiple articles on how to do that in the net.

Throttling issues are possible

That piece of code does not take in consideration throttling limits, but if you start getting 429 or 503 error codes then you will have to come up with better logic for retry when the server is telling you to back off . If ''retry-after'' header is present can be used to attempt retry after the time specified in that header.

Turn it to PowerShell cmdlet or exe and move the configurations in file

The next step for me is to turn that code into executable or PowerShell cmdlet, move the hard-coded variables to app.config file or other format configuration file and run it with an Azure DevOps release pipeline.

Conclusion

We had to speed the release process since we had to deploy 3 packages to 150 SharePoint site app catalogs with every deployment and this code with slight modifications and better handling on retry will be used. Enjoy.

Useful links

The sample is using Flurl, a modern, fluent, asynchronous, testable, portable, buzzword-laden URL builder and HTTP client library. Credits to these guys since this is must have when building testable code.

Posted on

Tags: SharePoint, Site collection AppCatalog, Site collection App Catalog, Package deployment, SPFx package deploy, skip feature deployment, SPFx solution package deployment async

Comments