Thursday, March 13, 2014

A Model for Upgradeable SharePoint Sites


Intro


It's not necessarily obvious how to handle changes to a deployed SharePoint-based application. I'm going to lay out a proven system for upgradeable SharePoint sites that I've successfully used on multiple projects.

To illustrate some of my points, I'm going to reference an open source project I created called SPMeta-Northwind. It's a reimagining of Microsoft's classic Northwind database schema as a SharePoint farm solution. I developed SPMeta-Northwind for SharePoint 2010 in Visual Studio 2012. If you're using SharePoint 2013 or a different version of Visual Studio, I believe the general concepts will still apply.


Feature Upgrading


SharePoint 2010 introduced the concept of feature upgrading. This capability provides a great way forward for pushing changes out to existing SharePoint sites. My Northwind solution (pictured below) contains one upgradeable feature, called Northwind.Common.


Setting up the feature properties

There are a few settings we'll need to configure in the Properties window for the Northwind.Common feature.


We need to configure which assembly ('Upgrade Actions Receiver Assembly') and class ('Upgrade Actions Receiver Class') will be invoked when the feature is upgraded. We'll set it to use the same assembly and class that are used on feature activation.

Decide on a beginning version number for the feature. I tend to go with '1.0.0.1'.

Setting up the feature manifest

To make the feature upgrade-aware, we'll need to make a small edit to the manifest.


Add an 'UpgradeActions' element with a 'CustomUpgradeAction' element inside it. For our purposes, the 'Name' attribute really doesn't matter--I usually just use 'Upgrade'.

There is another edit you may want to make to the feature manifest if you're using modules, to ensure that new files added to the module get deployed on upgrade, but I'll cover that topic in a future post.

Deploying feature upgrades

My Northwind solution includes some PowerShell scripts for deploying upgrades. I can go over them in depth in a future post, but for the purposes of this post, I'll just point out a couple lines from FeatureUtils.ps1 directly related to feature upgrading.

if ($feature.Version.CompareTo($feature.Definition.Version) -lt 0)
{
    # ...snip...

    $exceptions = $feature.Upgrade($false)
 
    # ...snip...
}

In code, you can compare the version of an activated feature against the version of its installed feature definition to tell if it needs to be upgraded. If so, you can call SPFeature.Upgrade() to bring the feature up to date.


Idempotent Feature Receivers


Feature receivers represent a powerful hook for running code against your SharePoint environment. My philosophy around feature receivers includes the concept of idempotence.

Not relying on version numbers

When my journey with SharePoint 2010 and its upgradeable feature concept began, I quickly learned that maintaining different code paths for specific feature versions was unsustainable. Since features are aware of which specific version number they had at the time of their last upgrade, you could be tempted to write conditional logic in your feature receiver that checks that version number and takes divergent paths based on it. Not only do I discourage you from doing this sort of thing, I discourage you from distinguishing between feature activation and feature upgrading.

FeatureActivated and FeatureUpgrading identical

Let's take a look at the Northwind.Common feature receiver.

public class NorthwindCommonEventReceiver : SPFeatureReceiver
{
    // ...snip...

    public override void FeatureActivated(SPFeatureReceiverProperties properties)
    {
        SPSite site = properties.Feature.Parent as SPSite;

        Ensure(site.RootWeb);
    }

    public override void FeatureUpgrading(SPFeatureReceiverProperties properties, string upgradeActionName, IDictionary<string, string> parameters)
    {
        SPSite site = properties.Feature.Parent as SPSite;

        Ensure(site.RootWeb);
    }

    // ...snip...
}

The contents of FeatureActivated and FeatureUpgrading are identical. I design my feature receivers to not only ignore which version they're upgrading from, but to not even care whether they're upgrading or activating. The resulting state will be the same regardless. This approach requires thinking about changes to your SharePoint environment as being "ensured."


"Ensuring"


The term I use to indicate idempotence in my code is "ensure." I was inspired to use this term by the SharePoint API's own SPWeb.EnsureUser(). If you're not familiar with this method, it simply does what it needs to do internally to make sure that a user with a particular login name exists and returns the user. It doesn't matter if you're calling it for the first time or the fiftieth time--the result is the same.

IEnsurableList

As I discussed in my last post, one of the ways I conceive of SharePoint is as a relational database system. And one of the most common upgrades I find myself doing is making "schema" changes, i.e., structural changes to SharePoint lists. This includes adding fields and views to lists and changing their various properties.

My approach to "ensuring" lists and their later schema changes is by maintaining one .NET class per list. Microsoft's famous Northwind database schema has 13 tables, and my Northwind solution has 11 "list classes" as I call them (I was able to eliminate 2 tables via SharePoint's unique functionality).

Microsoft's Northwind database schema

One "list class" per SharePoint list

What all lists have in common is that they can be ensured (FeatureActivated & FeatureUpgrading) or torn down (FeatureDeactivating). I express this commonality via a C# interface called IEnsurableList.

namespace Northwind.Common.Lists.Base
{
    public interface IEnsurableList
    {
        void Ensure(SPWeb web);
        void TearDown(SPWeb web);
    }
}

All of my list classes implement this interface. This allows each list to be treated identically by my ListManager class.

ListManager

The ListManager takes a collection of IEnsurableLists and knows how to ensure them and tear them down in the proper order.

public class NorthwindCommonEventReceiver : SPFeatureReceiver
{
    private readonly ListManager listManager = new ListManager(new List<IEnsurableList>
    {
        new CustomerDemographics(),
        new Customers(),
        new Employees(),
        new Shippers(),
        new Orders(),
        new Suppliers(),
        new Categories(),
        new Products(),
        new Regions(),
        new Territories(),
        new OrderDetails()
    });

    // ...snip...
}

I like to declare a ListManager instance right at the top of my feature receiver, so that you can see clearly which lists the feature ensures and in what order.

SPFieldCollection.Ensure<T>()

The Northwind solution contains several extension methods that augment the SharePoint API with "ensure" behavior. I'll talk about the most important one, SPFieldCollection.Ensure<T>().

public static T Ensure<T>(this SPFieldCollection fields, string fieldDisplayName) where T : SPField
{
    if (typeof(T) == typeof(SPFieldLookup))
    {
        throw new ArgumentException("Lookups are not supported");
    }

    SPFieldType fieldType = SharePointHelper.GetFieldType<T>();

    if (fields.ContainsField(fieldDisplayName))
    {
        SPField field = fields.GetField(fieldDisplayName);

        if (field.Type == fieldType)
        {
            return field as T;
        }

        field.Delete();
    }

    fields.Add(fieldDisplayName, fieldType, false);

    return fields.GetField(fieldDisplayName) as T;
}

Ensuring fields is so common that I tried to distill the process into its tersest form, reducing noise in the code, and maintaining maximum readability. I'll get more into depth about the design decisions behind my various extension methods in a later post, but for now, I just want to show how the concept of "ensuring" applies to fields. Below is a snippet from Employees.cs, where I'm ensuring the fields for the Employees list.

private void EnsureFields(SPList list)
{
    SPFieldText lastName = list.Fields.Ensure<SPFieldText>("Last Name");
    lastName.Required = true;
    lastName.MaxLength = 20;
    lastName.Update();

    SPFieldText firstName = list.Fields.Ensure<SPFieldText>("First Name");
    firstName.Required = true;
    firstName.MaxLength = 10;
    firstName.Update();

    // ...snip...

    SPFieldDateTime birthDate = list.Fields.Ensure<SPFieldDateTime>("Birth Date");
    birthDate.DisplayFormat = SPDateTimeFieldFormatType.DateOnly;
    birthDate.Update();

    // ...snip...

    SPFieldUrl photo = list.Fields.Ensure<SPFieldUrl>("Photo");
    photo.DisplayFormat = SPUrlFieldFormatType.Image;
    photo.Update();

    list.Fields.Ensure<SPFieldMultiLineText>("Notes");

    // ...snip...

    SPFieldCalculated displayName = list.Fields.Ensure<SPFieldCalculated>("Display Name");
    displayName.Formula = "=[First Name]&\" \"&[Last Name]";
    displayName.Update();

    // ...snip...
}


Closing


I've tried to provide a clear way forward for upgradeable SharePoint sites. We've looked at how to set up a SharePoint feature to be upgradeable, how to deploy upgrades via a PowerShell script, how to write idempotent feature receivers, and the concept of "ensuring." 

The SPMeta-Northwind project is a working example of my approach, and if you're a "show me the code" kind of person (like I am), then I hope you'll find it helpful.

See you next time.

0 comments:

Post a Comment