Extensibility handler for OfficeDev PnP provisioning framework

The PnP Provisioning framework can be used to remotely extract and provision standardized sites based on templates. This is a typical requirement for enterprises and we have classically used technologies like site definitions, site templates or web templates to achieve this. Releases of the framework are made recently, but sometimes functionality is desired which the framework doesn’t provide (yet).

To extend the framework yourself Extensibility Handlers can be implemented.
An extensibility handler can be executed when provisioning and extracting a site as template.

To immediately dive into the details, let’s start Visual Studio and

  1. Start a new class library project
  2. Add a references to Microsoft.SharePoint.Client, Microsoft.Client.Runtime and OfficeDevPnp.Core
  3. Add a class and implement interface IProvisioningExtensibilityHandler

At this point the code looks like this:

public class DoSomethingHandler : IProvisioningExtensibilityHandler
{
  public ProvisioningTemplate Extract(ClientContext ctx, ProvisioningTemplate template,
   ProvisioningTemplateCreationInformation creationInformation,
   PnPMonitoredScope scope, string configurationData)
  {
    throw new NotImplementedException();
  }

  public IEnumerable<TokenDefinition> GetTokens(ClientContext ctx,
   ProvisioningTemplate template, string configurationData)
  {
    throw new NotImplementedException();
  }

  public void Provision(ClientContext ctx, ProvisioningTemplate template,
   ProvisioningTemplateApplyingInformation applyingInformation,
   TokenParser tokenParser, PnPMonitoredScope scope, string configurationData)
  {
    throw new NotImplementedException();
  }
}

When a site is exported the Extract method is called and when a site is created the Provision method is called.
The GetTokens method provides a way to use your own token definitions.

To use the extensibility provider in the template xml a providers section has to be added:

<pnp:Providers>
 <pnp:Provider Enabled="true"
   HandlerType="<namespace + classname, <assembly name>, <version>, <culture>, <publickeytoken>">
 <pnp:Configuration>
 <valid_xml xmlns="http://schemas.itidea.com/extensibilityproviders">true</valid_xml>
 </pnp:Configuration>
 </pnp:Provider>
</pnp:Providers>

The Provider element has two attributes:

  1. Enabled, to enable or (temporarily) disable the handler
  2. HandlerType, which describes where the handler is located.

Inside the pnp:Configuration element anything can be placed, as long it’s valid xml.

The Provisioning engine has to be able to find the assembly referenced in the HandlerType attribute. To accomplish this the PowerShell commandlet Add-Type can be used.
To use the (right now not so useful) handler three lines of PowerShell are sufficient:

Connect-SPOnline -Url <url>
Add-Type -Path C:\_Tools\ITIdea.PnPExtensions\bin\Debug\ITIdea.PnPExtensions.dll
Apply-SPOProvisioningTemplate -Path C:\_Tools\ITIdea.PnPExtensions\Template\template.xml

Ofcourse this returns a NotImplementedException, because the methods aren’t implemented.

Attach the powershell process to debug the handler in Visual Studio.
The xml defined inside the handler is captured in a string called configurationData, which can be serialized when needed.
PnP Extensibility Handler - configurationdata

Token definition

The Provisioning Framework has lots of token definitions implemented, like {sitename}, {roledefinition:Reader}, {sitecollectiontermstoreid} and much more.
PnP Extensibility Handler - list of tokendefinitions

When a custom token is needed it can be implemented by creating a class derived from TokenDefinition and override the method GetReplaceValue.
Suppose the title of the rootweb of the site collection is needed in a subsite.

public class SiteCollectionNameToken : TokenDefinition
{
  public SiteCollectionNameToken(Web web)
   : base(web, "{sitecollectionname}")
  {
  }

  public override string GetReplaceValue()
  {
    if (CacheValue == null)
    {
      this.Web.EnsureProperty(w => w.Url);
      using (ClientContext context = this.Web.Context.Clone(this.Web.Url))
      {
        var site = context.Site;
        context.Load(site, s => s.RootWeb.Title);
        context.ExecuteQueryRetry();
        CacheValue = context.Site.RootWeb.Title;
      }
    }
    return CacheValue;
  }
}

The token which can be used in the template xml is listed in the constructor. In this case {sitecollectionname} can be used.

This token will be replaced by the actual value in the method GetReplaceValue.
Once the tokendefinition is in place it can be used in the GetTokens method in the extensibility handler.

public IEnumerable<TokenDefinition> GetTokens(ClientContext ctx,
 ProvisioningTemplate template, string configurationData)
{
  var customTokens = new List<TokenDefinition>();
  customTokens.Add(new SiteCollectionNameToken(ctx.Web));
  return customTokens;
}

And it can be used in the xml which describes the template.

<pnp:Providers>
  <pnp:Provider Enabled="true"
   HandlerType="ITIdea.PnPExtensions.Extensions.DoSomethingHandler, ITIdea.PnPExtensions,
   Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
    <pnp:Configuration>
      <valid_xml xmlns="http://schemas.itidea.com/extensibilityproviders">{sitecollectionname}</valid_xml>
    </pnp:Configuration>
  </pnp:Provider>
</pnp:Providers>

To show the output the Provision method writes the configurationData to the console:

PnP Extensibility Handler - replaced token

As can be seen the token is replaced by its value.

Provision

Suppose permissions are inherited at subsite level. The permissions of a list at subsite level are broken and some sitegroups have to be deleted from this list by the provisioning framework.
The title of the rootweb of the site collection is ‘Dev Anita’ in this example, so the sitegroups are called ‘Dev Anita Members’, ‘Dev Anita Owners’ and ‘Dev Anita Visitors’.
The token defined earlier gets the title of the rootweb of the site collection, convenient…

To be able to delete groups from a list a couple of things are needed, like the list, the group names and roledefinitions. I came up with the following XML:

<pnp:Providers>
  <pnp:Provider Enabled="true"
   HandlerType="ITIdea.PnPExtensions.Extensions.DoSomethingHandler, ITIdea.PnPExtensions,
   Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
    <pnp:Configuration>
      <List Name="Documents" xmlns="http://schemas.itidea.com/extensibilityproviders">
        <SiteGroups>
          <SiteGroup Name="Approvers" RoleDefinitionName="Approve"/>
          <SiteGroup Name="{sitecollectionname} Members" RoleDefinitionName="{roledefinition:Editor}"/>
        </SiteGroups>
      </List>
    </pnp:Configuration>
  </pnp:Provider>
</pnp:Providers>

{roledefinition:Editor} is a token which is implemented by the OfficeDev PnP provisioning framework.
Inside the Provision method the xml configurationdata is received as a string and written to the console for convenience:
PnP Extensibility Handler - serialized xml provision

To be able to process the XML nicely it’s serialized to a custom object ‘MyList’ which consists of the items the XML does.

The code to remove the roledefinition from the list is easy:


public void Provision(ClientContext ctx, ProvisioningTemplate template,
 ProvisioningTemplateApplyingInformation applyingInformation,
 TokenParser tokenParser, PnPMonitoredScope scope, string configurationData)
{
  Console.WriteLine(configurationData);

  var reader = new StringReader(configurationData);
  var serializer = new XmlSerializer(typeof(MyList));
  var listConfig = (MyList)serializer.Deserialize(reader);

  List theList = ctx.Web.Lists.GetByTitle(listConfig.Name);
  ctx.Load(theList);
  ctx.ExecuteQueryRetry();

  SiteGroups groups = listConfig.SiteGroups;
  foreach (var group in groups.Groups)
  {
    theList.RemovePermissionLevelFromGroup(group.Name, group.RoleDefinitionName);
  }

  theList.Update();
}

Summary

The OfficeDev PnP framework provides a lot of functionality to remotely extract and provision sites. To implement specific functionality an extensibility handler can be developed using CSOM.
Be aware that handlers are always executed last irrespectively their location in the xml file.

Leave a Reply

Your email address will not be published. Required fields are marked *