Split your PnP Provisioning schema into multiple files for better maintenance

This post is for those, who have huge PnP Provisioning schema with hundreds or thousands of lines.

Or for those, who want to organize PnP provisioning schema into isolated logical components (folders & files) instead of having a solid single schema file.

When your PnP provisioning template grows, it becomes harder to search nodes, to add and update entities, to merge changes from other team members. Maybe you, reader, remember times of farm solutions (I've never thought that I will ever use this term in this blog in 2019, but anyway...) where all components were logically divided into different folders and files. We had different files for list schema, for fields, files, and for many other SharePoint artifacts. Having multiple files makes it easier to maintain such a solution (especially if you provision a lot of components). Additionally, your components are described in separate files and are referenced in different templates (instead of copy-pasting). In other words, you achieve schema reuse.

It's possible to have such logical separation in your PnP provisioning schema as well! Let's explore how to do it.

What we're going to do

Let's say we have a provisioning template with a list definition. I want to move the list definition into a separate file, in order to achieve logical separation of components in my schema.

Please note that I'm not going to create separate PnP templates. Technically it's the same template in different files. 

xi:include for the rescue

PnP provisioning schema supports xi:include elements. That's a way to move the list definition into separate files. 

This is how your schema might look like when you have list definitions in separate files (take a note at xi:include element):

<?xml version="1.0" encoding="utf-8" ?>
<pnp:Provisioning xmlns:pnp="http://schemas.dev.office.com/PnP/2019/03/ProvisioningSchema"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xsi:schemaLocation="http://schemas.dev.office.com/PnP/2019/03/ProvisioningSchema" >

  <pnp:Preferences Generator="Manual"></pnp:Preferences>

  <pnp:Templates ID="CommonTemplates">

    <pnp:ProvisioningTemplate ID="AwesomeTeamTemplate">

      <pnp:Lists>
        <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="templates/lists/awesome-news.xml" />
      </pnp:Lists>
    </pnp:ProvisioningTemplate>
  </pnp:Templates>

</pnp:Provisioning>

awesome-news.xml just a regular XML file with pnp schema for a list:

<pnp:ListInstance 
                  Title="awesome-news"
                  TemplateType="100" 
                  Url="Lists/awesomenews">
  <pnp:ContentTypeBindings>
    <pnp:ContentTypeBinding ContentTypeID="0x01" Default="true" />
    <pnp:ContentTypeBinding ContentTypeID="0x0120" />
  </pnp:ContentTypeBindings>
  <pnp:Views>
    <View Name="{8A8627BA-BE89-4C82-AFE1-7084920EE50E}" DefaultView="TRUE" MobileView="TRUE" MobileDefaultView="TRUE" Type="HTML" DisplayName="All Items" Url="/sites/pnp-template/Lists/awesomenews/AllItems.aspx" Level="1" BaseViewID="1" ContentTypeID="0x" ImageUrl="/_layouts/15/images/generic.png?rev=44">
      ...
    </View>
  </pnp:Views>
  <pnp:Fields>
    <Field AppendOnly="FALSE" DisplayName="Description" IsolateStyles="TRUE" NumLines="6" RichText="TRUE" RichTextMode="FullHtml" Title="Description" Type="Note" ID="{f9b1b539-e820-4554-b3b7-00a556457d4c}" SourceID="{{listid:awesome-news}}" StaticName="Description" Name="Description" ColName="ntext2" RowOrdinal="0" />
  </pnp:Fields>
  <pnp:FieldRefs>
    <pnp:FieldRef ID="3a6b296c-3f50-445c-a13f-9c679ea9dda3" Name="ComplianceAssetId" DisplayName="Compliance Asset Id" />
    ....
  </pnp:FieldRefs>
</pnp:ListInstance>

While it works, there is an issue with intellisense and schema validation inside Visual Studio.

Fixing intellisense and schema validation inside Visual Studio

I prefer to use Visual Studio for modifying XML files with schema because Visual Studio has excellent intellisense. VSCode still lacks this feature for XML files with XSD schemas (or I haven't found proper add-in). 

To fix the issue in Visual Studio you should download xsd schema for your template (in my case it's 2019-03 schema). In Visual Studio create a folder schemas and add downloaded schema to that folder. 

Now from your root pnp template XML file, you should explicitly reference downloaded file:

<?xml version="1.0" encoding="utf-8" ?>
<pnp:Provisioning xmlns:pnp="http://schemas.dev.office.com/PnP/2019/03/ProvisioningSchema"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xsi:schemaLocation="http://schemas.dev.office.com/PnP/2019/03/ProvisioningSchema ..\..\schemas\ProvisioningSchema-2019-03.xsd" >

  <pnp:Preferences Generator="Manual"></pnp:Preferences>

  <pnp:Templates ID="CommonTemplates">

    <pnp:ProvisioningTemplate ID="AwesomeTeamTemplate">
.....

Check a note on the xsi:schemaLocation="http://schemas.dev.office.com/PnP/2019/03/ProvisioningSchema ..\..\schemas\ProvisioningSchema-2019-03.xsd" line. That way we explicitly say for Visual Studio to use this XSD schema. Visual Studio will still complain about xi:include element, but don't pay attention to it. 

We fixed our root template, but not the list definition. For the list definition, create a copy of downloaded XSD and rename it to <name>-custom.xsd (in my case it's ProvisioningSchema-2019-03-custom.xsd). In our pnp list definition XML Visual Studio will highlight the root element saying that there is no pnp:ListInstance element. And it's true because according to the XSD schema pnp:Provisioning is the only possible root element. 

How to fix it? Very simple, open ProvisioningSchema-2019-03-custom.xsd in edit mode, and add <xsd:element name="ListInstance" type="pnp:ListInstance" /> at the root of the schema. Then update PnP list definition to include a reference to our custom schema:

<pnp:ListInstance xmlns:pnp="http://schemas.dev.office.com/PnP/2019/03/ProvisioningSchema"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xsi:schemaLocation="http://schemas.dev.office.com/PnP/2019/03/ProvisioningSchema ..\..\..\..\schemas\ProvisioningSchema-2019-03-custom.xsd" 
                  Title="awesome-news"
                  TemplateType="100" 
                  Url="Lists/awesomenews">
  <pnp:ContentTypeBindings>
    <pnp:ContentTypeBinding ContentTypeID="0x01" Default="true" />
.....

Now you have intellisense for ListInstance element as well!

How to do that for other elements? Just repeat the process and keep updating your <name>-custom.xsd file. For example, you want to have intellisense for separate files for ClientSidePages. In you <name>-custom.xsd search for ClientSidePages. You're looking for <xsd:complexType name="ClientSidePages"> node. If there is a node with such name, then add <xsd:element name="ClientSidePages" type="pnp:ClientSidePages" /> to the top of your <name>-custom.xsd file. If there is <xsd:element and no <xsd:complexType with such name, then you should copy the whole <xsd:element definition to the top of your <name>-custom.xsd file. 

Here is how my custom xsd file looks like with root definitions for ListInstance, SiteFields, ClientSidePages, Files:

<?xml version="1.0" encoding="utf-8"?>
<xsd:schema targetNamespace="http://schemas.dev.office.com/PnP/2019/03/ProvisioningSchema"
    elementFormDefault="qualified"
    xmlns="http://schemas.dev.office.com/PnP/2019/03/ProvisioningSchema"
    xmlns:pnp="http://schemas.dev.office.com/PnP/2019/03/ProvisioningSchema"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">

  <xsd:element name="ListInstance" type="pnp:ListInstance" />
  <xsd:element name="ClientSidePages" type="pnp:ClientSidePages" />
  <xsd:element name="Files">
    <xsd:complexType>
      <xsd:sequence>
        <xsd:element name="File" type="pnp:File"
                     minOccurs="0" maxOccurs="unbounded" />
        <xsd:element name="Directory" type="pnp:Directory"
                     minOccurs="0" maxOccurs="unbounded" />
      </xsd:sequence>
    </xsd:complexType>
  </xsd:element>

  <xsd:element name="SiteFields">
    <xsd:complexType>
      <xsd:sequence>
        <xsd:any minOccurs="1" maxOccurs="unbounded"
                 processContents="lax" namespace="##any" />
      </xsd:sequence>
    </xsd:complexType>
  </xsd:element>

  <!-- The main element -->
  <xsd:element name="Provisioning" type="pnp:Provisioning">
    <xsd:annotation>
      <xsd:documentation xml:lang="en">
        This is the base element of a Provisioning File.
      </xsd:documentation>
    </xsd:annotation>
  </xsd:element>
......

If you want later to have a file with all components merged into it, you can simply use below code to save it on disk:

var provider = new XMLFileSystemTemplateProvider($@"{Environment.CurrentDirectory}\..\..\templates\awesome-team\", "");
var template = provider.GetTemplate("awesome-team.xml");
provider.SaveAs(template, "awesome-team.generated.xml");

Sample project

To give you a better idea on how to organize different components into folder and files, I've created a sample project at GitHub here. It uses different XML definition files for lists, fields, files, client-side pages. 

Some limitations

You can't have any part of the original PnP schema divided with xi:include. However, almost any of the root components of pnp:ProvisioningTemplate should just work.