In SharePoint Online projects, provisioning templates are a powerful way to capture site structures (lists, fields, content types, navigation, etc.) and reapply them to new sites.
Exporting SharePoint Templates with PnP Framework and MSAL (C# Console App)
In SharePoint Online projects, provisioning templates are a powerful way to capture site structures (lists, fields, content types, navigation, etc.) and reapply them to new sites.
In this guide, we’ll build a simple C# console application that exports a site template using the PnP Framework and authenticates with MSAL (Microsoft Authentication Library).
🔹 What the app does
- Authenticates to SharePoint Online using Azure AD app registration (MSAL interactive login).
- Connects to a given source site.
- Exports its structure as a PnP XML template.
- Saves the template in the same directory where the app is running.
✅ Full Source Code
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.SharePoint.Client;
using Microsoft.Identity.Client;
using PnP.Framework.Provisioning.Model;
using PnP.Framework.Provisioning.ObjectHandlers;
using PnP.Framework.Provisioning.Connectors;
using PnP.Framework.Provisioning.Providers.Xml;
namespace PnPConsoleExport
{
internal class Program
{
// 🔐 Replace with your registered Azure AD App
private static readonly string tenantId = "<TENANT_ID>";
private static readonly string clientId = "<CLIENT_ID>";
// Scope for SharePoint
private static readonly string[] scopes = new[] { "https://<yourtenant>.sharepoint.com/AllSites.FullControl" };
static async Task Main()
{
Console.WriteLine("=== PnP Template Export Tool ===");
Console.Write("Enter the source SharePoint site URL: ");
string sourceUrl = Console.ReadLine();
// Current directory where the app is running
string workDir = AppDomain.CurrentDomain.BaseDirectory;
try
{
// 🔐 Authenticate with MSAL (Interactive)
string token = await GetAccessTokenAsync();
// Export Template
ExportTemplate(sourceUrl, token, workDir);
Console.WriteLine("✅ Export finished.");
}
catch (Exception ex)
{
Console.WriteLine($"❌ ERROR: {ex.Message}");
if (ex.InnerException != null) Console.WriteLine($"🔎 Inner: {ex.InnerException.Message}");
}
}
// --- Authentication (MSAL Interactive) ---
private static async Task<string> GetAccessTokenAsync()
{
var app = PublicClientApplicationBuilder.Create(clientId)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}")
.WithRedirectUri("http://localhost")
.Build();
var result = await app.AcquireTokenInteractive(scopes).ExecuteAsync();
Console.WriteLine("🔑 Token acquired.");
return result.AccessToken;
}
// --- Export template as XML ---
private static void ExportTemplate(string sourceUrl, string token, string workDir)
{
using (var ctx = GetContext(sourceUrl, token))
{
Console.WriteLine($"🌐 Exporting template from {sourceUrl}...");
var creationInfo = new ProvisioningTemplateCreationInformation(ctx.Web)
{
FileConnector = new FileSystemConnector(workDir, ""),
HandlersToProcess = Handlers.Lists | Handlers.Fields | Handlers.ContentTypes,
MessagesDelegate = (msg, type) => Console.WriteLine($"[PnP {type}] {msg}")
};
var template = ctx.Web.GetProvisioningTemplate(creationInfo);
// Save as XML in the app directory
string filePath = Path.Combine(workDir, "ExportedTemplate.xml");
// Create XML provider pointing to the current folder
var provider = new XMLFileSystemTemplateProvider(workDir, "");
// Save the template as XML
provider.SaveAs(template, "ExportedTemplate.xml");
Console.WriteLine($"📦 Template exported to {filePath}");
}
}
// --- Helper: Get Context with Token ---
private static ClientContext GetContext(string url, string token)
{
var ctx = new ClientContext(url);
ctx.ExecutingWebRequest += (s, e) =>
{
e.WebRequestExecutor.WebRequest.Headers["Authorization"] = "Bearer " + token;
};
return ctx;
}
}
}
🔹 How to run
- Register an app in Azure AD (Entra ID):
- Allow public client flows.
- Grant delegated permission
AllSites.FullControl.
- Replace values in the code:
private static readonly string tenantId = "<TENANT_ID>"; private static readonly string clientId = "<CLIENT_ID>"; private static readonly string[] scopes = new[] { "https://<yourtenant>.sharepoint.com/AllSites.FullControl" }; - Build and run the console app:
dotnet build dotnet run - Enter your source site URL when prompted.
- The tool will open a MSAL login window.
- The template will be saved as
ExportedTemplate.xmlin the same directory as the.exe.
🔹 Output Example
=== PnP Template Export Tool ===
Enter the source SharePoint site URL: https://tenant.sharepoint.com/sites/Engineering
🔑 Token acquired.
🌐 Exporting template from https://tenant.sharepoint.com/sites/Engineering...
[PnP Information]: Extracting Lists...
[PnP Information]: Extracting Fields...
[PnP Information]: Extracting Content Types...
📦 Template exported to C:\Users\you\source\repos\PnPConsoleExport\bin\Debug\net6.0\ExportedTemplate.xml
✅ Export finished.
🔹 Next Steps
- Create a second console app (or extend this one) to apply the template to a target site.
- Extend handlers if you want to capture navigation, features, or branding.
- Automate token acquisition using client credentials if you want a headless job.
🔹 References
- PnP Framework Documentation
- Provisioning Engine Overview
- MSAL .NET Documentation
- SharePoint ClientContext Class
