Table of Contents

Demo.Console.Provisioning

An interactive console for the PnP Core provisioning engine: extract a template from a SharePoint site, look at what it captured, and apply it to a different site.


Before you start

You need:

  • .NET 10 SDK or later
  • A SharePoint Online site you can administer — two if you want to try the full round trip
  • An account that can sign in interactively (a browser window opens; there is no stored password)

App registration

In order to use this app you will need to register an app in Azure AD and grant it the following permissions:

  • Go to Azure Active Directory Portal

  • In App registrations, click New registration

  • Enter a name for your new app, make sure Accounts in this organizational directory only is selected. As the Redirect URI, change from Web Platform to "Mobile and Desktop Applications" use http://localhost for the redirect URI (only needed if you want use an interactive authentication flow)

  • Under Implicit grant section, check ID tokens and Access tokens

  • Under Advanced settings section, set Allow public client flows to yes

  • Go to API permissions section , click Add a permission

    • Select Microsoft Graph > Delegated permissions > select Group.ReadWrite.All
    • Select Microsoft Graph > Delegated permissions > select openid
    • Select Microsoft Graph > Delegated permissions > select profile
    • Select Microsoft Graph > Delegated permissions > select Sites.Manage.All
    • Select Microsoft Graph > Delegated permissions > select User.Read.All
    • Select SharePoint > Delegated permissions > select AllSites.FullControl
    • Select SharePoint > Delegated permissions > select AllSites.Manage
    • Select SharePoint > Delegated permissions > select TermStore.ReadWrite.All
    • Select SharePoint > Delegated permissions > select User.Read.All
  • Click Grant admin consent for {tenant}

  • From Overview,

    • copy the value of Directory (tenant) ID
    • copy the value of Application (client) ID

Running it

There are two ways in. Apply one template and exit:

cd pnpcore/samples/Demo.Console.Provisioning
dotnet run -- https://contoso.sharepoint.com/sites/target Templates/site.xml

Or with no arguments, for the menu:

dotnet run
=== PnP Core provisioning ===
  1  Extract a template from a site
  2  List saved templates
  3  Show what a saved template contains
  4  Apply a saved template to a site
  5  Export a site to a .pnp package
  6  Apply a .pnp package to a site
  7  Extract a tenant template (site collections, subsites and teams)
  0  Exit

Site urls are typed in when they are needed rather than configured, precisely so that extracting from one site and applying to another does not mean editing a settings file in between.


Extracting from the command line

dotnet run -- extract <site-url> <output.xml> [options]
dotnet run -- extract <site-url> <output.pnp> [options]    as a package, see Packages (.pnp)

A template is structure only by default — columns, content types, lists, security, navigation, branding, but none of the content sitting in them. Content is the expensive part of an extract, so you opt into it:

Option
--items Include the items of every list on the site
--items=A,B Include the items of these lists only
--files Export the files held in every document library
--files=A,B Export the files of these document libraries only
--pages Include the site's client side pages and their contents
--hidden-lists Include hidden lists in the structure
# structure only
dotnet run -- extract https://contoso.sharepoint.com/sites/marketing out.xml

# structure, every list's items, and the pages
dotnet run -- extract https://contoso.sharepoint.com/sites/marketing out.xml --items --pages

# just the two lists you care about
dotnet run -- extract https://contoso.sharepoint.com/sites/marketing out.xml --items="Tasks,Announcements"

Extracting a tenant template from the command line

dotnet run -- extract-tenant <site-url> <output.xml> [options]

A tenant template describes the site collection itself — its kind, url, title, owner, language and hub settings — in a sequence, alongside the template of its root web. It is what you apply to create a copy of the site rather than to fill an existing one.

Option
--joined-sites If the site is a hub site, also take the sites joined to it
--subsites Also take its subsites, each with a template of its own
--teams Also take the Microsoft Team behind each group connected site
--pages Include the pages of each site and their contents
--hidden-lists Include hidden lists in each site's structure
# a hub site, the sites joined to it, and their teams
dotnet run -- extract-tenant https://contoso.sharepoint.com/sites/hub tenant.xml --joined-sites --teams

Reading a site collection's properties goes through the SharePoint admin center, so this needs an account that is a SharePoint administrator. The url and title of each site are saved as parameters of the template; applying it from the menu asks for a new value for each, so that the copy does not land on the sites it came from. Applying a tenant template does not create subsites yet.

Applying from the command line

dotnet run -- <site-url> <template.xml>
dotnet run -- apply <site-url> <template.xml>    the same, spelled out
Option
-v, --verbose Log the SDK's own requests as well. Reach for this when a failure needs the traffic behind it.
-h, --help Usage.

Exit codes, so a script can tell the three outcomes apart:

Code Meaning
0 Applied, nothing reported
2 Applied, but warnings or errors were reported
1 Failed

0 and 2 are genuinely different. The engine reports a problem and carries on rather than stopping, so a run can finish having quietly skipped part of the template — the reported problems are printed at the end of the run, as in the example below.

Example:

Template: D:\templates\marketing.xml
Target:   https://contoso.sharepoint.com/sites/target

  Contains: 14 site columns, 3 content types, 6 lists

Connecting - a sign in window may appear...
Applying...
  1/20  Initializing engine
  [Warning] ListInstance Style Library is a Style Library of a site with NoScript enabled and will be skipped
  2/20  Regional Settings
  ...

Applied, but 1 thing(s) were reported:
  [Warning] ListInstance Style Library is a Style Library of a site with NoScript enabled and will be skipped

How to use the menu

1. Extract

Choose 1, then paste a site url:

Extract from which site? (full url, blank to cancel): https://contoso.sharepoint.com/sites/marketing
Save as (file name, blank for an automatic one):

Structure - columns, content types, lists, security - is always included.
Include list items as well? (y/N): y
  Which lists (comma separated, blank for all):
  Document libraries are skipped - see the note after the extract.
Include the site's pages and their contents? (y/N): y
Include hidden lists? (y/N): n

A browser window opens for sign in. Then:

Extracting...
  1/12  Initializing engine
  2/12  Site Fields
  ...
Saved Templates\sites-marketing-20260811-142233.xml

  Contains: 14 site columns, 3 content types, 6 lists, 1 client side page, 2 property bag entries

Templates land in a Templates folder beside the executable.

2. Look at it

Choose 3 and pick the file. You get the same summary, and can print the raw XML.

This is worth doing before you apply anything. An extract captures what a site has, which is usually more than you meant to copy — and the summary is the quickest way to notice that.

3. Apply

Choose 4, pick the template, then give a different site:

Apply to which site? (full url, blank to cancel): https://contoso.sharepoint.com/sites/marketing-copy

About to apply sites-marketing-20260811-142233.xml to https://contoso.sharepoint.com/sites/marketing-copy

  Contains: 14 site columns, 3 content types, 6 lists, 1 client side page, 2 property bag entries

Type the site's name to confirm, or anything else to cancel: marketing-copy

Applying changes a real site and there is no undo, which is why it asks you to type the name rather than press y. The previous prompt was a url, and urls are easy to mistype.

4. Export a site as a .pnp package

Choose 5. It asks the same questions as option 1, but writes a single .pnp package instead of an .xml file:

Extract from which site? (full url, blank to cancel): https://contoso.sharepoint.com/sites/marketing
Save as (package file name, blank for an automatic one): marketing

Structure - columns, content types, lists, security - is always included.
Include list items as well? (y/N): y
Export the files held in document libraries? (y/N): y
  Which libraries (comma separated, blank for all): Documents
Include the site's pages and their contents? (y/N): y
Saved Templates\marketing.pnp

5. Apply a .pnp package

Choose 6. It lists the .pnp files in the template folder rather than the .xml ones, then asks for the target site and confirms exactly as option 4 does.

6. Extract a tenant template

Choose 7, paste the url of a site collection, and answer whether to take the sites joined to it if it is a hub, its subsites, and the teams behind group connected sites:

Extract which site collection? (full url, blank to cancel): https://contoso.sharepoint.com/sites/hub
Save as (file name, blank for an automatic one):

The site collection and the template of its root web are always included.
If it is a hub site, include the sites joined to it? (y/N): y
Include its subsites, each with a template of its own? (y/N): n
Include the team behind each group connected site? (y/N): y
Include the pages of each site and their contents? (y/N): n
Saved Templates\tenant-sites-hub-20260811-142233.xml

  Sequence TENANTSEQUENCE:
    CommunicationSiteCollection Hub (hub)
    TeamSiteCollection Marketing
  2 site template(s), 5 parameter(s)
  Team Marketing: 3 channel(s), 12 app(s)

Applying it with option 4 asks whether the template should create its own sites, and then for a value for each parameter — the url and title of every site among them.


Packages (.pnp)

A .pnp package is a single file holding the template and everything it references — the provisioning XML plus the documents exported from libraries. It is an OPC package, the same container format as .docx, so it can be copied, versioned and handed over as one artefact.

Use a package when the template ships content. An .xml template records file entries but leaves the bytes beside it on disk, so moving the template to another machine without its neighbouring files silently produces an apply that creates empty libraries. A .pnp cannot come apart that way.

From the command line

The file extension decides the format — there is no separate switch:

# export a site as a package, with the items and the documents in it
dotnet run -- extract https://contoso.sharepoint.com/sites/marketing marketing.pnp --items --files

# apply that package to another site
dotnet run -- apply https://contoso.sharepoint.com/sites/marketing-copy marketing.pnp
# the same thing as loose files
dotnet run -- extract https://contoso.sharepoint.com/sites/marketing marketing.xml --items --files
dotnet run -- apply https://contoso.sharepoint.com/sites/marketing-copy marketing.xml

Everything that works for .xml works for .pnp — --items, --files, --pages, --hidden-lists, the exit codes, and the warning output are all unchanged.

What ends up inside

template.xml the provisioning template
exported library files under the folder path they came from
package properties author (your Windows user name) and generator

When --files is combined with a .pnp output, the file connector points at the package, so the documents are written into it as they are exported rather than to the folder next to it.

Reading a package back

Applying a .pnp opens the package, reads template.xml out of it, and points the template's connector at the package — so file uploads resolve from inside it. Nothing has to be unzipped.

Note: the .pnp support in PnP.Core.Provisioning is a direct port of PnP Framework's OpenXML connector and has no automated test coverage yet. Check the result of a package round trip before relying on it for anything you cannot repeat.


Running as the application (app-only)

Everything above signs a user in. For unattended use — a build agent, a scheduled job — the sample can authenticate as the application instead. Set a certificate thumbprint in appsettings.json and no browser window appears:

{
  "CustomSettings": {
    "ClientId": "<client id of the app registration>",
    "TenantId": "<tenant id>",
    "CertificateThumbprint": "DF5450F6FB23838465128BBFC95C86091504B16B",
    "CertificateStoreName": "My",
    "CertificateStoreLocation": "CurrentUser",
    "TemplateFolder": "Templates"
  }
}

Leave CertificateThumbprint empty and the sample signs a user in interactively as before. CertificateStoreName and CertificateStoreLocation are optional and default to My and CurrentUser.

A certificate is required — a client secret will not work

Azure AD app-only against SharePoint must use a certificate. SharePoint rejects app-only tokens obtained with a client secret, and PnP Core ships no client-secret provider.

$c = New-SelfSignedCertificate -Subject "CN=PnPProvisioningAppOnly" `
    -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable `
    -KeySpec Signature -NotAfter (Get-Date).AddYears(2)
Export-Certificate -Cert $c -FilePath "$env:USERPROFILE\Desktop\PnPProvisioningAppOnly.cer"
$c.Thumbprint

Upload the .cer under Certificates & secrets → Certificates on the app registration.

Application permissions

These are Application permissions, not Delegated, and they need Grant admin consent:

API Permission Needed for
SharePoint Sites.FullControl.All applying templates, creating sites
SharePoint User.Read.All resolving users in security and list items
Microsoft Graph Sites.FullControl.All modern sites
Microsoft Graph Group.ReadWrite.All group connected sites
Microsoft Graph User.Read.All user lookup
Microsoft Graph TermStore.ReadWrite.All term groups, term sets, taxonomy columns

The Graph term store permission is the one people miss. SharePoint has a permission with the same name, and picking that one instead leaves every taxonomy operation failing with HTTP 403 accessDenied — term groups, labels and custom properties, while term sets and terms keep working, because those go through a different path.

What behaves differently app-only

There is no current user, and two things follow from that:

  • Owner becomes mandatory on any site a template creates. Under a signed-in user the owner defaults to that user; as an application there is nobody to default to, and site creation fails with "You need to set an owner when using Application permissions to create a communication site". Set Owner on the site collection in hierarchy and sequence templates.
  • Anything resolving "me" — a template using the current user as a value — has nothing to resolve.

Templates that apply cleanly interactively can fail unattended for exactly these reasons, so it is worth applying a template app-only once before relying on it in automation.

Configuration

appsettings.json:

{
  "CustomSettings": {
    "ClientId": "31359c7f-bd7e-475c-86db-fdb8c937548e",
    "TenantId": "common",
    "RedirectUri": "http://localhost",
    "TemplateFolder": "Templates"
  },
  "Logging": {
    "LogLevel": { "Default": "Warning" }
  }
}
Setting What it does
ClientId The Entra application to sign in with. Replace with your own registration if you have one.
TenantId common asks which tenant at sign in. Set a tenant id to skip that.
RedirectUri Where sign in returns to. Must match the application's registration — leave it alone unless you changed ClientId.
TemplateFolder Where extracted templates are written, relative to the executable.
CertificateThumbprint Set it to authenticate as the application instead of signing a user in. See Running as the application. Leave empty for interactive sign in.
CertificateStoreName Optional, defaults to My.
CertificateStoreLocation Optional, defaults to CurrentUser.

Raise Logging:LogLevel:Default to Information to see what the engine is doing underneath. It is verbose — the sample prints its own progress precisely so you do not need to.

back to samples