Delegate SharePoint Online CSOM (REST API) With .NET Core 3.1

 Introduction 

 
This article will cover, How to consume Sharepoint online CSOM (REST API) operation using .NET Core 3.1 framework into a console application.
  



The topic needs to cover here:
  1. Create Azure AD Instance with Delegate Permission.
  2. Create Console Application and Add Microsoft.SharePointOnline.CSOM Nuget Package.
  3. Add Authentication Manager Class to generate the access token.
  4. Get Access Token & access SharePoint Online List data 
Step 1 - Create Azure AD Instance with Delegate Permission
 
Note
Users should have access to Azure Active Directory.
  • Login to https://portal.azure.com
  • Search and Select Azure Active Directory 
Once the user selects Azure AD, the App registration page will appear.
  1. Select App registrations
  2. Select +New registration 




Once the user selects new registration, add below details
  1. Name "spocsom-api", user can add as per project namespace or naming convention guidelines.
  2. Select "Account in this organization (single-tenant)
  3. Click on the Register button 


Grant access to SharePoint Online.
  1. Select API Permission
  2. Click to Add Permission
  3. Search and Add SharePoint from Request API Permission 


Delegate Permission, It required where user application needs to access data as the signed-in user.
  1. Select Add a Permission
  2. Selection Delegate Permission
  3. Search and Select Sites 
  4. Choose "All Site.Read" -> This is not site specific but quite secure , It will just come into use to generate token, not going to grant access to the site to access the content.
  5. Click to "Add Permission" 


Grant Admin Consent
 
Grant permission by admin or user as part of the consent process.
  1. Select "Grant admin consent for default directory and click ok to proceed.
  2. All added api will show granted admin consent on behalf of the user. so it will not prompt for consent. 

Allow Public Client Flows
 
The app is going to collect username and password into plain text, so it should allow to yes.
  1. Select authentication and scroll down till the end
  2. Under advance settings -> select Allow Public client flows -> Select Yes -> Click on Save to Proceed.


Step 2 - Create Console Application and Add Microsoft.SharePointOnline.CSOM Nuget Package
  1. Login to Visual Studio 2019 and Create New Project
  2. Select ConsoleApp (.Net Core) and Ok to proceed.


Install NuGet Package
 
Select solution and right-click on dependencies and NuGet Packages
 
Install the below packages:
  1. Microsoft.SharepointOnline.CSOM 
  2. Newtonsoft.Json
  3. System.IdentityModel.Token.Jwt




Step 3 - Add Authentication Manager Class to generate the access token
 
This class helps us to generate access tokens based on application URI, UserName, Password & Azurre AD App Client ID.
 
Create Auth Manager Class and Copy paste the below codebase:

  1. public class AuthManager : IDisposable  
  2.    {  
  3.        private static readonly HttpClient httpClient = new HttpClient();  
  4.        private const string tokenEndpoint = "https://login.microsoftonline.com/common/oauth2/token";  
  5.        // Replace with Azure AD Client ID  -Generated in above Steps
  6.        private const string defaultAADAppId = "Azure Active Director Client ID";  
  7.   
  8.        // Token cache handling  
  9.        private static readonly SemaphoreSlim semaphoreSlimTokens = new SemaphoreSlim(1);  
  10.        private AutoResetEvent tokenResetEvent = null;  
  11.        private readonly ConcurrentDictionary<string, string> tokenCache = new ConcurrentDictionary<string, string>();  
  12.        private bool disposedValue;  
  13.   
  14.        internal class TokenWaitInfo  
  15.        {  
  16.            public RegisteredWaitHandle Handle = null;  
  17.        }  
  18.   
  19.        public ClientContext GetContext(Uri web, string userPrincipalName, SecureString userPassword)  
  20.        {  
  21.            var context = new ClientContext(web);  
  22.   
  23.            context.ExecutingWebRequest += (sender, e) =>  
  24.            {  
  25.                string accessToken = EnsureAccessTokenAsync(new Uri($"{web.Scheme}://{web.DnsSafeHost}"), userPrincipalName, new System.Net.NetworkCredential(string.Empty, userPassword).Password).GetAwaiter().GetResult();  
  26.                e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessToken;  
  27.            };  
  28.   
  29.            return context;  
  30.        }  
  31.   
  32.   
  33.        public async Task<string> EnsureAccessTokenAsync(Uri resourceUri, string userPrincipalName, string userPassword)  
  34.        {  
  35.            string accessTokenFromCache = TokenFromCache(resourceUri, tokenCache);  
  36.            if (accessTokenFromCache == null)  
  37.            {  
  38.                await semaphoreSlimTokens.WaitAsync().ConfigureAwait(false);  
  39.                try  
  40.                {  
  41.                    // No async methods are allowed in a lock section  
  42.                    string accessToken = await AcquireTokenAsync(resourceUri, userPrincipalName, userPassword).ConfigureAwait(false);  
  43.                    Console.WriteLine($"Successfully requested new access token resource {resourceUri.DnsSafeHost} for user {userPrincipalName}");  
  44.                    AddTokenToCache(resourceUri, tokenCache, accessToken);  
  45.   
  46.                    // Register a thread to invalidate the access token once's it's expired  
  47.                    tokenResetEvent = new AutoResetEvent(false);  
  48.                    TokenWaitInfo wi = new TokenWaitInfo();  
  49.                    wi.Handle = ThreadPool.RegisterWaitForSingleObject(  
  50.                        tokenResetEvent,  
  51.                        async (state, timedOut) =>  
  52.                        {  
  53.                            if (!timedOut)  
  54.                            {  
  55.                                TokenWaitInfo internalWaitToken = (TokenWaitInfo)state;  
  56.                                if (internalWaitToken.Handle != null)  
  57.                                {  
  58.                                    internalWaitToken.Handle.Unregister(null);  
  59.                                }  
  60.                            }  
  61.                            else  
  62.                            {  
  63.                                try  
  64.                                {  
  65.                                    // Take a lock to ensure no other threads are updating the SharePoint Access token at this time  
  66.                                    await semaphoreSlimTokens.WaitAsync().ConfigureAwait(false);  
  67.                                    RemoveTokenFromCache(resourceUri, tokenCache);  
  68.                                    Console.WriteLine($"Cached token for resource {resourceUri.DnsSafeHost} and user {userPrincipalName} expired");  
  69.                                }  
  70.                                catch (Exception ex)  
  71.                                {  
  72.                                    Console.WriteLine($"Something went wrong during cache token invalidation: {ex.Message}");  
  73.                                    RemoveTokenFromCache(resourceUri, tokenCache);  
  74.                                }  
  75.                                finally  
  76.                                {  
  77.                                    semaphoreSlimTokens.Release();  
  78.                                }  
  79.                            }  
  80.                        },  
  81.                        wi,  
  82.                        (uint)CalculateThreadSleep(accessToken).TotalMilliseconds,  
  83.                        true  
  84.                    );  
  85.   
  86.                    return accessToken;  
  87.   
  88.                }  
  89.                finally  
  90.                {  
  91.                    semaphoreSlimTokens.Release();  
  92.                }  
  93.            }  
  94.            else  
  95.            {  
  96.                Console.WriteLine($"Returning token from cache for resource {resourceUri.DnsSafeHost} and user {userPrincipalName}");  
  97.                return accessTokenFromCache;  
  98.            }  
  99.        }  
  100.   
  101.        public async Task<string> AcquireTokenAsync(Uri resourceUri, string username, string password)  
  102.        {  
  103.            string resource = $"{resourceUri.Scheme}://{resourceUri.DnsSafeHost}";  
  104.   
  105.            var clientId = defaultAADAppId;  
  106.            var body = $"resource={resource}&client_id={clientId}&grant_type=password&username={HttpUtility.UrlEncode(username)}&password={HttpUtility.UrlEncode(password)}";  
  107.            using (var stringContent = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded"))  
  108.            {  
  109.   
  110.                var result = await httpClient.PostAsync(tokenEndpoint, stringContent).ContinueWith((response) =>  
  111.                {  
  112.                    return response.Result.Content.ReadAsStringAsync().Result;  
  113.                }).ConfigureAwait(false);  
  114.   
  115.                var tokenResult = JsonSerializer.Deserialize<JsonElement>(result);  
  116.                var token = tokenResult.GetProperty("access_token").GetString();  
  117.                return token;  
  118.            }  
  119.        }  
  120.   
  121.        private static string TokenFromCache(Uri web, ConcurrentDictionary<string, string> tokenCache)  
  122.        {  
  123.            if (tokenCache.TryGetValue(web.DnsSafeHost, out string accessToken))  
  124.            {  
  125.                return accessToken;  
  126.            }  
  127.   
  128.            return null;  
  129.        }  
  130.   
  131.        private static void AddTokenToCache(Uri web, ConcurrentDictionary<string, string> tokenCache, string newAccessToken)  
  132.        {  
  133.            if (tokenCache.TryGetValue(web.DnsSafeHost, out string currentAccessToken))  
  134.            {  
  135.                tokenCache.TryUpdate(web.DnsSafeHost, newAccessToken, currentAccessToken);  
  136.            }  
  137.            else  
  138.            {  
  139.                tokenCache.TryAdd(web.DnsSafeHost, newAccessToken);  
  140.            }  
  141.        }  
  142.   
  143.        private static void RemoveTokenFromCache(Uri web, ConcurrentDictionary<string, string> tokenCache)  
  144.        {  
  145.            tokenCache.TryRemove(web.DnsSafeHost, out string currentAccessToken);  
  146.        }  
  147.   
  148.        private static TimeSpan CalculateThreadSleep(string accessToken)  
  149.        {  
  150.            var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(accessToken);  
  151.            var lease = GetAccessTokenLease(token.ValidTo);  
  152.            lease = TimeSpan.FromSeconds(lease.TotalSeconds - TimeSpan.FromMinutes(5).TotalSeconds > 0 ? lease.TotalSeconds - TimeSpan.FromMinutes(5).TotalSeconds : lease.TotalSeconds);  
  153.            return lease;  
  154.        }  
  155.   
  156.        private static TimeSpan GetAccessTokenLease(DateTime expiresOn)  
  157.        {  
  158.            DateTime now = DateTime.UtcNow;  
  159.            DateTime expires = expiresOn.Kind == DateTimeKind.Utc ? expiresOn : TimeZoneInfo.ConvertTimeToUtc(expiresOn);  
  160.            TimeSpan lease = expires - now;  
  161.            return lease;  
  162.        }  
  163.   
  164.        protected virtual void Dispose(bool disposing)  
  165.        {  
  166.            if (!disposedValue)  
  167.            {  
  168.                if (disposing)  
  169.                {  
  170.                    if (tokenResetEvent != null)  
  171.                    {  
  172.                        tokenResetEvent.Set();  
  173.                        tokenResetEvent.Dispose();  
  174.                    }  
  175.                }  
  176.   
  177.                disposedValue = true;  
  178.            }  
  179.        }  
  180.   
  181.        public void Dispose()  
  182.        {  
  183.            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method  
  184.            Dispose(disposing: true);  
  185.            GC.SuppressFinalize(this);  
  186.        }  
  187.   
  188.    }  


Step 4 - Get Access Token & access SharePoint Online List data
 

 Get List Data function get data from SharePoint online List and Print out all result based on return data.

  1. static void Main(string[] args)  
  2.       {  
  3.           List<Data> getdata = GetListData();  
  4.           foreach (Data data in getdata)  
  5.           {  
  6.               Console.WriteLine("Employee Name " + data.Title);  
  7.           }  
  8.           Console.ReadLine();  
  9.       }

The codebase will invoke the SharePoint online api with help of an access token 

  1. public static List<Data> GetListData()  
  2.        {  
  3.            const string DataColumn = "ID,Title";  
  4.            const string DataAPIAllData = "{0}/_api/lists/getbytitle('{1}')/items?$top=10&$select=" + DataColumn + "&$orderby=Modified desc";  
  5.   
  6.            try  
  7.            {  
  8.                var results = new List<Data>();  
  9.   
  10.                string sharepointSiteUrl = Convert.ToString("https://mittal1201.sharepoint.com/sites/CommSiteHub");  
  11.                if (!string.IsNullOrEmpty(sharepointSiteUrl))  
  12.                {  
  13.                    string listname = "Employee";  
  14.                    string api = string.Format(DataAPIAllData, sharepointSiteUrl, listname);  
  15.                    if (!string.IsNullOrEmpty(listname))  
  16.                    {  
  17.                        //Invoke REST Call  
  18.                        string response = TokenHelper.GetAPIResponse(api);  
  19.                        if (!String.IsNullOrEmpty(response))  
  20.                        {  
  21.                            JObject jobj = Utility.Deserialize(response);  
  22.                            JArray jarr = (JArray)jobj["d"]["results"];  
  23.   
  24.                            //Write Response to Output  
  25.                            foreach (JObject j in jarr)  
  26.                            {  
  27.                                Data data = new Data();  
  28.                                data.Title = Convert.ToString(j["Title"]);  
  29.   
  30.                                results.Add(data);  
  31.                            }  
  32.                        }  
  33.                        return results;  
  34.                    }  
  35.                    else  
  36.                    {  
  37.                        throw new Exception("Custom Message");  
  38.                    }  
  39.                }  
  40.                else  
  41.                {  
  42.                    throw new Exception("Custom Message");  
  43.                }  
  44.            }  
  45.            catch (Exception ex)  
  46.            {  
  47.                throw new Exception("Custom Message");  
  48.            }  
  49.        }  
  50.    }  


Created a data class  with filed title here


  1. public class Data  
  2. {  
  3. public string Title { get; set; }  

Create TokenHelper class to get access token based on user credentials and URI. This function helps to get access from AuthManager Class with the provided information.

  1. public static string GetAPIResponse(string url)  
  2.        {  
  3.            string response = String.Empty;  
  4.            try  
  5.            {  
  6.                //Call to get AccessToken  
  7.                string accessToken = GetSharePointAccessToken();  
  8.                //Call to get the REST API response from Sharepoint  
  9.                System.Net.HttpWebRequest endpointRequest = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(url);  
  10.                endpointRequest.Method = "GET";  
  11.                endpointRequest.Accept = "application/json;odata=verbose";  
  12.                endpointRequest.Headers.Add("Authorization""Bearer " + accessToken);  
  13.                System.Net.WebResponse webResponse = endpointRequest.GetResponse();  
  14.                Stream webStream = webResponse.GetResponseStream();  
  15.                StreamReader responseReader = new StreamReader(webStream);  
  16.                response = responseReader.ReadToEnd();  
  17.                return response;  
  18.            }  
  19.            catch (Exception ex)  
  20.            {  
  21.                throw;  
  22.            }  
  23.   
  24.        }  
  25.   
  26.        public static string GetSharePointAccessToken()  
  27.        {  
  28.   
  29.            Uri site = new Uri("https://mittal1201.sharepoint.com/sites/CommSiteHub");  
  30.            string user = "user email address";  
  31.            string pwd = "user password";  
  32.            string result;  
  33.            using (var authenticationManager = new AuthManager())  
  34.            {  
  35.                string accessTokenSP = authenticationManager.AcquireTokenAsync(site, user, pwd).Result;  
  36.                result = accessTokenSP;  
  37.            }  
  38.            return result;  
  39.        }  


Output Window 
 
Execute this solution or press F5.
 
SharePoint List Screen Shot where data is going to read by a console app
 


Console Output



Finally, we got output using .NetCore 3.1 console app instead of .NetStandard framework. 
 
I hope you enjoyed and learned something new in this article. 

 

Publish Duplicate Client Side Page Changes To Original Page Within Same SPO Site Pages Library

 

Problem Statement

 
This article is a continuation of my previous article, Duplicate the Client Side Pages into same Modern SharePoint Online Library Folder where I explained how to duplicate the client side page with all existing controls.
 
In this article, I would like to explain this: If the user makes any changes to duplicate page, how to publish the duplicate client side pages to the original page into same site pages library within the same site collection.
 
As a developer, we have a couple of options to duplicate the page into another modern site collection library or different folder structure, but there is no way to copy the pages within the same modern site collection sites library within the same folder structure.
 
Prerequisite Steps
 
Let's create a page and add existing below control, or user can add any other available controls. This is just for demo purposes.
  • SharePoint Online PnP PowerShell Overview here
  • Browse the existing pages
Home Page looks like this,




Duplicate Page looks like this with changes,
  • Add new control
  • Modify the pages control layout





Approach Overview

 
Key steps to publish duplicate client side page with changes to original Modern Site Collection Site Pages into Same Library Folder.
  1. Use PnP Online to connect to SharePoint Online Modern Site
  2. Export PnP Client Side Pages Command export pages with PnP Provisioning Template
  3. Store it locally.
  4. Apply PnP Provisioning Template store it with different name. 
  1. try   
  2.    {  
  3.   
  4.          $srcUrl = "https://mittal1201.sharepoint.com/sites/commsitehub"   
  5.          Connect-PnPOnline -Url $srcUrl  
  6.   
  7.          $SourcePageName = "home_duplicate"  
  8.          $TargetPageName = "home"  
  9.       
  10.          $tempFile = 'C:\CsharpCorner\'+ $SourcePageName +'.xml'  
  11.          Export-PnPClientSidePage -Force -Identity $SourcePageName -Out $tempFile  
  12.   
  13.          $con = Get-Content $tempFile  
  14.          $sourcepage=$SourcePageName +".aspx"  
  15.          $targetpage=$TargetPageName +".aspx"  
  16.        
  17.          $con | % { $_.Replace($sourcepage,$targetpage) } | Set-Content $tempFile  
  18.          Apply-PnPProvisioningTemplate -Path  $tempFile  
  19.          write-host -ForegroundColor Magenta "Page reverted with name of  " $targetpage      
  20.        
  21.      
  22. }   
  23. catch {  
  24.     Write-Host - ForegroundColor Red 'Error '':'  
  25.     $Error[0].ToString();  
  26.      
  27. }  


Save and run this script.
 

OutPut Steps

 
Applying template to client side pages
 

Export-PnPClientSidePage Cmdlets applying the PnP Provisioning template and export ".xml" file into shared locaiton. Cmdlets referece can be find here 



Creation of client side pages
 
Apply-PnPProvisioningTemplate cmdlets will create a new page within the same library from download or exported ".xml" file with provided name i.e. pagename _ duplicate
Cmdlets referece can be find here






Final Duplicate Page Outcome
 
Browse the url; i.e., original page. New Client Side Page will be available with all configuration and controls.




I hope you have enjoyed and learned something new in this article. Thanks for reading and stay tuned for the next article.



Duplicate Client Side Pages Into Same Modern SharePoint Online SitePages Lib

 

Problem Statement 

 
In this article, I would like to explain how to duplicate client side pages which have OOTB webparts and custom SPFx webparts into the same site pages library within the same site collection.
 
As a developer, we have a couple of options to duplicate the page into another modern site collection library or different folder structure, but  there is no way to copy the pages within the same modern site collection site library within the same folder structure.



 
Prerequisites Steps  
 
Let's create a page and add the exisitng below controls,  or the user can add any other available controls. This is just for demo purposes. 
  • SharePoint Online PnP PowerShell Overview here
  • Add and Configure  Custom SPFx Control
  • Add and Configure  Image Control
  • Add and Configure  News Control



Approach Overview

 
We need to follow the below key steps to perform duplication of Modern Site Collection Site Pages into the Same Library Folder. 
  1. Use PnP Online to connect to SharePoint Online Modern Site
  2. Export PnP Client Side Pages Command  export pages with PnP Provisioning Template
  3. Store it locally.
  4. Apply PnP Provisioning Template store it with different name. 
SharePoint Online PnP PowerShell Script

  1. try   
  2.    {  
  3.       
  4.         $srcUrl = "https://mittal1201.sharepoint.com/sites/commsitehub"   
  5.         Connect-PnPOnline -Url $srcUrl  
  6.         $pageName  = [System.Web.HttpUtility]::UrlDecode("Home")  
  7.         write-host $pageName  
  8.         $tempFile = 'C:\CsharpCorner\'+ $pageName +'.xml'  
  9.         Export-PnPClientSidePage -Force -Identity $pageName -Out $tempFile  
  10.   
  11.         $con = Get-Content $tempFile  
  12.         $sourcepage=$pageName +".aspx"  
  13.         $targetpage=$pageName +"_Duplicate.aspx"  
  14.        
  15.         $con | % { $_.Replace($sourcepage, $targetpage) } | Set-Content $tempFile  
  16.         Apply-PnPProvisioningTemplate -Path  $tempFile  
  17.         write-host -ForegroundColor Magenta "Page Created with name of  " $targetpage    
  18.      
  19. }   
  20. catch {  
  21.     Write-Host - ForegroundColor Red 'Error '':'  
  22.     $Error[0].ToString(); 


Save and run this script.

OutPut Steps 

 
Applying template to client side pages
 
Export-PnPClientSidePage Cmdlets applying the PnP Provisioning template and export ".xml" file into shared locaiton. Cmdlets referece can be found here 


Creation of client side pages 
 
Apply-PnPProvisioningTemplate cmdlets will create a new page within same library from download or exported ".xml" file with provided name i.e. pagename _ duplicate
Cmdlets referece can be found here
 



 Final Duplicate Page outcome

 
Browse the url with suffix as _duplicate in the url. New Client Side Page will be available with all configuration and controls.



I hope you have enjoyed and learned something new in this article. Thanks for reading and stay tuned for the next article.


SPFX with DevOps using SharePoint ALM & Blob Storage

Implement SPFx (SharePoint Framework) deployment with DevOps using SharePoint ALM Commands & Blob Storage

Azure DevOps (Visual Studio Team Services / Team Foundation Server) consists of a set of tools and services that help developers implement DevOps, Continuous Integration, and Continuous Deployment processes for their development projects.

This article explains the steps involved in setting up your Azure DevOps environment with Continuous Integration and Continuous Deployment to automate your SharePoint Framework builds, unit tests, and deployment.

SharePoint ALM (Application Life Cycle Management) APIs provide simple APIs to manage deployment of your SharePoint Framework solutions and add-ins across your tenant or Site Collection level.

ALM APIs can be used to perform exactly the same operations that are available from a UI perspective

Continuous Integration

Continuous Integration (CI) helps developers integrate code into a shared repository by automatically build  and packaging the solution each time new code changes are submitted.

Setting up Azure DevOps for Continuous Integration with a SharePoint Framework solution requires the following steps:

Office Azure & SharePoint ALM - Build Pipeline

  • Node 10.x
  • Gulp Clean
  • Gulp Build
  • Gulp Bundle --ship
  • Gulp Package-Solution --ship
  • Publish Build Artifacts  (Task Version 1)
    • "Display Name" -> Publish Artifact: CDN
    • "Path to Publish" -> temp/deploy
    • "Artifact Name" -> CDN
    • "Artifact Publish Location" -> Azure Pipeline
  • Publish Build  Artifact
    • "Display Name" -> Publish Artifact: App
    • "Path to Publish" -> sharepoint/solution
    • "Artifact Name" -> App
    • "Artifact Publish Location" -> Azure Pipeline


Continuous Deployment

Continuous Deployment (CD) takes validated code packages from build process and deploys them into a staging or production environment. Developers can track which deployments were successful or not and narrow down issues to specific package versions.

Setting up Azure DevOps for Continuous Deployments with a SharePoint Framework solution requires the following steps:

  • Office Azure & SharePoint ALM - Build Pipeline
  • Azure file copy (Task Version 2)
    • "Display name"-> AzureBlobFileCopy
    • "Source"->  $(System.DefaultWorkingDirectory)/_AzureDevOpsCI/CDN
    • "AzureConnectionType"-> Azure Resource Manager
    • "Azure Subscription"
    • "Destination Type"-> Azure Blob
    • "RM Storage Account"- > 
    • "Container Name"->
  • SharePoint ALM: Catalog Scoped Actions
    • "Display name"->SharePoint ALM: Add Solution Package
    • "Connection to SharePoint" -> commsite01
    • "Action"-> Add Solution  Package
    • "Path to SharePoint Solution Package"-> $(System.DefaultWorkingDirectory)/_AzureDevOpsCI/App/devops-01.sppkg
    • "
    • "PackageId output variable"-> AppPackageId
  • SharePoint ALM: Catalog Scoped Actions
    • "Display name"->SharePoint ALM: Deploy Solution Package
    • "Connection to SharePoint" -> commsite01
    • "Action"-> Deploy Solution Package
    • "Id of the package in the SharePoint App Catalog"  -> $(AppPackageId)


End to End implementation SPFx deployment using Azure DevOps with SharePoint (Application Life Cycle Management) & Blob Storage for Static files.




SPFX with DevOps using Office 365 CLI

Implement SPFx (SharePoint Framework) deployment with Azure DevOps Pipeline using Office 365 CLI

Azure DevOps (Visual Studio Team Services / Team Foundation Server) consists of a set of tools and services that help developers implement DevOps, Continuous Integration, and Continuous Deployment processes for their development projects.

This article explains the steps involved in setting up your Azure DevOps environment with Continuous Integration and Continuous Deployment to automate your SharePoint Framework builds, unit tests, and deployment.


Continuous Integration

Continuous Integration (CI) helps developers integrate code into a shared repository by automatically build  and packaging the solution each time new code changes are submitted.

Setting up Azure DevOps for Continuous Integration with a SharePoint Framework solution requires the following steps:

  • Office 365 CLI Build Definition
  • Node 10.x
  • Gulp Clean
  • Gulp Build
  • Gulp Bundle --ship
  • Gulp Package-Solution --ship
  • Copy Files
    •  "Display Name" -> Copy Files To: Copy Files to: $(Build.ArtifactStagingDirectory)/drop
    • "Contents" ->  **/*.sppkg
    • "Target Folder" -> $(Build.ArtifactStagingDirectory)/drop
  • Publish Build  Artifact
    • "Display Name" -> Publish Artifact: drop
    • "Publish To Path" -> $(Build.ArtifactStagingDirectory)
    • "Artifact Name"  -> Drop
    • "Artifact Publish Location" -> Azure Pipelines

Continuous Deployment

Continuous Deployment (CD) takes validated code packages from build process and deploys them into a staging or production environment. Developers can track which deployments were successful or not and narrow down issues to specific package versions.

Setting up Azure DevOps for Continuous Deployments with a SharePoint Framework solution requires the following steps:


  • Office 365 CLI Release Pipeline
  • Node 10.x
  • NPM 
    • "Display Name" -> npm custom
    • "Command" -> custom
    • "command and arguments" -> install -g @pnp/office365-cli
  • Command Line
    • "Display Name" -> Connect to SPO
    • "Scripts" -> 
    •  o365 login https://$(tenant).sharepoint.com/$(catalogsite) --authType password --userName $(username) --password $(password)
  • Command Line
    • "Display Name" ->SP add solution
    • "Scripts" -> 
      • o365 spo app add -p $(System.DefaultWorkingDirectory)/_SPFx-AutomateDeployment-CI/drop/drop/sharePoint/solution/clidemo-02.sppkg --overwrite --appCatalogUrl https://$(tenant).sharepoint.com/$(Devcatalogsite) --scope sitecollection
  • Command Line
    • "Display Name" -> Deploy Solution
    • "Scripts" -> 
      • o365 spo app deploy --name clidemo-02.sppkg --appCatalogUrl https://$(tenant).sharepoint.com/$(Devcatalogsite) --scope sitecollection

End to End implementation SPFx deployment with Azure DevOps using Office 365 CLI