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:

  1. Fields → must exist before lists/pages reference them
  2. Lists → create structure and metadata
  3. 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.xml or Navigation.xml across different sites.
  • Automation → orchestrate imports in CI/CD pipelines.
  • Auditability → logs in console + file show exactly what happened.

✅ Key Takeaways

StepPurposeFile Output
Export per handlerSplit Navigation, Features, PagesNavigation.xml, Features.xml, Pages.xml
Export fieldsIsolate site columnsFields.xml
Export per listKeep each list separateList_<Name>.xml
Import sequenceApply in correct orderFields → Lists → Handlers
LogsMonitor + debugConsole colors + import_log.txt

👉 With this approach, provisioning becomes modular, transparent and maintainable, instead of a black-box “all-or-nothing” XML.


Edvaldo Guimrães Filho Avatar

Published by