When working with SharePoint provisioning, exporting a site template often results in a large, monolithic XML file containing everything: lists, fields, navigation, pages, features, etc.
Modular Export and Import of SharePoint Templates with PnP
When working with SharePoint provisioning, exporting a site template often results in a large, monolithic XML file containing everything: lists, fields, navigation, pages, features, etc.
For simple scenarios this is fine, but in real-world projects it’s much more useful to split the template into modular files. That way you can:
- Manage one XML per handler (Navigation, Features, Pages…)
- Keep site columns (fields) in their own file
- Export one XML per list
- Reapply them in controlled order (fields → lists → handlers)
This article shows how to implement this approach using PnP Framework in C#.
1. Exporting Modular Templates
The first step is to split the export into multiple files.
We’ll use ProvisioningTemplateCreationInformation and run the export in three modes:
- Per Handler → Navigation, Features, Pages, etc.
- Fields only → all site columns into
Fields.xml - Per List → one XML file for each list
Code: Split Export
using System;
using System.Linq;
using Microsoft.SharePoint.Client;
using PnP.Framework;
using PnP.Framework.Provisioning.Model;
using PnP.Framework.Provisioning.ObjectHandlers;
using PnP.Framework.Provisioning.Providers.Xml;
using PnP.Framework.Provisioning.Connectors;
namespace SplitPnPExport
{
class Program
{
static void Main(string[] args)
{
string siteUrl = "https://yourtenant.sharepoint.com/sites/YourSite";
string user = "user@yourtenant.onmicrosoft.com";
string password = "yourpassword";
using (var ctx = new AuthenticationManager().GetSharePointOnlineAuthenticatedContextTenant(siteUrl, user, password))
{
// Export per handler
var handlers = new[]
{
Handlers.Navigation,
Handlers.Features,
Handlers.Pages,
Handlers.ExtensibilityProviders
};
foreach (var handler in handlers)
{
var creationInfo = new ProvisioningTemplateCreationInformation(ctx.Web)
{
FileConnector = new FileSystemConnector(@"c:\pnp", ""),
HandlersToProcess = handler,
MessagesDelegate = (msg, type) => Console.WriteLine($"[{handler}] {type}: {msg}")
};
var template = ctx.Web.GetProvisioningTemplate(creationInfo);
new XMLFileSystemTemplateProvider(@"c:\pnp", "").SaveAs(template, $"{handler}.xml");
}
// Export fields
var fieldInfo = new ProvisioningTemplateCreationInformation(ctx.Web)
{
FileConnector = new FileSystemConnector(@"c:\pnp", ""),
HandlersToProcess = Handlers.Fields
};
var fieldsTemplate = ctx.Web.GetProvisioningTemplate(fieldInfo);
new XMLFileSystemTemplateProvider(@"c:\pnp", "").SaveAs(fieldsTemplate, "Fields.xml");
// Export one XML per list
ctx.Load(ctx.Web.Lists, l => l.Include(li => li.Title, li => li.Hidden));
ctx.ExecuteQuery();
foreach (var list in ctx.Web.Lists.Where(l => !l.Hidden))
{
var listInfo = new ProvisioningTemplateCreationInformation(ctx.Web)
{
FileConnector = new FileSystemConnector(@"c:\pnp", ""),
HandlersToProcess = Handlers.Lists
};
listInfo.ListsToExtract.Add(list.Title);
var listTemplate = ctx.Web.GetProvisioningTemplate(listInfo);
var safeName = list.Title.Replace(" ", "_").Replace("/", "_");
new XMLFileSystemTemplateProvider(@"c:\pnp", "").SaveAs(listTemplate, $"List_{safeName}.xml");
}
}
}
}
}
👉 Output in c:\pnp will look like:
Fields.xml
Navigation.xml
Features.xml
Pages.xml
List_Projects.xml
List_Tasks.xml
List_Announcements.xml
...
2. Importing Templates in Sequence
When importing, the order is critical:
- Fields → must exist before lists/pages reference them
- Lists → create structure and metadata
- Handlers → navigation, features, pages, etc.
Code: Import with Logs
using System;
using System.IO;
using System.Linq;
using Microsoft.SharePoint.Client;
using PnP.Framework;
using PnP.Framework.Provisioning.Providers.Xml;
using PnP.Framework.Provisioning.ObjectHandlers;
namespace SplitPnPImport
{
class Program
{
private static string logFile = @"c:\pnp\import_log.txt";
static void Main(string[] args)
{
string targetUrl = "https://yourtenant.sharepoint.com/sites/TargetSite";
string user = "user@yourtenant.onmicrosoft.com";
string password = "yourpassword";
File.WriteAllText(logFile, $"--- Import started at {DateTime.Now} ---\n");
using (var ctx = new AuthenticationManager().GetSharePointOnlineAuthenticatedContextTenant(targetUrl, user, password))
{
var provider = new XMLFileSystemTemplateProvider(@"c:\pnp", "");
var applyingInfo = new ProvisioningTemplateApplyingInformation
{
ClearNavigation = false,
MessagesDelegate = (msg, type) =>
{
Console.ForegroundColor = type switch
{
ProvisioningMessageType.Error => ConsoleColor.Red,
ProvisioningMessageType.Warning => ConsoleColor.Yellow,
ProvisioningMessageType.Progress => ConsoleColor.Cyan,
_ => ConsoleColor.White
};
Console.WriteLine($"[{type}] {msg}");
Console.ResetColor();
File.AppendAllText(logFile, $"[{DateTime.Now:HH:mm:ss}] [{type}] {msg}\n");
}
};
// 1. Fields
if (File.Exists(@"c:\pnp\Fields.xml"))
{
ApplyTemplate(ctx, provider, applyingInfo, "Fields.xml");
}
// 2. Lists
var listFiles = Directory.GetFiles(@"c:\pnp", "List_*.xml").OrderBy(f => f);
foreach (var file in listFiles)
{
ApplyTemplate(ctx, provider, applyingInfo, Path.GetFileName(file));
}
// 3. Handlers
foreach (var file in new[] { "Navigation.xml", "Features.xml", "Pages.xml", "ExtensibilityProviders.xml" })
{
if (File.Exists(Path.Combine(@"c:\pnp", file)))
{
ApplyTemplate(ctx, provider, applyingInfo, file);
}
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("🎉 Import finished successfully!");
Console.ResetColor();
}
}
private static void ApplyTemplate(ClientContext ctx, XMLFileSystemTemplateProvider provider,
ProvisioningTemplateApplyingInformation applyingInfo, string fileName)
{
Console.WriteLine($"\n📥 Applying {fileName} ...");
var template = provider.GetTemplate(fileName);
ctx.Web.ApplyProvisioningTemplate(template, applyingInfo);
Console.WriteLine($"✅ {fileName} applied.");
File.AppendAllText(@"c:\pnp\import_log.txt", $"[{DateTime.Now:HH:mm:ss}] ✅ {fileName} applied.\n");
}
}
}
3. Benefits of Modular Provisioning
- Debugging → if one list fails, you can reapply only that XML.
- Reusability → reuse
Fields.xmlorNavigation.xmlacross different sites. - Automation → orchestrate imports in CI/CD pipelines.
- Auditability → logs in console + file show exactly what happened.
✅ Key Takeaways
| Step | Purpose | File Output |
|---|---|---|
| Export per handler | Split Navigation, Features, Pages | Navigation.xml, Features.xml, Pages.xml |
| Export fields | Isolate site columns | Fields.xml |
| Export per list | Keep each list separate | List_<Name>.xml |
| Import sequence | Apply in correct order | Fields → Lists → Handlers |
| Logs | Monitor + debug | Console colors + import_log.txt |
👉 With this approach, provisioning becomes modular, transparent and maintainable, instead of a black-box “all-or-nothing” XML.
