One of the great improvements in SharePoint 2010 over previous versions has been in the area of records management and information policies. Right now I’m working on a project where I need to programmatically set retention policies on content types. There are a couple things to note about retention (also known as expiration) policies:

  • They can be modified even after the “trigger” action (i.e. created or last modified) has happened. With a standard workflow, the workflow kicks off when the trigger happens (such as the document is created or is modified), and at that point the workflow is “dehydrated” into the database. The only change you can make to a workflow at that point is pretty much to delete the workflow and start again. In contrast, I can set a retention policy on a content type in my site collection that says, “Recycle documents one year after the created date”, and then decide later that the number should really be 2 years. When I change the retention policy on the content type, all content types in my site collection using that content type will automatically reflect the new retention policy.
  • Retention policies can be set either at the site collection level or at the list level, but not both. This is different than standard content type behavior, where you can set properties on a content type at the list level that override the properties you set at the site collection level.
  • If you have the In Place Records Management feature enabled in the site collection, you can have separate expiration policies for records and for non-records.
  • You can make a Document Library into a Records Library if Records Management has been enabled. You can do this by going to the List Settings page of the document library and clicking on the Record Declaration Settings link. Basically, a Records library is simply a document library that has been configured so that any documents added to the library are automatically declared a record. You can also do this programmatically by using the Microsoft.Office.RecordsManagement.RecordsRepository.Records.ConfigureListForAutoDeclaration() method.

To work with expiration policies programmatically, you’ll be using the Microsoft.Office.RecordsManagement.InformationPolicy namespace which is part of the Microsoft.Office.Policy assembly.

Any content type can have multiple information policies assigned to it. For instance, a content type could have an expiration policy, but also have a barcode policy assigned to it. To retrieve all the information policies associated with a content type, you can use the static method Policy.GetPolicy(SPcontentType contentType). This will return a collection of PolicyItem objects. Each PolicyItem object has an Id property. If the information policy is an expiration policy, it will have an ID of Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration. Once you’ve identified your policy item, you can retrieve the specifics about by querying the CustomData property. The configuration of your expiration policy will be stored as an XML string in the CustomData property.

So, to find out information about a given expiration policy, you could use code like this:

Policy policy = Policy.GetPolicy(contentType);
if (policy != null)
{
  foreach (PolicyItem policyItem in policy.Items)
  {
    if (policyItem.Id == "Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration")
    {
      customData = policyItem.CustomData;
    }
  }
}

The CustomData XML string might look something like this:

<data>
  <formula id="Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Formula.BuiltIn">
    <number>0</number>
    <property>Created</property>
    <period>days</period>
  </formula>
  <action type="action" id="Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Action.MoveToRecycleBin" />
</data>

The node tells SharePoint what the unit of time is (days, months or years). The tells SharePoint what the period of time itself is. The node tells SharePoint what the trigger is, whether it’s when the item was Created or Modified. The node’s id attribute tells SharePoint what should happen once the elapsed time has happened, whether the item should be moved to the recycle bin, permanently deleted, etc. This isn’t a straight enumeration because the whole XML is a string (rather than enum), but the various values you could use are listed here: http://msdn.microsoft.com/en-us/library/dd928385(v=office.12).aspx.

So, with that in mind, you could use a method like this to create your own XML string to populate your CustomData property of your PolicyItem, passing in your various values:

public static string GeneratePolicyItemXML(string period, string periodUnit, string periodAction, string triggerAction)
{
  StringBuilder xml = new StringBuilder("<data><formula id="Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Formula.BuiltIn">");
  xml.Append("<number>");
  xml.Append(period);
  xml.Append("</number><property>");
  xml.Append(periodAction);
  xml.Append("</property><period>");
  xml.Append(periodUnit);
  xml.Append("</period></formula><action type="action" id="");
  xml.Append(triggerAction);
  xml.Append("" /></data>");
  
  return xml.ToString();
}

Now, to make things more complicated, if you have In Place Records Management turned on, you could potentially have different policies assigned to records and non-records. The XML of the CustomData property looks slightly different. Now you have <Schedule> nodes, with a “Default” schedule for non-records and a specific “Record” type Schedule that will be executed on documents that have been declared a record. Your XML might look like this:

<Schedules nextStageId="3" default="false">
  <Schedule type="Default">
    <stages>
      <data stageId="1">
        <formula id="Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Formula.BuiltIn">
          <number>3</number>
          <property>Modified</property>
          <period>years</period>
        </formula>
        <action type="action" id="Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Action.MoveToRecycleBin" />
      </data>
    </stages>
  </Schedule>
  <Schedule type="Record">
    <stages>
      <data stageId="2">
        <formula id="Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Formula.BuiltIn">
          <number>6</number>
          <property>Modified</property>
          <propertyId>8c06beca-0777-48f7-91c7-6da68bc07b69</propertyId>
          <period>months</period>
        </formula>
        <action type="action" id="Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Action.Delete" />
      </data>
    </stages>
  </Schedule>
</Schedules>

The only thing to note is that the node of type Record has an additional node called propertyId which has a particular GUID. (This is not a randomly generated GUID, but should always be this value.) I’ll be perfectly honest. I don’t know what this value refers to, but it seems to always be there.

Here’s a similar method you can use to construct this string:

public static string GeneratePolicyItemXML(string period, string periodUnit, string periodAction, string triggerAction, string recordPeriod, string recordPeriodUnit, string recordPeriodAction, string recordTriggerAction)
{
   StringBuilder xml = new StringBuilder("<Schedules nextStageId="3" default="false">");
   xml.Append("<Schedule type="Default"><stages><data stageId="1">");
   xml.Append("<formula id="Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Formula.BuiltIn">");
   xml.Append("<number>");
   xml.Append(period);
   xml.Append("</number><property>");
   xml.Append(periodAction);
   xml.Append("</property><period>");
   xml.Append(periodUnit);
   xml.Append("</period></formula><action type="action" id="");
   xml.Append(triggerAction);
   xml.Append("" /></data></stages></Schedule>");
   xml.Append("<Schedule type="Record"><stages><data stageId="2">");
   xml.Append("<formula id="Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Formula.BuiltIn">");
   xml.Append("<number>");
   xml.Append(recordPeriod);
   xml.Append("</number><property>");
   xml.Append(recordPeriodAction);
   xml.Append("</property><propertyId>8c06beca-0777-48f7-91c7-6da68bc07b69</propertyId><period>");
   xml.Append(recordPeriodUnit);
   xml.Append("</period></formula><action type="action" id="");
   xml.Append(recordTriggerAction);
   xml.Append("" /></data></stages></Schedule></Schedules>");
   return xml.ToString();
}

To add a new PolicyItem to a content type, you would use the Policy.CreatePolicy(SPContentType contentType, Policy globalPolicy) method. Essentially, if your content type policy is being declared at the site collection level, you can reuse an existing policy that was defined in the site collection, and assign it to your content type. However, if you want to assign a policy to a content type in a list, you would just pass in a null value.

To add a new expiration policy PolicyItem to the collection of PolicyItems in a content type’s Policy.Items property, you’ll use the PolicyItemCollection.Add(string policyFeatureId, string customData) method. As mentioned previously, the Policy Feature ID property value is “Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration”.

So, say I have a content type and I want to add my retention policy to it. I would first check to see if my content type had a policy already. If it didn’t, I’d go ahead and create a new one. Once I’ve created a new policy, I can circle back and retrieve it, and then add a new PolicyItem (which is my retention policy) to the collection of PolicyItems in that content type’s Policy, like this (using the GeneratePolicyItemXML() methods mentioned above):

bool areRecordsEnabled = (site.Features[new Guid("DA2E115B-07E4-49d9-BB2C-35E93BB9FCA9")] == null);

Policy policy = Policy.GetPolicy(contentType);
if (policy == null)
{
  Policy.CreatePolicy(<font face="Courier New">contentType</font>, null);
  policy = Policy.GetPolicy(<font face="Courier New">contentType</font>);
} 

if (areRecordsEnabled)
{
  policy.Items.Add("Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration", GeneratePolicyItemXML("1", "years", "Modified", "Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Action.MoveToRecycleBin", "6", "months", "Modified",  "Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Action.Delete"));
}
else
{
  policy.Items.Add("Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration", GeneratePolicyItemXML("1", "years", "Created", "Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Action.DeletePreviousVersions"));
}

contentType.Update();

Don’t forget to update the content type after you’ve added the expiration policy.